def __init__(self) -> None: """Initialise main window """ super().__init__() if which("pandoc") is None and sys.platform.startswith("win32"): os.environ.setdefault("PYPANDOC_PANDOC", get_resource("pandoc-2.13/pandoc.exe")) self.date_opened = date.today() self.refresh_stylesheet() # Load config config_file = get_resource("config.json") with open(config_file, "r") as file: config_dict = json.loads(file.read()) self.no_journal_dialog = QtWidgets.QMessageBox() self.no_journal_dialog.setText("No journal directory configured") self.no_journal_dialog.setStandardButtons(QtWidgets.QMessageBox.Close | QtWidgets.QMessageBox.Open) self.no_journal_dialog.setDefaultButton(QtWidgets.QMessageBox.Open) # Folder is not an absolute folder try: if not (config_dict['diary_directory'][0] == '/' or config_dict['diary_directory'][1] == ':' ) or not os.path.exists(config_dict['diary_directory']): answer = self.no_journal_dialog.exec() if answer == QtWidgets.QMessageBox.Open: self.select_diary_directory() else: exit(0) except IndexError: answer = self.no_journal_dialog.exec() if answer == QtWidgets.QMessageBox.Open: self.select_diary_directory() else: exit(0) # Layout self.layout = QtWidgets.QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(0) # Edit pane self.edit_pane = EditPane() self.web_view = WebView(self.edit_pane) # Menu bar and adding panes underneath self.tool_bar = CustomToolbar(self, self.edit_pane, self.web_view) self.layout.addWidget(self.tool_bar) self.layout.addWidget(self.edit_pane) self.layout.addWidget(self.web_view) self.edit_pane.open_file_from_date(date.today())
def open_file_from_date(self, file_date: date): """Open or create a markdown file corresponding to today""" formatted_date = file_date.strftime("%Y-%m-%d") # Add ordinal to end of number if it exists try: day_of_month = num2words(file_date.day, to="ordinal_num", lang=locale.getlocale()[0]) except NotImplementedError: day_of_month = file_date.day long_date = file_date.strftime(f"%A {day_of_month} %B %Y") # Get folder for today's journal entry config_file = get_resource("config.json") with open(config_file, "r") as file: file_directory = os.path.join( json.loads(file.read())["diary_directory"], str(file_date.year), str(file_date.month), formatted_date) # Make folder for today's entry if not already exist print(get_resource("config.json")) if not os.path.exists(file_directory): pathlib.Path(file_directory).mkdir(parents=True, exist_ok=True) # Open markdown in r+ mode if it exists, else open in w+ mode try: with open(os.path.join(file_directory, f"{formatted_date}.md"), "r+") as file: self.set_current_file(file) except FileNotFoundError: with open(os.path.join(file_directory, f"{formatted_date}.md"), "w+") as file: self.set_current_file(file) self.setText('---\n' f'title: {long_date}\n' f'date: {formatted_date}\n' 'tags: []\n' '---\n') self.save_current_file() self.file_changed.emit() self.window().setWindowTitle(long_date) self.set_margins()
def refresh_page(self): """Convert markdown to html and set webView""" parsed_stylesheet = parse_stylesheet(get_resource('ViewPaneStyle.css'), CONSTANTS.theme) # Write parsed stylesheet to file so it can be passed to pandoc with open(get_resource("parsed_stylesheet.css"), "w") as file: file.write(parsed_stylesheet) # Convert markdown to html using pandoc html = pypandoc.convert_text( self.edit_pane.toPlainText(), "html", format="markdown", extra_args=[ f"--highlight-style={get_resource('syntax.theme')}", "-s", "--css=" f"{get_resource('parsed_stylesheet.css')}", f"--katex={get_resource('katex/')}" ]) self.setHtml(html, QtCore.QUrl().fromLocalFile(self.edit_pane.current_file))
def paintCell(self, painter: QtGui.QPainter, rect: QtCore.QRect, date: typing.Union[QtCore.QDate, datetime.date]) -> None: painter.save() with open(get_resource("config.json")) as file: if date.toPyDate().strftime("%Y-%m-%d") in json.loads( file.read())["favorites"]: painter.fillRect(rect, QtGui.QColor.fromRgb(255, 255, 0)) if (date.month() != self.monthShown()): painter.setPen(QtGui.QColor("#888888")) elif date.dayOfWeek() == 6 or date.dayOfWeek() == 7: painter.setPen(QtGui.QColor("red")) tags = self.get_tags_from_date_file(date.toPyDate()) rect.adjust(0, 0, -1, -1) pen = painter.pen() pen.setColor( QtGui.QColor.fromHsl(pen.color().hue(), pen.color().saturation(), pen.color().lightness(), 150)) painter.setPen(pen) painter.drawRect(rect) pen.setColor( QtGui.QColor.fromHsl(pen.color().hue(), pen.color().saturation(), pen.color().lightness(), 255)) painter.setPen(pen) painter.drawText(rect, QtCore.Qt.AlignTop, str(date.day())) text = "" try: for tag in tags[:5]: if len(tag) > 12: tag = str(tag[:12]) + "..." text += f" {tag} \n" except TypeError: text = "" font = QtGui.QFont() font.setPixelSize(10) painter.setFont(font) brush = painter.background() random.seed(date) brush.setColor(QtGui.QColor().fromHsl(randint(0, 255), randint(0, 255), randint(200, 255))) painter.setPen(QtGui.QColor("black")) painter.setBackground(brush) painter.setBackgroundMode(QtCore.Qt.OpaqueMode) painter.drawText(rect, QtCore.Qt.AlignBottom | QtCore.Qt.AlignHCenter, text) painter.restore()
def createCustomContextMenu(self, pos) -> QtWidgets.QMenu: menu = self.createStandardContextMenu() menu.addSeparator() add_icon = QtGui.QIcon( get_resource(CONSTANTS.icons["plus"][CONSTANTS.theme])) if sys.platform != "win32": # Don't add plus icon on windows (looks ugly) menu.addAction(add_icon, "Insert Image", self.insert_image) else: menu.addAction("Insert Image", self.insert_image) menu.addSeparator() self.word_cursor = self.cursorForPosition(pos) self.word_cursor.select(QtGui.QTextCursor.WordUnderCursor) # Find misspelled word in highlighted text misspelled = [ token[0] for token in self.spell_tknzr(self.word_cursor.selectedText()) if token[0][0].islower() and not spell_dict.check(token[0]) ] # If there is a misspelled word and the word matches the whole of the highlighted text if len(misspelled ) > 0 and misspelled[0] == self.word_cursor.selectedText(): # Add 'Add to Dictionary option' self.add_to_dict_action_handler = ActionHandler( menu.addAction("Add to dictionary"), self.add_to_word_list) menu.addSeparator() spell_suggestion_handlers = [] # Get spelling suggestions spell_suggestions = spell_dict.suggest(misspelled[0]) # Add suggestions to menu until there is no more left or a maximum of 10 while len(spell_suggestion_handlers) < 10 and len( spell_suggestion_handlers) < len(spell_suggestions): for suggestion in spell_suggestions: new_action = menu.addAction(suggestion) spell_suggestion_handlers.append( ActionHandler(new_action, self.replace_selection)) # Save suggestion handlers to object so they persist self.spell_suggestion_handlers = spell_suggestion_handlers if len(self.spell_suggestion_handlers) == 0: no_suggestions = menu.addAction("No Suggestions") no_suggestions.setEnabled(False) return menu
def select_diary_directory(self): """Request user to select a directory and set it to diary_directory in config """ # TODO: Maybe change so only create folder on first save config_file = get_resource("config.json") with open(config_file, "r") as file: config_dict = json.loads(file.read()) file_dialog = QtWidgets.QFileDialog() old_dir = config_dict["diary_directory"] # Need non-native file in flatpak to get correct directory if sys.platform.startswith("linux"): config_dict[ "diary_directory"] = file_dialog.getExistingDirectory( self, "Please select a directory to store your journal files", "", QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog) else: config_dict[ "diary_directory"] = file_dialog.getExistingDirectory( self, "Please select a directory to store your journal files", "") print(config_dict["diary_directory"]) # Cancel has been clicked if config_dict["diary_directory"] == "": if os.path.exists(old_dir): config_dict["diary_directory"] = old_dir else: exit(0) with open(config_file, "w") as file: file.write(json.dumps(config_dict, sort_keys=True, indent=4)) try: self.edit_pane.open_file_from_date( date.fromisoformat(self.edit_pane.current_file_date)) except AttributeError: print("First time boot")
def get_tags_from_date_file(self, date: datetime.date): """Open or create a markdown file corresponding to today""" formatted_date = date.strftime("%Y-%m-%d") # Get folder for today's journal entry config_file = get_resource("config.json") with open(config_file, "r") as file: file_directory = os.path.join( json.loads(file.read())["diary_directory"], str(date.year), str(date.month), formatted_date) try: with open(os.path.join(file_directory, f"{formatted_date}.md"), "r") as file: pattern = regex.compile(r"(?<=(^-{3,}\n)).+?(?=-{3,})", regex.DOTALL) contents = file.read() metadata = regex.search(pattern, contents) if metadata is not None: meta_dict = yaml.safe_load(metadata.group()) if "tags" in meta_dict.keys(): return meta_dict["tags"] except Exception: return {}
def __init__(self, edit_pane: EditPane, web_view: WebView): """Constructor :param edit_pane: EditPane to open the new file in """ super().__init__() self.edit_pane = edit_pane self.web_view = web_view self.setWindowFlag(QtCore.Qt.Dialog) date = self.edit_pane.current_file_date.split("-") self.setSelectedDate( QtCore.QDate(int(date[0]), int(date[1]), int(date[2]))) self.selectionChanged.connect(self.selection_changed_handler) self.layout = QtWidgets.QVBoxLayout() self.setVerticalHeaderFormat(self.NoVerticalHeader) self.setMinimumSize(640, 640) self.setMaximumSize(640, 640) self.setWindowTitle("Calendar") self.setStyleSheet(""" QMenu { font-size:16px; width: 150px; left: 20px; background-color: palette(base); } QWidget{ background-color: palette(base); } QToolButton { icon-size: 24px, 24px; background-color: palette(base); color: palette(text); } QAbstractItemView { selection-background-color: rgb(255, 174, 0); } QToolButton::menu-arrow { } QToolButton::menu-button { } QToolButton::menu-indicator{ width: 50px; } QToolButton::menu-indicator:pressed, QToolButton::menu-indicator:open{ top:10px; left: 10px; } QListView { background-color:white; } QSpinBox { width:200px; border-width: 2px; } QSpinBox::up-button { subcontrol-origin: border; subcontrol-position: top right; width:50px; border-image: url(icons:arrow_up_n.png); } QSpinBox::down-button { subcontrol-origin: border; subcontrol-position: bottom right; border-width: 1px; width:10px; } QSpinBox::down-arrow { width:6px; height:6px; image: url(icons:arrow_down_n.png); } QMenu::item:selected{ background-color: palette(highlight); } """) # Set format of how favorites appear in calendar favorite_format = QtGui.QTextCharFormat() favorite_brush = QtGui.QBrush() favorite_brush.setColor(QtGui.QColor.fromRgb(255, 228, 0)) favorite_format.setBackground(favorite_brush) favorite_brush.setColor(QtGui.QColor.fromRgb(33, 33, 33)) favorite_format.setForeground(favorite_brush) prev_month_button = self.findChild(QtWidgets.QToolButton, "qt_calendar_prevmonth") next_month_button = self.findChild(QtWidgets.QToolButton, "qt_calendar_nextmonth") prev_month_button.setIcon( QtGui.QIcon( get_resource(CONSTANTS.icons["left_arrow"][CONSTANTS.theme]))) next_month_button.setIcon( QtGui.QIcon( get_resource(CONSTANTS.icons["right_arrow"][CONSTANTS.theme]))) with open(get_resource("config.json")) as file: for day in json.loads(file.read())["favorites"]: formatted_date = [int(x) for x in day.split("-")] self.setDateTextFormat( QtCore.QDate(formatted_date[0], formatted_date[1], formatted_date[2]), favorite_format)
def license_clicked(self): self.license_window = QtWidgets.QTextBrowser() self.license_window.setFixedSize(600, 600) self.license_window.setFont( QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)) self.license_window.setStyleSheet( "background-color: palette(base); border: 0px solid white;") license_text = """ All of the source code to this product is available under licenses which are both free and open source. Most of the code is available under the GNU General Public License 3.0, the licenses of included software, as well as that of the main software is listed below. """ with open(get_resource("licenses/gpl-3.0.txt")) as file: license_text += '\n\nLicense used by main source available at <https://github.com/LukeBriggsDev/Pepys>' \ 'as well as PyQt5 <https://www.riverbankcomputing.com/static/Docs/PyQt5/>\n\n' license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/num2words_license.txt")) as file: license_text += "License used by python library num2words <https://github.com/savoirfairelinux/num2words>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/pandoc_license.txt")) as file: license_text += "License used by pandoc binary <https://github.com/jgm/pandoc>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/pyenchant_license.txt")) as file: license_text += "License used by python library pyenchant <https://github.com/pyenchant/pyenchant>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/pypandoc_LICENSE.txt")) as file: license_text += "License used by python library pypandoc <https://github.com/bebraw/pypandoc>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/pypdf4_license.txt")) as file: license_text += "License used by python library PyPDF4 <https://github.com/claird/PyPDF4>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/pyyaml_license.txt")) as file: license_text += "License used by python library pyYAML <https://github.com/yaml/pyyaml>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/regex_license.txt")) as file: license_text += "License used by python library regex <https://bitbucket.org/mrabarnett/mrab-regex>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/setproctitle_license.txt")) as file: license_text += "License used by python library setproctitle <https://github.com/dvarrazzo/py-setproctitle>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" with open(get_resource("licenses/wkhtmltopdf_license.txt")) as file: license_text += "License used by wkhtmltopdf binary <https://github.com/wkhtmltopdf/wkhtmltopdf>\n\n" license_text += file.read( ) + "\n\n=============================================================================\n\n" self.license_window.setText(license_text) self.license_window.show()
from CONSTANTS import get_resource import CONSTANTS from pypandoc.pandoc_download import download_pandoc import pypandoc from MainWindow import MainWindow if __name__ == "__main__": if sys.platform.lower().startswith("linux"): os.putenv("QT_QPA_PLATFORM", "xcb") if sys.platform.startswith("win32"): import ctypes myappid = 'dev.lukebriggs.pepys' # arbitrary string ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) if not os.path.isfile(get_resource("wordlist.txt")): with open(get_resource("wordlist.txt"), "w+") as file: file.write("") app = QtWidgets.QApplication(sys.argv) app.setApplicationName("Pepys") app.setApplicationDisplayName("Pepys") app.setWindowIcon(QtGui.QIcon(get_resource("icons/appicons/icon.svg"))) CONSTANTS.theme = "light" if QtWidgets.QApplication.palette().color( QtGui.QPalette.Active, QtGui.QPalette.Base).lightness() > 122 else "dark" CONSTANTS.light_palette = QtWidgets.QApplication.palette() setproctitle.setproctitle("Pepys") #Initialise and set size of main_window main_window = MainWindow() main_window.resize(800, 600) main_window.setMinimumSize(640, 480)
def export_diary(self): # Load dialog self.load_dialog = QtWidgets.QDialog(self) self.load_dialog.setWindowModality(QtCore.Qt.WindowModal) self.load_dialog.setMinimumSize(400, 100) self.load_dialog.setMaximumSize(400, 100) self.load_dialog.setWindowTitle("Exporting...") progress_label = QtWidgets.QLabel() progress_label.setAlignment(QtCore.Qt.AlignCenter) progress_bar = QtWidgets.QProgressBar() progress_bar.setAlignment(QtCore.Qt.AlignCenter) self.load_dialog.setLayout(QtWidgets.QVBoxLayout()) self.load_dialog.layout().setAlignment(QtCore.Qt.AlignTop) self.load_dialog.layout().addWidget(progress_label) self.load_dialog.layout().addWidget(progress_bar) self.load_dialog.show() self.setDisabled(True) self.load_dialog.setEnabled(True) with open(get_resource("config.json"), "r") as file: directory = json.loads(file.read())["diary_directory"] if not os.path.exists(self.chosen_directory.text()): try: os.mkdir(os.path.join(Path(directory), "export")) except FileExistsError: shutil.rmtree(os.path.join(Path(directory), "export"), ignore_errors=True) os.mkdir(os.path.join(Path(directory), "export")) self.chosen_directory.setText(os.path.join(directory, "export")) diary_entries = list(Path(directory).rglob("*-*-*.[mM][dD]")) # Custom Range if self.date_options.currentText() == "Custom Range": diary_entries = [entry for entry in diary_entries if self.start_date_widget.date().toString("yyyy-MM-dd") <= entry.name[:-3] <= self.end_date_widget.date().toString("yyyy-MM-dd")] # Current Date if self.date_options.currentText() == "Current Entry": diary_entries = list(Path(directory).rglob(f"{self.edit_pane.current_file_date}.[mM][dD]")) format = self.output_formats[self.export_options.currentText()] pdoc_args = ["--standalone", f"--katex={get_resource('katex/')}"] if format["type"] == "pdf": # Convert to html before pdf to apply css pdoc_args.append("-thtml") if sys.platform.startswith("win32") and shutil.which("wkhtmltopdf") is None: pdoc_args.append(f"--pdf-engine={get_resource('wkhtmltopdf.exe')}") pass if format["type"] == "html" or format["type"] == "pdf": with open(get_resource("parsed_stylesheet.css"), "w+") as f: f.write(parse_stylesheet(get_resource("ViewPaneStyle.css"), "light")) pdoc_args.append("--css="+get_resource("HTMLExport.css")) pdoc_args.append("--self-contained") pdoc_args.append("--pdf-engine-opt=--enable-local-file-access") progress_label.setText("Converting to " + str(format["type"])) progress_bar.setMaximum(len(diary_entries)) QtWidgets.QApplication.processEvents() finished = 0 errors = [] # Conversion for entry in diary_entries: if self.load_dialog.isVisible(): os.chdir(entry.parent.as_posix()) try: pypandoc.convert_file(entry.as_posix(), format["type"], outputfile=(self.chosen_directory.text() + "/" + entry.name[:-3] + "." + format["ext"]), extra_args=pdoc_args) except RuntimeError as err: progress_label.setText("ERROR IN FILE " + entry.name) QtWidgets.QApplication.processEvents() print(str(err)) errors.append(entry) if str(err).startswith('Pandoc died with exitcode "47"'): self.load_dialog.setMinimumSize(550, 150) self.load_dialog.setMaximumSize(550, 150) progress_bar.setVisible(False) exit_button = QtWidgets.QPushButton("Close") self.load_dialog.layout().addWidget(exit_button) exit_button.clicked.connect(self.load_dialog.close) progress_label.setText("No wkhtmltopdf installation found.\n" "Please install wkhtmltopdf.\n\n") return 1 print(err) finished += 1 progress_label.setText(str(finished) + "/" + str(len(diary_entries))) progress_bar.setValue(finished) QtWidgets.QApplication.processEvents() else: return 2 progress_label.setText("Conversion finished") QtWidgets.QApplication.processEvents() if self.will_collate.isChecked(): # Collate pdfs together into one pdf if format["type"] == "pdf": progress_label.setText("Starting pdf collation") QtWidgets.QApplication.processEvents() file_merger = PyPDF4.PdfFileMerger(strict=False) pdf_list = sorted([Path(os.path.join(self.chosen_directory.text(), entry.name[:-3] + ".pdf")) for entry in diary_entries if os.path.isfile(os.path.join(self.chosen_directory.text(), entry.name[:-3] + ".pdf"))], key=lambda x: x.name) for pdf in pdf_list: progress_label.setText(pdf.name) QtWidgets.QApplication.processEvents() file_merger.append(pdf.as_posix(), pdf.name[:-4], import_bookmarks=False) try: os.remove(pdf.as_posix()) except PermissionError: pass file_merger.write(self.chosen_directory.text() + "/diary.pdf") progress_label.setText("pdf collation finished") QtWidgets.QApplication.processEvents() # Collate html together into one pdf if format["type"] == "html": progress_label.setText("Starting html collation") QtWidgets.QApplication.processEvents() html_list = sorted([Path(os.path.join(self.chosen_directory.text(), entry.name[:-3] + ".html")) for entry in diary_entries if os.path.isfile(os.path.join(self.chosen_directory.text(), entry.name[:-3] + ".html"))], key=lambda x: x.name) html = "" try: os.mkdir(os.path.join(Path(self.chosen_directory.text()), "html_export")) except FileExistsError: shutil.rmtree(os.path.join(Path(self.chosen_directory.text()), "html_export"), ignore_errors=True) os.mkdir(os.path.join(Path(self.chosen_directory.text()), "html_export")) for document in html_list: shutil.move(document, os.path.join(Path(self.chosen_directory.text()), "html_export", document.name)) with open(os.path.join(Path(self.chosen_directory.text()), "html_export", "_index.html"), "w") as f: shutil.copyfile(get_resource("parsed_stylesheet.css"), os.path.join(Path(self.chosen_directory.text()), "html_export", "styles.css")) f.write('<!DOCTYPE HTML>' '<HTML>' '<HEAD><LINK rel="stylesheet" href="styles.css" type="text/css">' '</HEAD>') for page in html_list: f.write(f'<a href="{page.name}">{page.name[:-5]}</a><br>') f.write('</HTMl>') progress_label.setText("HTML collation finished") QtWidgets.QApplication.processEvents() print(self.chosen_directory.text()) if len(errors) > 0: self.error_dialog = QtWidgets.QMessageBox() self.error_dialog.setText("Errors occured in the following entries and they were not converted.\n\nPerhaps they link to files that do not exist") self.error_dialog.setInformativeText("\n".join([str(error) for error in errors])) self.error_dialog.show() CONSTANTS.openFolder(self.chosen_directory.text()) self.load_dialog.close()