From d0c8e646e0927618d5b24d3d9c4c80f62a5204f8 Mon Sep 17 00:00:00 2001 From: aisuneko icecat Date: Mon, 30 Mar 2026 16:50:50 +0800 Subject: [PATCH 1/3] Add settings and about GUI --- .gitignore | 1 + README.md | 3 +- typstwriter/actions.py | 8 ++ typstwriter/mainwindow.py | 22 +++- typstwriter/menubar.py | 3 + typstwriter/settings_gui.py | 209 ++++++++++++++++++++++++++++++++++++ 6 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 typstwriter/settings_gui.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4bae5a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*/__pycache__ diff --git a/README.md b/README.md index 2e89abe..4c8920c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ pip install -e ./typstwriter/ Run the tests with pytest to make sure that everything is working correctly. Inside the source directory, run: ``` -pytest +pip install -e .[tests] +QT_API=pyside6 pytest ``` Activate pre-commit hooks to automatically check changes before committing. Inside the source directory, run: diff --git a/typstwriter/actions.py b/typstwriter/actions.py index 2be6cb6..4489bd6 100644 --- a/typstwriter/actions.py +++ b/typstwriter/actions.py @@ -148,10 +148,18 @@ def __init__(self, parent): self.show_compiler_output.setText("Show Compiler Output") self.show_compiler_output.setCheckable(True) + self.open_settings = QtWidgets.QAction(self) + self.open_settings.setIcon(QtGui.QIcon.fromTheme("preferences-system")) + self.open_settings.setText("Settings") + self.open_config = QtWidgets.QAction(self) self.open_config.setIcon(QtGui.QIcon.fromTheme("configure-symbolic")) self.open_config.setText("Open config file") + self.about = QtWidgets.QAction(self) + self.about.setIcon(QtGui.QIcon.fromTheme("help-about")) + self.about.setText("About") + self.run = util.TogglingAction(self) self.run.setIcon( QtGui.QIcon.fromTheme(QtGui.QIcon.MediaPlaybackStart, QtGui.QIcon(util.icon_path("start.svg"))), diff --git a/typstwriter/mainwindow.py b/typstwriter/mainwindow.py index 948f0e0..2bd79c2 100644 --- a/typstwriter/mainwindow.py +++ b/typstwriter/mainwindow.py @@ -16,6 +16,7 @@ from typstwriter import configuration from typstwriter import globalstate from typstwriter import arguments +from typstwriter import settings_gui logger = logging.getLogger(__name__) config = configuration.Config @@ -46,13 +47,13 @@ def __init__(self): self.addToolBar(self.toolbar) # Host Widget for Editor and PDF Viewer - self.WidtetCore = QtWidgets.QWidget(self) - self.CoreLayout_v = QtWidgets.QVBoxLayout(self.WidtetCore) + self.WidgetCore = QtWidgets.QWidget(self) + self.CoreLayout_v = QtWidgets.QVBoxLayout(self.WidgetCore) self.CoreLayout_v.setContentsMargins(4, 4, 4, 4) self.CoreLayout_v.setSpacing(0) - self.setCentralWidget(self.WidtetCore) + self.setCentralWidget(self.WidgetCore) - self.splitter = QtWidgets.QSplitter(self.WidtetCore) + self.splitter = QtWidgets.QSplitter(self.WidgetCore) self.splitter.setOrientation(QtCore.Qt.Vertical) self.CoreLayout_v.addWidget(self.splitter) @@ -139,7 +140,9 @@ def __init__(self): self.actions.run.deactivated.connect(self.CompilerConnector.stop) self.CompilerConnector.started.connect(lambda: self.actions.run.setChecked(True)) self.CompilerConnector.stopped.connect(lambda: self.actions.run.setChecked(False)) + self.actions.open_settings.triggered.connect(self.open_settings) self.actions.open_config.triggered.connect(self.open_config) + self.actions.about.triggered.connect(self.open_about) self.FSExplorer.open_file.connect(self.editor.open_file) state.working_directory.Signal.connect(self.FSExplorer.root_changed) @@ -246,6 +249,17 @@ def set_compiler_output_visibility(self, visibility): """Set the visibility of the fs explplorer.""" self.CompilerOutputdock.setVisible(visibility) + def open_settings(self): + """Open settings dialog.""" + dialog = settings_gui.SettingsDialog(self) + dialog.exec() + + def open_about(self): + """Open about dialog.""" + from typstwriter import __version__ + QtWidgets.QMessageBox.about(self, "About Typstwriter", + f"Typstwriter {__version__}\nAn integrated editor for the typst typesetting system\nhttps://github.com/Bzero/typstwriter\nLicensed under MIT ") + def open_config(self): """Open config file.""" config.write() diff --git a/typstwriter/menubar.py b/typstwriter/menubar.py index 1e57a02..bd993fb 100644 --- a/typstwriter/menubar.py +++ b/typstwriter/menubar.py @@ -79,7 +79,10 @@ def __init__(self, actions): self.menuView.addSeparator() self.menuView.addMenu(self.editor_zoom_menu) + self.menuSettings.addAction(actions.open_settings) self.menuSettings.addAction(actions.open_config) + self.menuSettings.addSeparator() + self.menuSettings.addAction(actions.about) class RecentFilesMenu(QtWidgets.QMenu): diff --git a/typstwriter/settings_gui.py b/typstwriter/settings_gui.py new file mode 100644 index 0000000..0edd589 --- /dev/null +++ b/typstwriter/settings_gui.py @@ -0,0 +1,209 @@ +from qtpy import QtCore, QtWidgets + +import qt_themes + +from typstwriter import configuration +from typstwriter import logging + +logger = logging.getLogger(__name__) +config = configuration.Config + +class SettingsDialog(QtWidgets.QDialog): + """Settings Dialog.""" + + def __init__(self, parent=None): + """Init.""" + super().__init__(parent) + self.setWindowTitle("Settings") + self.resize(600, 400) + + self.main_layout = QtWidgets.QVBoxLayout(self) + + self.tabs = QtWidgets.QTabWidget(self) + self.main_layout.addWidget(self.tabs) + + self.init_general_tab() + self.init_compiler_tab() + self.init_editor_tab() + self.init_layout_tab() + self.init_internals_tab() + + self.button_box = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel + ) + self.button_box.accepted.connect(self.save_settings) + self.button_box.rejected.connect(self.reject) + self.main_layout.addWidget(self.button_box) + + def init_general_tab(self): + tab = QtWidgets.QWidget() + layout = QtWidgets.QFormLayout(tab) + + self.general_working_directory = QtWidgets.QLineEdit(config.get("General", "working_directory")) + layout.addRow("Working Directory:", self.general_working_directory) + + self.general_resume_session = QtWidgets.QCheckBox() + self.general_resume_session.setChecked(config.get("General", "resume_last_session", "bool")) + layout.addRow("Resume Last Session:", self.general_resume_session) + + self.general_theme = QtWidgets.QComboBox() + theme_names = ["System Default"] + sorted(qt_themes.get_themes().keys()) + for t in theme_names: + if t == "System Default": + self.general_theme.addItem(t, None) + else: + self.general_theme.addItem(t.title().replace("_", " "), t) + + current_theme = config.get("General", "theme") + index = self.general_theme.findData(current_theme) + self.general_theme.setCurrentIndex(index if index >= 0 else 0) + layout.addRow("Theme:", self.general_theme) + + hint_label = QtWidgets.QLabel() + hint_label.setWordWrap(True) + hint_label.setText( + "
Typstwriter looks for configuration files in the following locations, in reverse order:
" + "- /etc/typstwriter/typstwriter.ini
" + "- /usr/local/etc/typstwriter/typstwriter.ini
" + "- $XDG_CONFIG_HOME/typstwriter/typstwriter.ini (Linux/Unix Only)
" + "- %USERPROFILE%\\AppData\\Local\\typstwriter\\typstwriter.ini (Windows Only)
" + "- ~/.typstwriter.ini
" + "- ./typstwriter.ini (Working Directory)

" + "You can either edit the settings here in this GUI, or manually through the configuration file in the aforementioned locations." + ) + layout.addRow(hint_label) + self.tabs.addTab(tab, "General") + + def init_compiler_tab(self): + tab = QtWidgets.QWidget() + layout = QtWidgets.QFormLayout(tab) + + self.compiler_name = QtWidgets.QLineEdit(config.get("Compiler", "name")) + layout.addRow("Name of Typst executable:", self.compiler_name) + + self.compiler_mode = QtWidgets.QComboBox() + self.compiler_mode.addItem("Live", "live") + self.compiler_mode.addItem("On Demand", "on_demand") + + current_mode = config.get("Compiler", "mode") + index = self.compiler_mode.findData(current_mode) + if index >= 0: + self.compiler_mode.setCurrentIndex(index) + + layout.addRow("Mode:", self.compiler_mode) + + self.tabs.addTab(tab, "Compiler") + + def init_editor_tab(self): + tab = QtWidgets.QWidget() + layout = QtWidgets.QFormLayout(tab) + + self.editor_font_size = QtWidgets.QSpinBox() + self.editor_font_size.setRange(4, 72) + self.editor_font_size.setValue(config.get("Editor", "font_size", "int")) + layout.addRow("Font Size:", self.editor_font_size) + + self.editor_save_at_run = QtWidgets.QCheckBox() + self.editor_save_at_run.setChecked(config.get("Editor", "save_at_run", "bool")) + layout.addRow("Save at run:", self.editor_save_at_run) + + self.editor_highlighter_style = QtWidgets.QLineEdit(config.get("Editor", "highlighter_style")) + layout.addRow("Highlighter Style:", self.editor_highlighter_style) + + hint_label = QtWidgets.QLabel() + hint_label.setText("See https://pygments.org/styles/ for available options") + hint_label.setOpenExternalLinks(True) + layout.addRow("", hint_label) + + self.editor_highlight_syntax = QtWidgets.QCheckBox() + self.editor_highlight_syntax.setChecked(config.get("Editor", "highlight_syntax", "bool")) + layout.addRow("Highlight Syntax:", self.editor_highlight_syntax) + + self.editor_show_line_numbers = QtWidgets.QCheckBox() + self.editor_show_line_numbers.setChecked(config.get("Editor", "show_line_numbers", "bool")) + layout.addRow("Show Line Numbers:", self.editor_show_line_numbers) + + self.editor_highlight_line = QtWidgets.QCheckBox() + self.editor_highlight_line.setChecked(config.get("Editor", "highlight_line", "bool")) + layout.addRow("Highlight Line:", self.editor_highlight_line) + + self.editor_use_spaces = QtWidgets.QCheckBox() + self.editor_use_spaces.setChecked(config.get("Editor", "use_spaces", "bool")) + layout.addRow("Use Spaces:", self.editor_use_spaces) + + self.tabs.addTab(tab, "Editor") + + def init_layout_tab(self): + tab = QtWidgets.QWidget() + layout = QtWidgets.QFormLayout(tab) + + self.layout_default_layout = QtWidgets.QComboBox() + self.layout_default_layout.addItem("Typewriter (Editor under Preview)", "typewriter") + self.layout_default_layout.addItem("Editor Right of Preview", "editor_right") + self.layout_default_layout.addItem("Editor Left of Preview", "editor_left") + + current_layout = config.get("Layout", "default_layout") + index = self.layout_default_layout.findData(current_layout) + if index >= 0: + self.layout_default_layout.setCurrentIndex(index) + + layout.addRow("Default Layout:", self.layout_default_layout) + + self.layout_show_fs_explorer = QtWidgets.QCheckBox() + self.layout_show_fs_explorer.setChecked(config.get("Layout", "show_fs_explorer", "bool")) + layout.addRow("Show FS Explorer:", self.layout_show_fs_explorer) + + self.layout_show_compiler_options = QtWidgets.QCheckBox() + self.layout_show_compiler_options.setChecked(config.get("Layout", "show_compiler_options", "bool")) + layout.addRow("Show Compiler Options:", self.layout_show_compiler_options) + + self.layout_show_compiler_output = QtWidgets.QCheckBox() + self.layout_show_compiler_output.setChecked(config.get("Layout", "show_compiler_output", "bool")) + layout.addRow("Show Compiler Output:", self.layout_show_compiler_output) + + self.tabs.addTab(tab, "Layout") + + def init_internals_tab(self): + tab = QtWidgets.QWidget() + layout = QtWidgets.QFormLayout(tab) + + self.internals_recent_files_path = QtWidgets.QLineEdit(config.get("Internals", "recent_files_path")) + layout.addRow("Recent Files Path:", self.internals_recent_files_path) + + self.internals_recent_files_length = QtWidgets.QSpinBox() + self.internals_recent_files_length.setRange(1, 100) + self.internals_recent_files_length.setValue(config.get("Internals", "recent_files_length", "int")) + layout.addRow("Recent Files Length:", self.internals_recent_files_length) + + self.internals_session_path = QtWidgets.QLineEdit(config.get("Internals", "session_path")) + layout.addRow("Session Path:", self.internals_session_path) + + self.tabs.addTab(tab, "Internals") + + def save_settings(self): + config.set("General", "working_directory", self.general_working_directory.text()) + config.set("General", "resume_last_session", self.general_resume_session.isChecked()) + config.set("General", "theme", self.general_theme.currentData()) + + config.set("Compiler", "name", self.compiler_name.text()) + config.set("Compiler", "mode", self.compiler_mode.currentData()) + + config.set("Editor", "font_size", self.editor_font_size.value()) + config.set("Editor", "save_at_run", self.editor_save_at_run.isChecked()) + config.set("Editor", "highlighter_style", self.editor_highlighter_style.text()) + config.set("Editor", "highlight_syntax", self.editor_highlight_syntax.isChecked()) + config.set("Editor", "show_line_numbers", self.editor_show_line_numbers.isChecked()) + config.set("Editor", "highlight_line", self.editor_highlight_line.isChecked()) + config.set("Editor", "use_spaces", self.editor_use_spaces.isChecked()) + + config.set("Layout", "default_layout", self.layout_default_layout.currentData()) + config.set("Layout", "show_fs_explorer", self.layout_show_fs_explorer.isChecked()) + config.set("Layout", "show_compiler_options", self.layout_show_compiler_options.isChecked()) + config.set("Layout", "show_compiler_output", self.layout_show_compiler_output.isChecked()) + + config.set("Internals", "recent_files_path", self.internals_recent_files_path.text()) + config.set("Internals", "recent_files_length", self.internals_recent_files_length.value()) + config.set("Internals", "session_path", self.internals_session_path.text()) + + config.write() + self.accept() From cb0650efab8fb63e147dcdcaae3d88342c92ee5c Mon Sep 17 00:00:00 2001 From: aisuneko icecat Date: Mon, 30 Mar 2026 17:09:30 +0800 Subject: [PATCH 2/3] Fix text overflow --- typstwriter/settings_gui.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/typstwriter/settings_gui.py b/typstwriter/settings_gui.py index 0edd589..8828f85 100644 --- a/typstwriter/settings_gui.py +++ b/typstwriter/settings_gui.py @@ -1,4 +1,4 @@ -from qtpy import QtCore, QtWidgets +from qtpy import QtWidgets import qt_themes @@ -8,6 +8,7 @@ logger = logging.getLogger(__name__) config = configuration.Config + class SettingsDialog(QtWidgets.QDialog): """Settings Dialog.""" @@ -28,14 +29,13 @@ def __init__(self, parent=None): self.init_layout_tab() self.init_internals_tab() - self.button_box = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel - ) + self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.save_settings) self.button_box.rejected.connect(self.reject) self.main_layout.addWidget(self.button_box) def init_general_tab(self): + """Initialize the General tab.""" tab = QtWidgets.QWidget() layout = QtWidgets.QFormLayout(tab) @@ -47,13 +47,12 @@ def init_general_tab(self): layout.addRow("Resume Last Session:", self.general_resume_session) self.general_theme = QtWidgets.QComboBox() - theme_names = ["System Default"] + sorted(qt_themes.get_themes().keys()) + theme_names = ["System Default", *sorted(qt_themes.get_themes().keys())] for t in theme_names: if t == "System Default": self.general_theme.addItem(t, None) else: self.general_theme.addItem(t.title().replace("_", " "), t) - current_theme = config.get("General", "theme") index = self.general_theme.findData(current_theme) self.general_theme.setCurrentIndex(index if index >= 0 else 0) @@ -68,13 +67,15 @@ def init_general_tab(self): "- $XDG_CONFIG_HOME/typstwriter/typstwriter.ini (Linux/Unix Only)
" "- %USERPROFILE%\\AppData\\Local\\typstwriter\\typstwriter.ini (Windows Only)
" "- ~/.typstwriter.ini
" - "- ./typstwriter.ini (Working Directory)

" - "You can either edit the settings here in this GUI, or manually through the configuration file in the aforementioned locations." + "- ./typstwriter.ini (Working Directory)
" + "You can either edit the settings here in this GUI, or manually through the configuration file \ + in the aforementioned locations." ) layout.addRow(hint_label) self.tabs.addTab(tab, "General") def init_compiler_tab(self): + """Initialize the Compiler tab.""" tab = QtWidgets.QWidget() layout = QtWidgets.QFormLayout(tab) @@ -84,17 +85,16 @@ def init_compiler_tab(self): self.compiler_mode = QtWidgets.QComboBox() self.compiler_mode.addItem("Live", "live") self.compiler_mode.addItem("On Demand", "on_demand") - current_mode = config.get("Compiler", "mode") index = self.compiler_mode.findData(current_mode) if index >= 0: self.compiler_mode.setCurrentIndex(index) - layout.addRow("Mode:", self.compiler_mode) self.tabs.addTab(tab, "Compiler") def init_editor_tab(self): + """Initialize the Editor tab.""" tab = QtWidgets.QWidget() layout = QtWidgets.QFormLayout(tab) @@ -111,7 +111,10 @@ def init_editor_tab(self): layout.addRow("Highlighter Style:", self.editor_highlighter_style) hint_label = QtWidgets.QLabel() - hint_label.setText("See https://pygments.org/styles/ for available options") + hint_label.setText( + "See https://pygments.org/styles/\ + for available options" + ) hint_label.setOpenExternalLinks(True) layout.addRow("", hint_label) @@ -134,6 +137,7 @@ def init_editor_tab(self): self.tabs.addTab(tab, "Editor") def init_layout_tab(self): + """Initialize the Layout tab.""" tab = QtWidgets.QWidget() layout = QtWidgets.QFormLayout(tab) @@ -141,12 +145,11 @@ def init_layout_tab(self): self.layout_default_layout.addItem("Typewriter (Editor under Preview)", "typewriter") self.layout_default_layout.addItem("Editor Right of Preview", "editor_right") self.layout_default_layout.addItem("Editor Left of Preview", "editor_left") - current_layout = config.get("Layout", "default_layout") index = self.layout_default_layout.findData(current_layout) if index >= 0: self.layout_default_layout.setCurrentIndex(index) - + layout.addRow("Default Layout:", self.layout_default_layout) self.layout_show_fs_explorer = QtWidgets.QCheckBox() @@ -164,6 +167,7 @@ def init_layout_tab(self): self.tabs.addTab(tab, "Layout") def init_internals_tab(self): + """Initialize the Internals tab.""" tab = QtWidgets.QWidget() layout = QtWidgets.QFormLayout(tab) @@ -181,6 +185,7 @@ def init_internals_tab(self): self.tabs.addTab(tab, "Internals") def save_settings(self): + """Write the settings to the configuration file.""" config.set("General", "working_directory", self.general_working_directory.text()) config.set("General", "resume_last_session", self.general_resume_session.isChecked()) config.set("General", "theme", self.general_theme.currentData()) From e22bbbca5e9f186cb347436d72f319a12f3d39f1 Mon Sep 17 00:00:00 2001 From: aisuneko icecat Date: Sat, 4 Apr 2026 00:34:52 +0800 Subject: [PATCH 3/3] Make ruff happy --- typstwriter/mainwindow.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/typstwriter/mainwindow.py b/typstwriter/mainwindow.py index 2bd79c2..abe27bc 100644 --- a/typstwriter/mainwindow.py +++ b/typstwriter/mainwindow.py @@ -18,6 +18,8 @@ from typstwriter import arguments from typstwriter import settings_gui +from typstwriter import __version__ + logger = logging.getLogger(__name__) config = configuration.Config state = globalstate.State @@ -151,7 +153,7 @@ def __init__(self): self.CompilerConnector.compilation_finished.connect(self.editor.clear_errors) self.CompilerConnector.error_report.connect(self.editor.apply_errors) state.main_file.Signal.connect(lambda s: self.CompilerConnector.stop()) - state.main_file.Signal.connect(lambda s: self.CompilerOptions.main_changed(s)) # noqa: PLW0108 + state.main_file.Signal.connect(lambda s: self.CompilerOptions.main_changed(s)) state.main_file.Signal.connect(lambda s: self.PDFWidget.open(util.pdf_path(s))) # For now only display errors @@ -256,9 +258,12 @@ def open_settings(self): def open_about(self): """Open about dialog.""" - from typstwriter import __version__ - QtWidgets.QMessageBox.about(self, "About Typstwriter", - f"Typstwriter {__version__}\nAn integrated editor for the typst typesetting system\nhttps://github.com/Bzero/typstwriter\nLicensed under MIT ") + QtWidgets.QMessageBox.about( + self, + "About Typstwriter", + f"Typstwriter {__version__}\nAn integrated editor for the typst typesetting system\ + \nhttps://github.com/Bzero/typstwriter\nLicensed under MIT ", + ) def open_config(self): """Open config file."""