├── .gitignore ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── project.toml ├── screenshots ├── Screenshot1.png ├── Screenshot2.png ├── Screenshot3.png └── Screenshot4.png ├── src ├── __pycache__ │ ├── editor.cpython-310.pyc │ ├── lexer.cpython-310.pyc │ └── resources.cpython-310.pyc ├── autcompleter.py ├── css │ └── style.qss ├── editor.py ├── file_manager.py ├── fuzzy_searcher.py ├── icons │ ├── close-icon.svg │ ├── folder-icon-blue.svg │ ├── resouces.qrc │ └── search-icon.svg ├── lexer.py ├── main.py └── resources.py └── theme.json /.gitignore: -------------------------------------------------------------------------------- 1 | /venv 2 | src/__pycache__/*.pyc 3 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Start PPM", 8 | "type": "shell", 9 | "command": "ppm start" 10 | }, 11 | { 12 | "label": "Build Qrc", 13 | "type": "shell", 14 | "command": "ppm run build-qrc" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Asif Hossain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyqt-code-editor-yt 2 | 3 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/A0A0ETK5O) 4 | 5 | Part of a youtube series (Discontinued for now but still has alot of content) 6 | You can find it here: https://www.youtube.com/playlist?list=PL_4y7TZU8zkc20vgwQSUG_FcwjxedfsjD 7 | 8 | ### Improved version 9 | [**Neutron**](https://github.com/Fus3n/neutron), This is what the actual plan for the series was and where it was suposed to go, this still has alot missing but its overal alot improved from the last commit of the yt series, so if you know what you are doing you can take code from this to improve the current youtube version. 10 | 11 | ## Screenshots 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | More features will be added in the future 25 | -------------------------------------------------------------------------------- /project.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "editor-yt" 3 | version = "0.1.0" 4 | description = "" 5 | main_script = "./src/main.py" 6 | 7 | [packages] 8 | PyQt5 = "5.15.7" 9 | QScintilla = "2.13.3" 10 | jedi = "0.18.1" 11 | 12 | [scripts] 13 | build-qrc = "pyrcc5 ./src/icons/resouces.qrc -o ./src/resources.py" 14 | upgrade-pip = "python -m pip install --upgrade pip" 15 | -------------------------------------------------------------------------------- /screenshots/Screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot1.png -------------------------------------------------------------------------------- /screenshots/Screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot2.png -------------------------------------------------------------------------------- /screenshots/Screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot3.png -------------------------------------------------------------------------------- /screenshots/Screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/screenshots/Screenshot4.png -------------------------------------------------------------------------------- /src/__pycache__/editor.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/src/__pycache__/editor.cpython-310.pyc -------------------------------------------------------------------------------- /src/__pycache__/lexer.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/src/__pycache__/lexer.cpython-310.pyc -------------------------------------------------------------------------------- /src/__pycache__/resources.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fus3n/pyqt-code-editor-yt/7cd226a038dee402aeb2378f3fe3b3db463325b4/src/__pycache__/resources.cpython-310.pyc -------------------------------------------------------------------------------- /src/autcompleter.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QThread 2 | from PyQt5.Qsci import QsciAPIs 3 | from jedi import Script 4 | from jedi.api import Completion 5 | 6 | 7 | class AutoCompleter(QThread): 8 | def __init__(self, file_path, api): 9 | super(AutoCompleter, self).__init__(None) 10 | self.file_path = file_path 11 | self.script: Script = None 12 | self.api: QsciAPIs = api 13 | self.completions: list[Completion] = None 14 | 15 | 16 | self.line = 0 17 | self.index = 0 18 | self.text = "" 19 | 20 | def run(self): 21 | try: 22 | self.script = Script(self.text, path=self.file_path) 23 | self.completions = self.script.complete(self.line, self.index) 24 | self.load_autocomplete(self.completions) 25 | except Exception as err: 26 | print(err) 27 | 28 | self.finished.emit() 29 | 30 | def load_autocomplete(self, completions): 31 | self.api.clear() 32 | [self.api.add(i.name) for i in completions] 33 | self.api.prepare() 34 | 35 | def get_completions(self, line: int, index: int, text: str): 36 | self.line = line 37 | self.index = index 38 | self.text = text 39 | self.start() 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/css/style.qss: -------------------------------------------------------------------------------- 1 | QMainWindow { 2 | background-color: #282c34; 3 | color: #d3d3d3; 4 | } 5 | 6 | 7 | QTabWidget { 8 | background-color: #282c34; 9 | color: #d3d3d3; 10 | } 11 | 12 | 13 | QTabBar::tab { 14 | background-color: rgb(36, 36, 36); 15 | color: #d3d3d3; 16 | min-width: 10ex; 17 | min-height: 10ex; 18 | padding: 10px 20px; 19 | border-style: none; 20 | } 21 | 22 | 23 | QTabBar::tab::selected { 24 | color: #d3d3d3; 25 | border-style: none; 26 | background-color: #2d2d2d; 27 | border-bottom: 2px solid #d3d3d3; 28 | } 29 | 30 | 31 | QTabBar::close-button { 32 | image: url(":/icons/close-icon.svg"); 33 | } 34 | 35 | 36 | QSplitter { 37 | background-color: #282c34; 38 | } 39 | 40 | QSplitter::handle { 41 | background-color: #2d2d2d; 42 | } 43 | 44 | 45 | QScrollBar:vertical { 46 | border: none; 47 | width: 14px; 48 | margin: 15px 0 15px 0; 49 | background-color: #2d2d2d; 50 | border-radius: 0px; 51 | } 52 | 53 | 54 | QScrollBar:handle:vertical { 55 | background-color: #3e3d3d; 56 | } 57 | 58 | QScrollBar:handle:vertical:hover { 59 | background-color: #4c4a4a; 60 | } 61 | 62 | 63 | QScrollBar:handle:vertical:pressed { 64 | background-color: #5c5b5b; 65 | } 66 | 67 | 68 | QScrollBar::sub-line::vertical { 69 | border: none; 70 | background: none; 71 | height: 15px; 72 | border-top-left-radius: 7px; 73 | border-top-right-radius: 7px; 74 | subcontrol-position: top; 75 | subcontrol-origin: margin; 76 | } 77 | 78 | QScrollBar::add-line::vertical { 79 | border: none; 80 | background: none; 81 | height: 15px; 82 | border-top-left-radius: 7px; 83 | border-top-right-radius: 7px; 84 | subcontrol-position: bottom; 85 | subcontrol-origin: margin; 86 | } 87 | 88 | 89 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 90 | background: none; 91 | } 92 | 93 | 94 | QScrollBar:horizontal { 95 | border: none; 96 | height: 14px; 97 | margin: 0px 0 0 0; 98 | background-color: #2d2d2d; 99 | border-radius: 0px; 100 | } 101 | 102 | 103 | QScrollBar:handle:horizontal { 104 | background-color: #3e3d3d; 105 | } 106 | 107 | QScrollBar:handle:horizontal:hover { 108 | background-color: #4c4a4a; 109 | } 110 | 111 | QScrollBar:handle:horizontal:pressed { 112 | background-color: #5c5b5b; 113 | } 114 | 115 | 116 | QScrollBar::sub-line::horizontal { 117 | border: none; 118 | background: none; 119 | width: 15px; 120 | border-top-left-radius: 7px; 121 | border-top-right-radius: 7px; 122 | subcontrol-position: top; 123 | subcontrol-origin: margin; 124 | } 125 | 126 | QScrollBar::add-line::horizontal { 127 | border: none; 128 | background: none; 129 | width: 15px; 130 | border-top-left-radius: 7px; 131 | border-top-right-radius: 7px; 132 | subcontrol-position: bottom; 133 | subcontrol-origin: margin; 134 | } 135 | 136 | 137 | QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { 138 | background: none; 139 | } 140 | 141 | 142 | QsciScintilla { 143 | border: none; 144 | background-color: #282c34; 145 | color: #D3D3D3; 146 | } 147 | 148 | 149 | QMessageBox { 150 | color: white; 151 | border-radius: 30px; 152 | } 153 | 154 | QLineEdit { 155 | background-color: #21252b; 156 | border-radius: 4px; 157 | border: 1px solid #d3d3d3; 158 | padding: 3px; 159 | color: #D3D3D3; 160 | min-height: 6ex; 161 | } 162 | 163 | QLineEditBox:hover { 164 | color: white; 165 | } 166 | 167 | QListView { 168 | background-color: #21252b; 169 | border-radius: 5px; 170 | border: 1px solid #D3D3D3; 171 | padding: 5px; 172 | color: #d3d3d3; 173 | } 174 | QListView::item:hover { 175 | background-color: #323842; 176 | border: 1px soid #d3d3d3; 177 | border-radius: 3px; 178 | color: #d3d3d3; 179 | } 180 | 181 | QListView::item:selected { 182 | background-color: #323842; 183 | border-style: none; 184 | color: #D3D3D3; 185 | } 186 | 187 | QMenuBar { 188 | background-color: #282c34; 189 | border: 1px soid #21252b; 190 | padding: 3px; 191 | color: #D3D3D3; 192 | font-size: 15px; 193 | } 194 | 195 | QMenuBar::item { 196 | background-color: #282c34; 197 | padding: 3px; 198 | color: #d3d3d3; 199 | } -------------------------------------------------------------------------------- /src/editor.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5.QtCore import * 3 | from PyQt5.QtGui import * 4 | 5 | from PyQt5.Qsci import * 6 | 7 | 8 | import keyword 9 | import pkgutil 10 | from pathlib import Path 11 | from lexer import PyCustomLexer 12 | from autcompleter import AutoCompleter 13 | from typing import TYPE_CHECKING 14 | 15 | if TYPE_CHECKING: 16 | from main import MainWindow 17 | 18 | import resources 19 | 20 | class Editor(QsciScintilla): 21 | 22 | def __init__(self, main_window, parent=None, path: Path = None, is_python_file=True): 23 | super(Editor, self).__init__(parent) 24 | 25 | self.main_window: MainWindow = main_window 26 | self._current_file_changed = False 27 | self.first_launch = True 28 | 29 | self.path = path 30 | self.full_path = self.path.absolute() 31 | self.is_python_file = is_python_file 32 | 33 | # EDITOR 34 | self.cursorPositionChanged.connect(self._cusorPositionChanged) 35 | self.textChanged.connect(self._textChanged) 36 | 37 | # encoding 38 | self.setUtf8(True) 39 | # Font 40 | self.window_font = QFont("Fire Code") # font needs to be installed in your computer if its not use something else 41 | self.window_font.setPointSize(12) 42 | self.setFont(self.window_font) 43 | 44 | # brace matching 45 | self.setBraceMatching(QsciScintilla.SloppyBraceMatch) 46 | 47 | # indentation 48 | self.setIndentationGuides(True) 49 | self.setTabWidth(4) 50 | self.setIndentationsUseTabs(False) 51 | self.setAutoIndent(True) 52 | 53 | # autocomplete 54 | self.setAutoCompletionSource(QsciScintilla.AcsAll) 55 | self.setAutoCompletionThreshold(1) 56 | self.setAutoCompletionCaseSensitivity(False) 57 | self.setAutoCompletionUseSingle(QsciScintilla.AcusNever) 58 | 59 | 60 | # caret 61 | self.setCaretForegroundColor(QColor("#dedcdc")) 62 | self.setCaretLineVisible(True) 63 | self.setCaretWidth(2) 64 | self.setCaretLineBackgroundColor(QColor("#2c313c")) 65 | 66 | 67 | # EOL 68 | self.setEolMode(QsciScintilla.EolWindows) 69 | self.setEolVisibility(False) 70 | 71 | 72 | if self.is_python_file: 73 | # lexer for syntax highlighting 74 | self.pylexer = PyCustomLexer(self) 75 | self.pylexer.setDefaultFont(self.window_font) 76 | 77 | self.__api = QsciAPIs(self.pylexer) 78 | 79 | self.auto_completer = AutoCompleter(self.full_path, self.__api) 80 | self.auto_completer.finished.connect(self.loaded_autocomplete) # you can use this callback to do something 81 | 82 | self.setLexer(self.pylexer) 83 | else: 84 | self.setPaper(QColor("#282c34")) 85 | self.setColor(QColor("#abb2bf")) 86 | 87 | 88 | # line numbers 89 | self.setMarginType(0, QsciScintilla.NumberMargin) 90 | self.setMarginWidth(0, "000") 91 | self.setMarginsForegroundColor(QColor("#ff888888")) 92 | self.setMarginsBackgroundColor(QColor("#282c34")) 93 | self.setMarginsFont(self.window_font) 94 | 95 | # key press 96 | # self.keyPressEvent = self.handle_editor_press 97 | 98 | @property 99 | def current_file_changed(self): 100 | return self._current_file_changed 101 | 102 | @current_file_changed.setter 103 | def current_file_changed(self, value: bool): 104 | curr_index = self.main_window.tab_view.currentIndex() 105 | if value: 106 | self.main_window.tab_view.setTabText(curr_index, "*"+self.path.name) 107 | self.main_window.setWindowTitle(f"*{self.path.name} - {self.main_window.app_name}") 108 | else: 109 | if self.main_window.tab_view.tabText(curr_index).startswith("*"): 110 | self.main_window.tab_view.setTabText( 111 | curr_index, 112 | self.main_window.tab_view.tabText(curr_index)[1:] 113 | ) 114 | self.main_window.setWindowTitle(self.main_window.windowTitle()[1:]) 115 | 116 | self._current_file_changed = value 117 | 118 | def toggle_comment(self, text: str): 119 | lines = text.split('\n') 120 | toggled_lines = [] 121 | for line in lines: 122 | if line.startswith('#'): 123 | toggled_lines.append(line[1:].lstrip()) 124 | else: 125 | toggled_lines.append("# " + line) 126 | 127 | return '\n'.join(toggled_lines) 128 | 129 | def keyPressEvent(self, e: QKeyEvent) -> None: 130 | if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_Space: 131 | if self.is_python_file: 132 | pos = self.getCursorPosition() 133 | self.auto_completer.get_completions(pos[0]+1, pos[1], self.text()) 134 | self.autoCompleteFromAPIs() 135 | return 136 | 137 | if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_X: # CUT SHORTCUT 138 | if not self.hasSelectedText(): 139 | line, index = self.getCursorPosition() 140 | self.setSelection(line, 0, line, self.lineLength(line)) 141 | self.cut() 142 | return 143 | 144 | if e.modifiers() == Qt.ControlModifier and e.text() == "/": # COMMENT SHORTCUT 145 | if self.hasSelectedText(): 146 | start, srow, end, erow = self.getSelection() 147 | self.setSelection(start, 0, end, self.lineLength(end)-1) 148 | self.replaceSelectedText(self.toggle_comment(self.selectedText())) 149 | self.setSelection(start, srow, end, erow) 150 | else: 151 | line, _ = self.getCursorPosition() 152 | self.setSelection(line, 0, line, self.lineLength(line)-1) 153 | self.replaceSelectedText(self.toggle_comment(self.selectedText())) 154 | self.setSelection(-1, -1, -1, -1) # reset selection 155 | return 156 | 157 | return super().keyPressEvent(e) 158 | 159 | def _cusorPositionChanged(self, line: int, index: int) -> None: 160 | if self.is_python_file: 161 | self.auto_completer.get_completions(line+1, index, self.text()) 162 | 163 | def loaded_autocomplete(self): 164 | pass 165 | 166 | def _textChanged(self): 167 | if not self.current_file_changed and not self.first_launch: 168 | self.current_file_changed = True 169 | 170 | if self.first_launch: 171 | self.first_launch = False -------------------------------------------------------------------------------- /src/file_manager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import * 2 | from PyQt5.QtCore import * 3 | from PyQt5.QtGui import * 4 | from PyQt5.Qsci import * 5 | 6 | from pathlib import Path 7 | import shutil 8 | import os 9 | import sys 10 | import subprocess 11 | 12 | from editor import Editor 13 | 14 | 15 | class FileManager(QTreeView): 16 | def __init__(self, tab_view, set_new_tab=None, main_window=None): 17 | super(FileManager, self).__init__(None) 18 | 19 | self.set_new_tab = set_new_tab 20 | self.tab_view = tab_view 21 | self.main_window = main_window 22 | 23 | # renaming feature variables 24 | #... 25 | 26 | self.manager_font = QFont("FiraCode", 13) # font must be installed on pc 27 | 28 | self.model: QFileSystemModel = QFileSystemModel() 29 | self.model.setRootPath(os.getcwd()) 30 | # File system filters 31 | self.model.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs | QDir.Files | QDir.Drives) 32 | self.model.setReadOnly(False) 33 | self.setFocusPolicy(Qt.NoFocus) 34 | 35 | self.setFont(self.manager_font) 36 | self.setModel(self.model) 37 | self.setRootIndex(self.model.index(os.getcwd())) 38 | self.setSelectionMode(QAbstractItemView.ExtendedSelection) 39 | self.setSelectionBehavior(QTreeView.SelectRows) 40 | self.setEditTriggers(QTreeView.EditTrigger.NoEditTriggers) 41 | # addntext menu 42 | self.setContextMenuPolicy(Qt.CustomContextMenu) 43 | self.customContextMenuRequested.connect(self.show_context_menu) 44 | # hank 45 | self.clicked.connect(self.tree_view_clicked) 46 | self.setIndentation(10) 47 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 48 | # Hidnd hide other columns except for name 49 | self.setHeaderHidden(True) # hiding header 50 | self.setColumnHidden(1, True) 51 | self.setColumnHidden(2, True) 52 | self.setColumnHidden(3, True) 53 | 54 | # enable drag and drop 55 | self.setDragEnabled(True) 56 | self.setAcceptDrops(True) 57 | self.setDropIndicatorShown(True) 58 | self.setDragDropMode(QAbstractItemView.DragDrop) 59 | 60 | # enable file name editing 61 | 62 | # renaming 63 | self.previous_rename_name = None 64 | self.is_renaming = False 65 | self.current_edit_index = None 66 | 67 | self.itemDelegate().closeEditor.connect(self._on_closeEditor) 68 | 69 | def _on_closeEditor(self, editor: QLineEdit): 70 | if self.is_renaming: 71 | self.rename_file_with_index() 72 | 73 | def tree_view_clicked(self, index: QModelIndex): 74 | path = self.model.filePath(index) 75 | p = Path(path) 76 | if p.is_file(): 77 | self.set_new_tab(p) 78 | 79 | def show_context_menu(self, pos: QPoint): 80 | ix = self.indexAt(pos) 81 | menu = QMenu() 82 | menu.addAction("New File") 83 | menu.addAction("New Folder") 84 | menu.addAction("Open In File Manager") 85 | 86 | if ix.column() == 0: 87 | menu.addAction("Rename") 88 | menu.addAction("Delete") 89 | 90 | action = menu.exec_(self.viewport().mapToGlobal(pos)) 91 | 92 | if not action: 93 | return 94 | 95 | if action.text() == "Rename": 96 | self.action_rename(ix) 97 | elif action.text() == "Delete": 98 | self.action_delete(ix) 99 | elif action.text() == "New Folder": 100 | self.action_new_folder() 101 | elif action.text() == "New File": 102 | self.action_new_file(ix) 103 | elif action.text() == "Open In File Manager": 104 | self.action_open_in_file_manager(ix) 105 | else: 106 | pass 107 | 108 | def show_dialog(self, title, msg) -> int: 109 | dialog = QMessageBox(self) 110 | dialog.setFont(self.manager_font) 111 | dialog.font().setPointSize(14) 112 | dialog.setWindowTitle(title) 113 | dialog.setWindowIcon(QIcon(":/icons/close-icon.svg")) 114 | dialog.setText(msg) 115 | dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No) 116 | dialog.setDefaultButton(QMessageBox.No) 117 | dialog.setIcon(QMessageBox.Warning) 118 | return dialog.exec_() 119 | 120 | def rename_file_with_index(self): 121 | new_name = self.model.fileName(self.current_edit_index) 122 | if self.previous_rename_name == new_name: 123 | return 124 | 125 | # loop over all the tabs open and find the one with the old name 126 | for editor in self.tab_view.findChildren(Editor): # finding all children of type Editor 127 | if editor.path.name == self.previous_rename_name: # the editor should keep a path vatriable 128 | editor.path = editor.path.parent / new_name 129 | self.tab_view.setTabText( 130 | self.tab_view.indexOf(editor), new_name 131 | ) 132 | self.tab_view.repaint() 133 | editor.full_path = editor.path.absolute() # changing the editor instances full_path variable 134 | self.main_window.current_file = editor.path 135 | break 136 | 137 | def action_rename(self, ix): 138 | self.edit(ix) 139 | self.previous_rename_name = self.model.fileName(ix) 140 | self.is_renaming = True 141 | self.current_edit_index = ix 142 | 143 | def delete_file(self, path: Path): 144 | if path.is_dir(): 145 | shutil.rmtree(path) 146 | else: 147 | path.unlink() 148 | 149 | def action_delete(self, ix): 150 | # check if selection 151 | file_name = self.model.fileName(ix) 152 | dialog = self.show_dialog( 153 | "Delete", f"Are you sure you want to delete {file_name}" 154 | ) 155 | if dialog == QMessageBox.Yes: 156 | if self.selectionModel().selectedRows(): 157 | for i in self.selectionModel().selectedRows(): 158 | path = Path(self.model.filePath(i)) 159 | self.delete_file(path) 160 | for editor in self.tab_view.findChildren(Editor): 161 | if editor.path.name == path.name: 162 | self.tab_view.removeTab( 163 | self.tab_view.indexOf(editor) 164 | ) 165 | 166 | def action_new_file(self, ix: QModelIndex): 167 | root_path = self.model.rootPath() 168 | if ix.column() != -1 and self.model.isDir(ix): 169 | self.expand(ix) 170 | root_path = self.model.filePath(ix) 171 | 172 | f = Path(root_path) / "file" 173 | count = 1 174 | while f.exists(): 175 | f = Path(f.parent / f"file{count}") 176 | count += 1 177 | f.touch() 178 | idx = self.model.index(str(f.absolute())) 179 | self.edit(idx) 180 | 181 | def action_new_folder(self): 182 | f = Path(self.model.rootPath()) / "New Folder" 183 | count = 1 184 | while f.exists(): 185 | f = Path(f.parent / f"New Folder{count}") 186 | count += 1 187 | idx = self.model.mkdir(self.rootIndex(), f.name) 188 | # edit that index 189 | self.edit(idx) 190 | 191 | def action_open_in_file_manager(self, ix: QModelIndex): 192 | path = os.path.abspath(self.model.filePath(ix)) 193 | is_dir = self.model.isDir(ix) 194 | if os.name == "nt": 195 | # Windows 196 | if is_dir: 197 | subprocess.Popen(f'explorer "{path}"') 198 | else: 199 | subprocess.Popen(f'explorer /select,"{path}"') 200 | elif os.name == "posix": 201 | # Linux or Mac OS 202 | if sys.platform == "darwin": 203 | # macOS 204 | if is_dir: 205 | subprocess.Popen(["open", path]) 206 | else: 207 | subprocess.Popen(["open", "-R", path]) 208 | else: 209 | # Linux 210 | subprocess.Popen(["xdg-open", os.path.dirname(path)]) 211 | else: 212 | raise OSError(f"Unsupported platform {os.name}") 213 | 214 | # drag and drop functionality 215 | def dragEnterEvent(self, e: QDragEnterEvent) -> None: 216 | if e.mimeData().hasUrls(): 217 | e.accept() 218 | else: 219 | e.ignore() 220 | 221 | def dropEvent(self, e: QDropEvent) -> None: 222 | root_path = Path(self.model.rootPath()) 223 | if e.mimeData().hasUrls(): 224 | for url in e.mimeData().urls(): 225 | path = Path(url.toLocalFile()) 226 | if path.is_dir(): 227 | shutil.copytree(path, root_path / path.name) 228 | else: 229 | if root_path.samefile(self.model.rootPath()): 230 | idx: QModelIndex = self.indexAt(e.pos()) 231 | if idx.column() == -1: 232 | shutil.move(path, root_path / path.name) 233 | else: 234 | folder_path = Path(self.model.filePath(idx)) 235 | shutil.move(path, folder_path / path.name) 236 | else: 237 | shutil.copy(path, root_path / path.name) 238 | e.accept() 239 | 240 | return super().dropEvent(e) 241 | 242 | -------------------------------------------------------------------------------- /src/fuzzy_searcher.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QThread, pyqtSignal 2 | from PyQt5.QtWidgets import QListWidgetItem 3 | 4 | import os 5 | from pathlib import Path 6 | import re 7 | 8 | 9 | 10 | class SearchItem(QListWidgetItem): 11 | def __init__(self, name, full_path, lineno, end, line): 12 | self.name = name 13 | self.full_path = full_path 14 | self.lineno = lineno 15 | self.end = end 16 | self.line = line 17 | self.formatted = f'{self.name}:{self.lineno}:{self.end} - {self.line} ...' 18 | super().__init__(self.formatted) 19 | 20 | 21 | def __str__(self): 22 | return self.formatted 23 | 24 | def __repr__(self): 25 | return self.formatted 26 | 27 | 28 | class SearchWorker(QThread): 29 | finished = pyqtSignal(list) 30 | 31 | def __init__(self): 32 | super(SearchWorker, self).__init__(None) 33 | self.items = [] 34 | self.search_path: str = None 35 | self.search_text: str = None 36 | self.search_project: bool = None 37 | 38 | def is_binary(self, path): 39 | ''' 40 | Check if file is binary 41 | ''' 42 | with open(path, 'rb') as f: 43 | return b'\0' in f.read(1024) 44 | 45 | def walkdir(self, path, exclude_dirs: list, exclude_files: list): 46 | for root, dirs, files, in os.walk(path, topdown=True): 47 | # filtering 48 | dirs[:] = [d for d in dirs if d not in exclude_dirs] 49 | files[:] = [f for f in files if Path(f).suffix not in exclude_files] 50 | yield root, dirs, files 51 | 52 | def search(self): 53 | debug = False 54 | self.items = [] 55 | # you can add more 56 | exclude_dirs = set([".git", ".svn", ".hg", ".bzr", ".idea", "__pycache__", "venv"]) 57 | if self.search_project: 58 | exclude_dirs.remove("venv") 59 | exclude_files = set([".svg", ".png", ".exe", ".pyc", ".qm"]) 60 | 61 | for root, _, files in self.walkdir(self.search_path, exclude_dirs, exclude_files): 62 | # total search limit 63 | if len(self.items) > 5_000: 64 | break 65 | for file_ in files: 66 | full_path = os.path.join(root, file_) 67 | if self.is_binary(full_path): 68 | break 69 | 70 | try: 71 | with open(full_path, 'r', encoding='utf8') as f: 72 | try: 73 | reg = re.compile(self.search_text, re.IGNORECASE) 74 | for i, line in enumerate(f): 75 | if m := reg.search(line): 76 | fd = SearchItem( 77 | file_, 78 | full_path, 79 | i, 80 | m.end(), 81 | line[m.start():].strip()[:50], # limiting to 50 chars 82 | ) 83 | self.items.append(fd) 84 | except re.error as e: 85 | if debug: print(e) 86 | except UnicodeDecodeError as e: 87 | if debug: print(e) 88 | continue 89 | 90 | self.finished.emit(self.items) 91 | 92 | def run(self): 93 | self.search() 94 | 95 | def update(self, pattern, path, search_project): 96 | self.search_text = pattern 97 | self.search_path = path 98 | self.search_project = search_project 99 | self.start() 100 | -------------------------------------------------------------------------------- /src/icons/close-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/folder-icon-blue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icons/resouces.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | close-icon.svg 4 | folder-icon-blue.svg 5 | search-icon.svg 6 | 7 | -------------------------------------------------------------------------------- /src/icons/search-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lexer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import keyword 3 | import builtins 4 | import types 5 | import json 6 | 7 | from PyQt5.Qsci import QsciLexerCustom, QsciScintilla 8 | from PyQt5.QtGui import QFont, QColor 9 | from PyQt5.QtCore import * 10 | from PyQt5.QtWidgets import * 11 | 12 | 13 | # config type 14 | DefaultConfig = dict[str, str, tuple[str, int]] 15 | 16 | class NeutronLexer(QsciLexerCustom): 17 | """Base Custo Lexer class for all language""" 18 | 19 | def __init__(self, language_name, editor, theme=None, defaults: DefaultConfig = None): 20 | super(NeutronLexer, self).__init__(editor) 21 | 22 | self.editor = editor 23 | self.language_name = language_name 24 | self.theme_json = None 25 | if theme is None: 26 | self.theme = "./theme.json" 27 | else: 28 | self.theme = theme 29 | 30 | self.token_list: list[str, str] = [] 31 | 32 | self.keywords_list = [] 33 | self.builtin_names = [] 34 | 35 | if defaults is None: 36 | defaults: DefaultConfig = {} 37 | defaults["color"] = "#abb2bf" 38 | defaults["paper"] = "#282c34" 39 | defaults["font"] = ("Consolas", 14) 40 | 41 | 42 | # Default text settings 43 | self.setDefaultColor(QColor(defaults["color"])) 44 | self.setDefaultPaper(QColor(defaults["paper"])) 45 | self.setDefaultFont(QFont(defaults["font"][0], defaults["font"][1])) 46 | 47 | self._init_theme_vars() 48 | self._init_theme() 49 | 50 | def setKeywords(self, keywords: list[str]): 51 | """Set List of strings that considered keywords for this language.""" 52 | self.keywords_list =keywords 53 | 54 | def setBuiltinNames(self, buitin_names: list[str]): 55 | """Set list of builtin names""" 56 | self.builtin_names = buitin_names 57 | 58 | def _init_theme_vars(self): 59 | # color per style 60 | 61 | self.DEFAULT = 0 62 | self.KEYWORD = 1 63 | self.TYPES = 2 64 | self.STRING = 3 65 | self.KEYARGS = 4 66 | self.BRACKETS = 5 67 | self.COMMENTS = 6 68 | self.CONSTANTS = 7 69 | self.FUNCTIONS = 8 70 | self.CLASSES = 9 71 | self.FUNCTION_DEF = 10 72 | 73 | self.default_names = [ 74 | "default", 75 | "keyword", 76 | "types", 77 | "string", 78 | "keyargs", 79 | "brackets", 80 | "comments", 81 | "constants", 82 | "functions", 83 | "classes", 84 | "function_def" 85 | ] 86 | 87 | self.font_weights = { 88 | "thin": QFont.Thin, 89 | "extralight": QFont.ExtraLight, 90 | "light": QFont.Light, 91 | "normal": QFont.Normal, 92 | "medium": QFont.Medium, 93 | "demibold": QFont.DemiBold, 94 | "bold": QFont.Bold, 95 | "extrabold": QFont.ExtraBold, 96 | "black": QFont.Black, 97 | } 98 | 99 | def _init_theme(self): 100 | with open(self.theme, "r") as f: 101 | self.theme_json = json.load(f) 102 | 103 | colors = self.theme_json["theme"]["syntax"] 104 | 105 | for clr in colors: 106 | name: str = list(clr.keys())[0] 107 | 108 | if name not in self.default_names: 109 | print(f"Theme error: {name} is not a valid style name") 110 | continue 111 | 112 | for k, v in clr[name].items(): 113 | if k == "color": 114 | self.setColor(QColor(v), getattr(self, name.upper())) 115 | elif k == "paper-color": 116 | self.setPaper(QColor(v), getattr(self, name.upper())) 117 | elif k == "font": 118 | try: 119 | self.setFont( 120 | QFont( 121 | v.get("family", "Consolas"), 122 | v.get("font-size", 14), 123 | self.font_weights.get(v.get("font-weight", QFont.Normal)), 124 | v.get("italic", False) 125 | ), 126 | getattr(self, name.upper()) 127 | ) 128 | except AttributeError as e: 129 | print(f"theme error: {e}") 130 | 131 | 132 | def language(self) -> str: 133 | return self.language_name 134 | 135 | def description(self, style: int) -> str: 136 | if style == self.DEFAULT: 137 | return "DEFAULT" 138 | elif style == self.KEYWORD: 139 | return "KEYWORD" 140 | elif style == self.TYPES: 141 | return "TYPES" 142 | elif style == self.STRING: 143 | return "STRING" 144 | elif style == self.KEYARGS: 145 | return "KEYARGS" 146 | elif style == self.BRACKETS: 147 | return "BRACKETS" 148 | elif style == self.COMMENTS: 149 | return "COMMENTS" 150 | elif style == self.CONSTANTS: 151 | return "CONSTANTS" 152 | elif style == self.FUNCTIONS: 153 | return "FUNCTIONS" 154 | elif style == self.CLASSES: 155 | return "CLASSES" 156 | elif style == self.FUNCTION_DEF: 157 | return "FUNCTION_DEF" 158 | 159 | return "" 160 | 161 | def generate_token(self, text): 162 | # 3. Tokenize the text 163 | # --------------------- 164 | p = re.compile(r"[*]\/|\/[*]|\s+|\w+|\W") 165 | 166 | # 'token_list' is a list of tuples: (token_name, token_len), ex: '(class, 5)' 167 | self.token_list = [ (token, len(bytearray(token, "utf-8"))) for token in p.findall(text)] 168 | 169 | def next_tok(self, skip: int = None): 170 | if len(self.token_list) > 0: 171 | if skip is not None and skip != 0: 172 | for _ in range(skip-1): 173 | if len(self.token_list) > 0: 174 | self.token_list.pop(0) 175 | return self.token_list.pop(0) 176 | else: 177 | return None 178 | 179 | def peek_tok(self, n=0): 180 | try: 181 | return self.token_list[n] 182 | except IndexError: 183 | return [''] 184 | 185 | def skip_spaces_peek(self, skip=None): 186 | """find he next non-space token but using peek without consuming it""" 187 | i = 0 188 | tok = " " 189 | if skip is not None: 190 | i = skip 191 | while tok[0].isspace(): 192 | tok = self.peek_tok(i) 193 | i += 1 194 | return tok, i 195 | 196 | 197 | class PyCustomLexer(NeutronLexer): 198 | """Custom lexer for python""" 199 | 200 | def __init__(self, editor): 201 | super(PyCustomLexer, self).__init__("Python", editor) 202 | 203 | self.setKeywords(keyword.kwlist) 204 | self.setBuiltinNames([ 205 | name 206 | for name, obj in vars(builtins).items() 207 | if isinstance(obj, types.BuiltinFunctionType) 208 | ]) 209 | 210 | def styleText(self, start: int, end: int) -> None: 211 | # 1. Start styling procedure 212 | self.startStyling(start) 213 | 214 | # 2. Slice out part from the text 215 | text = self.editor.text()[start:end] 216 | 217 | # 3. Tokenize the text 218 | self.generate_token(text) 219 | 220 | # Flags 221 | string_flag = False 222 | comment_flag = False 223 | 224 | if start > 0: 225 | prev_style = self.editor.SendScintilla(self.editor.SCI_GETSTYLEAT, start -1) 226 | if prev_style == self.COMMENTS: 227 | comment_flag = False 228 | 229 | while True: 230 | curr_token = self.next_tok() 231 | 232 | if curr_token is None: 233 | break 234 | 235 | tok: str = curr_token[0] 236 | tok_len: int = curr_token[1] 237 | 238 | if comment_flag: 239 | self.setStyling(tok_len, self.COMMENTS) 240 | if tok.startswith("\n"): 241 | comment_flag = False 242 | continue 243 | 244 | 245 | if string_flag: 246 | self.setStyling(tok_len, self.STRING) 247 | if tok == '"' or tok == "'": 248 | string_flag = False 249 | continue 250 | 251 | if tok == "class": 252 | name, ni = self.skip_spaces_peek() 253 | brac_or_colon, _ = self.skip_spaces_peek(ni) 254 | if name[0].isidentifier() and brac_or_colon[0] in (":", "("): 255 | self.setStyling(tok_len, self.KEYWORD) 256 | _ = self.next_tok(ni) 257 | self.setStyling(name[1]+1, self.CLASSES) 258 | continue 259 | else: 260 | self.setStyling(tok_len, self.KEYWORD) 261 | continue 262 | elif tok == "def": 263 | name, ni = self.skip_spaces_peek() 264 | if name[0].isidentifier(): 265 | self.setStyling(tok_len, self.KEYWORD) 266 | _ = self.next_tok(ni) 267 | self.setStyling(name[1]+1, self.FUNCTION_DEF) 268 | continue 269 | else: 270 | self.setStyling(tok_len, self.KEYWORD) 271 | continue 272 | elif tok in self.keywords_list: 273 | self.setStyling(tok_len, self.KEYWORD) 274 | elif tok.strip() == "." and self.peek_tok()[0].isidentifier(): 275 | self.setStyling(tok_len, self.DEFAULT) 276 | curr_token = self.next_tok() 277 | tok: str = curr_token[0] 278 | tok_len: int = curr_token[1] 279 | if self.peek_tok()[0] == "(": 280 | self.setStyling(tok_len, self.FUNCTIONS) 281 | else: 282 | self.setStyling(tok_len, self.DEFAULT) 283 | continue 284 | elif tok.isnumeric() or tok == 'self': 285 | self.setStyling(tok_len, self.CONSTANTS) 286 | elif tok in ["(", ")", "{", "}", "[", "]"]: 287 | self.setStyling(tok_len, self.BRACKETS) 288 | elif tok == '"' or tok == "'": 289 | self.setStyling(tok_len, self.STRING) 290 | string_flag = True 291 | elif tok == "#": 292 | self.setStyling(tok_len, self.COMMENTS) 293 | comment_flag = True 294 | elif tok in self.builtin_names or tok in ['+', '-', '*', '/', '%', '=', '<', '>']: 295 | self.setStyling(tok_len, self.TYPES) 296 | else: 297 | self.setStyling(tok_len, self.DEFAULT) -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtCore import * 4 | from PyQt5.QtGui import * 5 | 6 | from PyQt5.Qsci import * 7 | # test 8 | 9 | import sys 10 | from pathlib import Path 11 | 12 | from editor import Editor 13 | from fuzzy_searcher import SearchItem, SearchWorker 14 | from file_manager import FileManager 15 | 16 | 17 | class MainWindow(QMainWindow): 18 | def __init__(self): 19 | super(QMainWindow, self).__init__() 20 | # add before init 21 | self.side_bar_clr = "#282c34" 22 | 23 | self.init_ui() 24 | 25 | self.current_file = None 26 | self.current_side_bar = None 27 | 28 | def init_ui(self): 29 | self.app_name = "PYQT EDITOR" 30 | self.setWindowTitle(self.app_name) 31 | self.resize(1300, 900) 32 | 33 | self.setStyleSheet(open("./src/css/style.qss", "r").read()) 34 | 35 | # alternative Consolas font 36 | self.window_font = QFont("Fire Code") # font needs to be installed in your computer if its not use something else 37 | self.window_font.setPointSize(12) 38 | self.setFont(self.window_font) 39 | 40 | 41 | self.set_up_menu() 42 | self.set_up_body() 43 | self.set_up_status_bar() 44 | 45 | self.show() 46 | 47 | def set_up_status_bar(self): 48 | # Create status bar 49 | stat = QStatusBar(self) 50 | stat.setStyleSheet("color: #D3D3D3;") 51 | stat.showMessage("Ready", 3000) 52 | self.setStatusBar(stat) 53 | 54 | def set_up_menu(self): 55 | menu_bar = self.menuBar() 56 | 57 | # File Menu 58 | file_menu = menu_bar.addMenu("File") 59 | 60 | new_file = file_menu.addAction("New") 61 | new_file.setShortcut("Ctrl+N") 62 | new_file.triggered.connect(self.new_file) 63 | 64 | open_file = file_menu.addAction("Open File") 65 | open_file.setShortcut("Ctrl+O") 66 | open_file.triggered.connect(self.open_file) 67 | 68 | open_folder = file_menu.addAction("Open Folder") 69 | open_folder.setShortcut("Ctrl+K") 70 | open_folder.triggered.connect(self.open_folder) 71 | 72 | file_menu.addSeparator() 73 | 74 | save_file = file_menu.addAction("Save") 75 | save_file.setShortcut("Ctrl+S") 76 | save_file.triggered.connect(self.save_file) 77 | 78 | save_as = file_menu.addAction("Save As") 79 | save_as.setShortcut("Ctrl+Shift+S") 80 | save_as.triggered.connect(self.save_as) 81 | 82 | 83 | # Edit menu 84 | edit_menu = menu_bar.addMenu("Edit") 85 | 86 | copy_action = edit_menu.addAction("Copy") 87 | copy_action.setShortcut("Ctrl+C") 88 | copy_action.triggered.connect(self.copy) 89 | # you can add more 90 | 91 | def get_editor(self, path: Path = None, is_python_file=True) -> QsciScintilla: 92 | editor = Editor(self, path=path, is_python_file=is_python_file) 93 | return editor 94 | 95 | def is_binary(self, path): 96 | ''' 97 | Check if file is binary 98 | ''' 99 | with open(path, 'rb') as f: 100 | return b'\0' in f.read(1024) 101 | 102 | def set_new_tab(self, path: Path, is_new_file=False): 103 | if not is_new_file and self.is_binary(path): 104 | self.statusBar().showMessage("Cannot Open Binary File", 2000) 105 | return 106 | 107 | if path.is_dir(): 108 | return 109 | 110 | # add whichever extentions you consider as python file 111 | editor = self.get_editor(path, path.suffix in {".py", ".pyw"}) 112 | 113 | if is_new_file: 114 | self.tab_view.addTab(editor, "untitled") 115 | self.setWindowTitle("untitled - " + self.app_name) 116 | self.statusBar().showMessage("Opened untitled") 117 | self.tab_view.setCurrentIndex(self.tab_view.count() - 1) 118 | self.current_file = None 119 | return 120 | 121 | # check if file already open 122 | for i in range(self.tab_view.count()): 123 | if self.tab_view.tabText(i) == path.name or self.tab_view.tabText(i) == "*"+path.name: 124 | self.tab_view.setCurrentIndex(i) 125 | self.current_file = path 126 | return 127 | 128 | # create new tab 129 | self.tab_view.addTab(editor, path.name) 130 | editor.setText(path.read_text(encoding="utf-8")) 131 | self.setWindowTitle(f"{path.name} - {self.app_name}") 132 | self.current_file = path 133 | self.tab_view.setCurrentIndex(self.tab_view.count() - 1) 134 | self.statusBar().showMessage(f"Opened {path.name}", 2000) 135 | 136 | def set_cursor_pointer(self, e): 137 | self.setCursor(Qt.PointingHandCursor) 138 | 139 | def set_cursor_arrow(self, e): 140 | self.setCursor(Qt.ArrowCursor) 141 | 142 | def get_side_bar_label(self, path, name): 143 | label = QLabel() 144 | label.setPixmap(QPixmap(path).scaled(QSize(30, 30))) 145 | label.setAlignment(Qt.AlignmentFlag.AlignTop) 146 | label.setFont(self.window_font) 147 | label.mousePressEvent = lambda e: self.show_hide_tab(e, name) 148 | # Chaning Cursor on hover 149 | label.enterEvent = self.set_cursor_pointer 150 | label.leaveEvent = self.set_cursor_arrow 151 | return label 152 | 153 | def get_frame(self) -> QFrame: 154 | frame = QFrame() 155 | frame.setFrameShape(QFrame.NoFrame) 156 | frame.setFrameShadow(QFrame.Plain) 157 | frame.setContentsMargins(0, 0, 0, 0) 158 | frame.setStyleSheet(''' 159 | QFrame { 160 | background-color: #21252b; 161 | border-radius: 5px; 162 | border: none; 163 | padding: 5px; 164 | color: #D3D3D3; 165 | } 166 | QFrame:hover { 167 | color: white; 168 | } 169 | ''') 170 | return frame 171 | 172 | def set_up_body(self): 173 | 174 | # Body 175 | body_frame = QFrame() 176 | body_frame.setFrameShape(QFrame.NoFrame) 177 | body_frame.setFrameShadow(QFrame.Plain) 178 | body_frame.setLineWidth(0) 179 | body_frame.setMidLineWidth(0) 180 | body_frame.setContentsMargins(0, 0, 0, 0) 181 | body_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 182 | body = QHBoxLayout() 183 | body.setContentsMargins(0, 0, 0, 0) 184 | body.setSpacing(0) 185 | body_frame.setLayout(body) 186 | 187 | ############################## 188 | ###### TAB VIEW ########## 189 | 190 | # Tab Widget to add editor to 191 | self.tab_view = QTabWidget() 192 | self.tab_view.setContentsMargins(0, 0, 0, 0) 193 | self.tab_view.setTabsClosable(True) 194 | self.tab_view.setMovable(True) 195 | self.tab_view.setDocumentMode(True) 196 | self.tab_view.tabCloseRequested.connect(self.close_tab) 197 | 198 | ############################## 199 | ###### SIDE BAR ########## 200 | self.side_bar = QFrame() 201 | self.side_bar.setFrameShape(QFrame.StyledPanel) 202 | self.side_bar.setFrameShadow(QFrame.Plain) 203 | self.side_bar.setStyleSheet(f''' 204 | background-color: {self.side_bar_clr}; 205 | ''') 206 | side_bar_layout = QVBoxLayout() 207 | side_bar_layout.setContentsMargins(5, 10, 5, 0) 208 | side_bar_layout.setSpacing(0) 209 | side_bar_layout.setAlignment(Qt.AlignTop | Qt.AlignCenter) 210 | 211 | # setup labels 212 | folder_label = self.get_side_bar_label("./src/icons/folder-icon-blue.svg", "folder-icon") 213 | side_bar_layout.addWidget(folder_label) 214 | 215 | search_label = self.get_side_bar_label("./src/icons/search-icon", "search-icon") 216 | side_bar_layout.addWidget(search_label) 217 | 218 | self.side_bar.setLayout(side_bar_layout) 219 | 220 | 221 | # split view 222 | self.hsplit = QSplitter(Qt.Horizontal) 223 | 224 | ############################## 225 | ###### FILE MANAGER ########## 226 | 227 | # frame and layout to hold tree view (file manager) 228 | self.file_manager_frame = self.get_frame() 229 | self.file_manager_frame.setMaximumWidth(400) 230 | self.file_manager_frame.setMinimumWidth(200) 231 | 232 | self.file_manager_layout = QVBoxLayout() 233 | self.file_manager_layout.setContentsMargins(0, 0, 0, 0) 234 | self.file_manager_layout.setSpacing(0) 235 | 236 | self.file_manager = FileManager( 237 | tab_view=self.tab_view, 238 | set_new_tab=self.set_new_tab, 239 | main_window=self 240 | ) 241 | 242 | # setup layout 243 | self.file_manager_layout.addWidget(self.file_manager) 244 | self.file_manager_frame.setLayout(self.file_manager_layout) 245 | 246 | 247 | ############################## 248 | ###### SEARCH VIEW ########## 249 | self.search_frame = self.get_frame() 250 | self.search_frame.setMaximumWidth(400) 251 | self.search_frame.setMinimumWidth(200) 252 | 253 | search_layout = QVBoxLayout() 254 | search_layout.setAlignment(Qt.AlignmentFlag.AlignTop) 255 | search_layout.setContentsMargins(0, 10, 0, 0) 256 | search_layout.setSpacing(0) 257 | 258 | search_input = QLineEdit() 259 | search_input.setPlaceholderText("Search") 260 | search_input.setFont(self.window_font) 261 | search_input.setAlignment(Qt.AlignmentFlag.AlignTop) 262 | 263 | ############# CHECKBOX ################ 264 | self.search_checkbox = QCheckBox("Search in modules") 265 | self.search_checkbox.setFont(self.window_font) 266 | self.search_checkbox.setStyleSheet("color: white; margin-bottom: 10px;") 267 | 268 | self.search_worker = SearchWorker() 269 | self.search_worker.finished.connect(self.search_finshed) 270 | 271 | search_input.textChanged.connect( 272 | lambda text: self.search_worker.update( 273 | text, 274 | self.file_manager.model.rootDirectory().absolutePath(), 275 | self.search_checkbox.isChecked() 276 | ) 277 | ) 278 | 279 | 280 | ############################## 281 | ###### SEARCH ListView ########## 282 | self.search_list_view = QListWidget() 283 | self.search_list_view.setFont(QFont("FiraCode", 13)) 284 | self.search_list_view.setStyleSheet(""" 285 | QListWidget { 286 | background-color: #21252b; 287 | border-radius: 5px; 288 | border: 1px solid #D3D3D3; 289 | padding: 5px; 290 | color: #D3D3D3; 291 | } 292 | """) 293 | 294 | self.search_list_view.itemClicked.connect(self.search_list_view_clicked) 295 | 296 | 297 | search_layout.addWidget(self.search_checkbox) 298 | search_layout.addWidget(search_input) 299 | search_layout.addSpacerItem(QSpacerItem(5, 5, QSizePolicy.Minimum, QSizePolicy.Minimum)) 300 | 301 | search_layout.addWidget(self.search_list_view) 302 | self.search_frame.setLayout(search_layout) 303 | 304 | 305 | 306 | 307 | ############################## 308 | ###### SETUP WIDGETS ########## 309 | 310 | # add tree view and tab view 311 | self.hsplit.addWidget(self.file_manager_frame) 312 | self.hsplit.addWidget(self.tab_view) 313 | 314 | body.addWidget(self.side_bar) 315 | body.addWidget(self.hsplit) 316 | 317 | body_frame.setLayout(body) 318 | 319 | self.setCentralWidget(body_frame) 320 | 321 | def search_finshed(self, items): 322 | self.search_list_view.clear() 323 | for i in items: 324 | self.search_list_view.addItem(i) 325 | 326 | def search_list_view_clicked(self, item: SearchItem): 327 | self.set_new_tab(Path(item.full_path)) 328 | editor: Editor = self.tab_view.currentWidget() 329 | editor.setCursorPosition(item.lineno, item.end) 330 | editor.setFocus() 331 | 332 | def show_dialog(self, title, msg) -> int: 333 | dialog = QMessageBox(self) 334 | dialog.setFont(self.font()) 335 | dialog.font().setPointSize(14) 336 | dialog.setWindowTitle(title) 337 | dialog.setWindowIcon(QIcon(":/icons/close-icon.svg")) 338 | dialog.setText(msg) 339 | dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No) 340 | dialog.setDefaultButton(QMessageBox.No) 341 | dialog.setIcon(QMessageBox.Warning) 342 | return dialog.exec_() 343 | 344 | def close_tab(self, index): 345 | editor: Editor = self.tab_view.currentWidget() 346 | if editor.current_file_changed: 347 | dialog = self.show_dialog( 348 | "Close", f"Do you want to save the changes made to {self.current_file.name}?" 349 | ) 350 | if dialog == QMessageBox.Yes: 351 | self.save_file() 352 | 353 | self.tab_view.removeTab(index) 354 | 355 | def show_hide_tab(self, e, type_): 356 | if type_ == "folder-icon": 357 | if not (self.file_manager_frame in self.hsplit.children()): 358 | self.hsplit.replaceWidget(0, self.file_manager_frame) 359 | elif type_ == "search-icon": 360 | if not (self.search_frame in self.hsplit.children()): 361 | self.hsplit.replaceWidget(0, self.search_frame) 362 | 363 | if self.current_side_bar == type_: 364 | frame = self.hsplit.children()[0] 365 | if frame.isHidden(): 366 | frame.show() 367 | else: 368 | frame.hide() 369 | 370 | self.current_side_bar = type_ 371 | 372 | 373 | def tree_view_context_menu(self, pos): 374 | ... 375 | 376 | def new_file(self): 377 | self.set_new_tab(Path("untitled"), is_new_file=True) 378 | 379 | def save_file(self): 380 | if self.current_file is None and self.tab_view.count() > 0: 381 | self.save_as() 382 | 383 | editor = self.tab_view.currentWidget() 384 | self.current_file.write_text(editor.text()) 385 | self.statusBar().showMessage(f"Saved {self.current_file.name}", 2000) 386 | editor.current_file_changed = False 387 | 388 | def save_as(self): 389 | # save as 390 | editor = self.tab_view.currentWidget() 391 | if editor is None: 392 | return 393 | 394 | file_path = QFileDialog.getSaveFileName(self, "Save As", os.getcwd())[0] 395 | if file_path == '': 396 | self.statusBar().showMessage("Cancelled", 2000) 397 | return 398 | path = Path(file_path) 399 | path.write_text(editor.text()) 400 | self.tab_view.setTabText(self.tab_view.currentIndex(), path.name) 401 | self.statusBar().showMessage(f"Saved {path.name}", 2000) 402 | self.current_file = path 403 | editor.current_file_changed = False 404 | 405 | def open_file(self): 406 | # open file 407 | ops = QFileDialog.Options() # this is optional 408 | ops |= QFileDialog.DontUseNativeDialog 409 | # i will add support for opening multiple files later for now it can only open one at a time 410 | new_file, _ = QFileDialog.getOpenFileName(self, 411 | "Pick A File", "", "All Files (*);;Python Files (*.py)", 412 | options=ops) 413 | if new_file == '': 414 | self.statusBar().showMessage("Cancelled", 2000) 415 | return 416 | f = Path(new_file) 417 | self.set_new_tab(f) 418 | 419 | 420 | def open_folder(self): 421 | # open folder 422 | ops = QFileDialog.Options() # this is optional 423 | ops |= QFileDialog.DontUseNativeDialog 424 | 425 | new_folder = QFileDialog.getExistingDirectory(self, "Pick A Folder", "", options=ops) 426 | if new_folder: 427 | self.model.setRootPath(new_folder) 428 | self.tree_view.setRootIndex(self.model.index(new_folder)) 429 | self.statusBar().showMessage(f"Opened {new_folder}", 2000) 430 | 431 | def copy(self): 432 | editor = self.tab_view.currentWidget() 433 | if editor is not None: 434 | editor.copy() 435 | 436 | 437 | 438 | 439 | if __name__ == '__main__': 440 | app = QApplication([]) 441 | window = MainWindow() 442 | sys.exit(app.exec()) 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | -------------------------------------------------------------------------------- /src/resources.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Resource object code 4 | # 5 | # Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore 10 | 11 | qt_resource_data = b"\ 12 | \x00\x00\x02\x2c\ 13 | \x3c\ 14 | \x73\x76\x67\x20\x66\x69\x6c\x6c\x3d\x22\x23\x32\x32\x38\x42\x45\ 15 | \x36\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\ 16 | \x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\ 17 | \x2f\x73\x76\x67\x22\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ 18 | \x30\x20\x30\x20\x34\x38\x20\x34\x38\x22\x20\x77\x69\x64\x74\x68\ 19 | \x3d\x22\x31\x34\x34\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ 20 | \x22\x31\x34\x34\x70\x78\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\ 21 | \x22\x4d\x20\x33\x39\x2e\x34\x38\x36\x33\x32\x38\x20\x36\x2e\x39\ 22 | \x37\x38\x35\x31\x35\x36\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\ 23 | \x20\x31\x2e\x35\x30\x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\x33\ 24 | \x38\x2e\x34\x33\x39\x34\x35\x33\x20\x37\x2e\x34\x33\x39\x34\x35\ 25 | \x33\x31\x20\x4c\x20\x32\x34\x20\x32\x31\x2e\x38\x37\x38\x39\x30\ 26 | \x36\x20\x4c\x20\x39\x2e\x35\x36\x30\x35\x34\x36\x39\x20\x37\x2e\ 27 | \x34\x33\x39\x34\x35\x33\x31\x20\x41\x20\x31\x2e\x35\x30\x30\x31\ 28 | \x35\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\ 29 | \x38\x2e\x34\x38\x34\x33\x37\x35\x20\x36\x2e\x39\x38\x34\x33\x37\ 30 | \x35\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\x2e\x35\x30\ 31 | \x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\x37\x2e\x34\x33\x39\x34\ 32 | \x35\x33\x31\x20\x39\x2e\x35\x36\x30\x35\x34\x36\x39\x20\x4c\x20\ 33 | \x32\x31\x2e\x38\x37\x38\x39\x30\x36\x20\x32\x34\x20\x4c\x20\x37\ 34 | \x2e\x34\x33\x39\x34\x35\x33\x31\x20\x33\x38\x2e\x34\x33\x39\x34\ 35 | \x35\x33\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\x2e\x35\ 36 | \x30\x30\x31\x35\x20\x30\x20\x31\x20\x30\x20\x39\x2e\x35\x36\x30\ 37 | \x35\x34\x36\x39\x20\x34\x30\x2e\x35\x36\x30\x35\x34\x37\x20\x4c\ 38 | \x20\x32\x34\x20\x32\x36\x2e\x31\x32\x31\x30\x39\x34\x20\x4c\x20\ 39 | \x33\x38\x2e\x34\x33\x39\x34\x35\x33\x20\x34\x30\x2e\x35\x36\x30\ 40 | \x35\x34\x37\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\x2e\ 41 | \x35\x30\x30\x31\x35\x20\x30\x20\x31\x20\x30\x20\x34\x30\x2e\x35\ 42 | \x36\x30\x35\x34\x37\x20\x33\x38\x2e\x34\x33\x39\x34\x35\x33\x20\ 43 | \x4c\x20\x32\x36\x2e\x31\x32\x31\x30\x39\x34\x20\x32\x34\x20\x4c\ 44 | \x20\x34\x30\x2e\x35\x36\x30\x35\x34\x37\x20\x39\x2e\x35\x36\x30\ 45 | \x35\x34\x36\x39\x20\x41\x20\x31\x2e\x35\x30\x30\x31\x35\x20\x31\ 46 | \x2e\x35\x30\x30\x31\x35\x20\x30\x20\x30\x20\x30\x20\x33\x39\x2e\ 47 | \x34\x38\x36\x33\x32\x38\x20\x36\x2e\x39\x37\x38\x35\x31\x35\x36\ 48 | \x20\x7a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ 49 | \x00\x00\x03\x54\ 50 | \x3c\ 51 | \x73\x76\x67\x20\x66\x69\x6c\x6c\x3d\x22\x23\x32\x32\x38\x42\x45\ 52 | \x36\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\ 53 | \x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\ 54 | \x2f\x73\x76\x67\x22\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ 55 | \x30\x20\x30\x20\x34\x38\x20\x34\x38\x22\x20\x77\x69\x64\x74\x68\ 56 | \x3d\x22\x31\x34\x34\x70\x78\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ 57 | \x22\x31\x34\x34\x70\x78\x22\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\ 58 | \x22\x4d\x20\x38\x2e\x35\x20\x38\x20\x43\x20\x36\x2e\x30\x33\x32\ 59 | \x34\x39\x39\x31\x20\x38\x20\x34\x20\x31\x30\x2e\x30\x33\x32\x34\ 60 | \x39\x39\x20\x34\x20\x31\x32\x2e\x35\x20\x4c\x20\x34\x20\x33\x35\ 61 | \x2e\x35\x20\x43\x20\x34\x20\x33\x37\x2e\x39\x36\x37\x35\x30\x31\ 62 | \x20\x36\x2e\x30\x33\x32\x34\x39\x39\x31\x20\x34\x30\x20\x38\x2e\ 63 | \x35\x20\x34\x30\x20\x4c\x20\x33\x39\x2e\x35\x20\x34\x30\x20\x43\ 64 | \x20\x34\x31\x2e\x39\x36\x37\x35\x30\x31\x20\x34\x30\x20\x34\x34\ 65 | \x20\x33\x37\x2e\x39\x36\x37\x35\x30\x31\x20\x34\x34\x20\x33\x35\ 66 | \x2e\x35\x20\x4c\x20\x34\x34\x20\x31\x37\x2e\x35\x20\x43\x20\x34\ 67 | \x34\x20\x31\x35\x2e\x30\x33\x32\x34\x39\x39\x20\x34\x31\x2e\x39\ 68 | \x36\x37\x35\x30\x31\x20\x31\x33\x20\x33\x39\x2e\x35\x20\x31\x33\ 69 | \x20\x4c\x20\x32\x34\x2e\x30\x34\x32\x39\x36\x39\x20\x31\x33\x20\ 70 | \x4c\x20\x31\x39\x2e\x35\x37\x32\x32\x36\x36\x20\x39\x2e\x32\x37\ 71 | \x35\x33\x39\x30\x36\x20\x43\x20\x31\x38\x2e\x35\x38\x34\x30\x35\ 72 | \x35\x20\x38\x2e\x34\x35\x32\x31\x31\x30\x35\x20\x31\x37\x2e\x33\ 73 | \x33\x39\x31\x36\x32\x20\x38\x20\x31\x36\x2e\x30\x35\x32\x37\x33\ 74 | \x34\x20\x38\x20\x4c\x20\x38\x2e\x35\x20\x38\x20\x7a\x20\x4d\x20\ 75 | \x38\x2e\x35\x20\x31\x31\x20\x4c\x20\x31\x36\x2e\x30\x35\x32\x37\ 76 | \x33\x34\x20\x31\x31\x20\x43\x20\x31\x36\x2e\x36\x33\x38\x33\x30\ 77 | \x37\x20\x31\x31\x20\x31\x37\x2e\x32\x30\x32\x35\x35\x35\x20\x31\ 78 | \x31\x2e\x32\x30\x35\x33\x35\x38\x20\x31\x37\x2e\x36\x35\x32\x33\ 79 | \x34\x34\x20\x31\x31\x2e\x35\x38\x30\x30\x37\x38\x20\x4c\x20\x32\ 80 | \x31\x2e\x31\x35\x36\x32\x35\x20\x31\x34\x2e\x35\x20\x4c\x20\x31\ 81 | \x37\x2e\x36\x35\x32\x33\x34\x34\x20\x31\x37\x2e\x34\x31\x39\x39\ 82 | \x32\x32\x20\x43\x20\x31\x37\x2e\x32\x30\x32\x35\x35\x35\x20\x31\ 83 | \x37\x2e\x37\x39\x34\x36\x34\x32\x20\x31\x36\x2e\x36\x33\x38\x33\ 84 | \x30\x37\x20\x31\x38\x20\x31\x36\x2e\x30\x35\x32\x37\x33\x34\x20\ 85 | \x31\x38\x20\x4c\x20\x37\x20\x31\x38\x20\x4c\x20\x37\x20\x31\x32\ 86 | \x2e\x35\x20\x43\x20\x37\x20\x31\x31\x2e\x36\x35\x33\x35\x30\x31\ 87 | \x20\x37\x2e\x36\x35\x33\x35\x30\x30\x39\x20\x31\x31\x20\x38\x2e\ 88 | \x35\x20\x31\x31\x20\x7a\x20\x4d\x20\x32\x34\x2e\x30\x34\x32\x39\ 89 | \x36\x39\x20\x31\x36\x20\x4c\x20\x33\x39\x2e\x35\x20\x31\x36\x20\ 90 | \x43\x20\x34\x30\x2e\x33\x34\x36\x34\x39\x39\x20\x31\x36\x20\x34\ 91 | \x31\x20\x31\x36\x2e\x36\x35\x33\x35\x30\x31\x20\x34\x31\x20\x31\ 92 | \x37\x2e\x35\x20\x4c\x20\x34\x31\x20\x33\x35\x2e\x35\x20\x43\x20\ 93 | \x34\x31\x20\x33\x36\x2e\x33\x34\x36\x34\x39\x39\x20\x34\x30\x2e\ 94 | \x33\x34\x36\x34\x39\x39\x20\x33\x37\x20\x33\x39\x2e\x35\x20\x33\ 95 | \x37\x20\x4c\x20\x38\x2e\x35\x20\x33\x37\x20\x43\x20\x37\x2e\x36\ 96 | \x35\x33\x35\x30\x30\x39\x20\x33\x37\x20\x37\x20\x33\x36\x2e\x33\ 97 | \x34\x36\x34\x39\x39\x20\x37\x20\x33\x35\x2e\x35\x20\x4c\x20\x37\ 98 | \x20\x32\x31\x20\x4c\x20\x31\x36\x2e\x30\x35\x32\x37\x33\x34\x20\ 99 | \x32\x31\x20\x43\x20\x31\x37\x2e\x33\x33\x39\x31\x36\x32\x20\x32\ 100 | \x31\x20\x31\x38\x2e\x35\x38\x34\x30\x35\x35\x20\x32\x30\x2e\x35\ 101 | \x34\x37\x38\x38\x39\x20\x31\x39\x2e\x35\x37\x32\x32\x36\x36\x20\ 102 | \x31\x39\x2e\x37\x32\x34\x36\x30\x39\x20\x4c\x20\x32\x34\x2e\x30\ 103 | \x34\x32\x39\x36\x39\x20\x31\x36\x20\x7a\x22\x2f\x3e\x3c\x2f\x73\ 104 | \x76\x67\x3e\ 105 | " 106 | 107 | qt_resource_name = b"\ 108 | \x00\x05\ 109 | \x00\x6f\xa6\x53\ 110 | \x00\x69\ 111 | \x00\x63\x00\x6f\x00\x6e\x00\x73\ 112 | \x00\x0e\ 113 | \x00\xb2\x47\x47\ 114 | \x00\x63\ 115 | \x00\x6c\x00\x6f\x00\x73\x00\x65\x00\x2d\x00\x69\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ 116 | \x00\x14\ 117 | \x04\x53\x5f\xa7\ 118 | \x00\x66\ 119 | \x00\x6f\x00\x6c\x00\x64\x00\x65\x00\x72\x00\x2d\x00\x69\x00\x63\x00\x6f\x00\x6e\x00\x2d\x00\x62\x00\x6c\x00\x75\x00\x65\x00\x2e\ 120 | \x00\x73\x00\x76\x00\x67\ 121 | " 122 | 123 | qt_resource_struct_v1 = b"\ 124 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 125 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ 126 | \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 127 | \x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x02\x30\ 128 | " 129 | 130 | qt_resource_struct_v2 = b"\ 131 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ 132 | \x00\x00\x00\x00\x00\x00\x00\x00\ 133 | \x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ 134 | \x00\x00\x00\x00\x00\x00\x00\x00\ 135 | \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ 136 | \x00\x00\x01\x82\x02\x59\x86\x74\ 137 | \x00\x00\x00\x32\x00\x00\x00\x00\x00\x01\x00\x00\x02\x30\ 138 | \x00\x00\x01\x82\x02\x57\xa4\xdd\ 139 | " 140 | 141 | qt_version = [int(v) for v in QtCore.qVersion().split('.')] 142 | if qt_version < [5, 8, 0]: 143 | rcc_version = 1 144 | qt_resource_struct = qt_resource_struct_v1 145 | else: 146 | rcc_version = 2 147 | qt_resource_struct = qt_resource_struct_v2 148 | 149 | def qInitResources(): 150 | QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 151 | 152 | def qCleanupResources(): 153 | QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) 154 | 155 | qInitResources() 156 | -------------------------------------------------------------------------------- /theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "name": "default-dark", 4 | "version": "1.0", 5 | 6 | "syntax": [ 7 | { 8 | "default": { 9 | "color": "#abb2bf", 10 | "paper-color": "#282c34", 11 | "font": { 12 | "family": "consolas", 13 | "font-size": 14, 14 | "font-weight": "bold", 15 | "italic": false 16 | } 17 | } 18 | }, 19 | { 20 | "keyword": { 21 | "color": "#c678dd", 22 | "paper-color": "#282c34", 23 | "font": { 24 | "family": "consolas", 25 | "font-size": 14, 26 | "font-weight": "bold", 27 | "italic": false 28 | } 29 | } 30 | }, 31 | { 32 | "classes": { 33 | "color": "#C68F55", 34 | "paper-color": "#282c34", 35 | "font": { 36 | "family": "consolas", 37 | "font-size": 14, 38 | "font-weight": "bold", 39 | "italic": false 40 | } 41 | } 42 | }, 43 | { 44 | "functions": { 45 | "color": "#61afd1", 46 | "paper-color": "#282c34", 47 | "font": { 48 | "family": "consolas", 49 | "font-size": 14, 50 | "font-weight": "bold", 51 | "italic": false 52 | } 53 | } 54 | }, 55 | { 56 | "function_def": { 57 | "color": "#61afd1", 58 | "paper-color": "#282c34", 59 | "font": { 60 | "family": "consolas", 61 | "font-size": 14, 62 | "font-weight": "bold", 63 | "italic": true 64 | } 65 | } 66 | }, 67 | { 68 | "string": { 69 | "color": "#98c379", 70 | "paper-color": "#282c34" 71 | } 72 | }, 73 | { 74 | "types": { 75 | "color": "#56b6c2", 76 | "paper-color": "#282c34" 77 | } 78 | }, 79 | { 80 | "keyargs": { 81 | "color": "#c678dd", 82 | "paper-color": "#282c34" 83 | } 84 | }, 85 | { 86 | "brackets": { 87 | "color": "#c678dd", 88 | "paper-color": "#282c34" 89 | } 90 | }, 91 | { 92 | "comments": { 93 | "color": "#777777", 94 | "paper-color": "#282c34" 95 | } 96 | }, 97 | { 98 | "constants": { 99 | "color": "#d19a5e", 100 | "paper-color": "#282c34" 101 | } 102 | } 103 | ] 104 | } 105 | } 106 | --------------------------------------------------------------------------------