Add light/dark theme switching and merge syntax and GUI theme (#2373)

This commit is contained in:
Veronica Berglyd Olsen
2025-06-03 22:54:52 +02:00
committed by GitHub
75 changed files with 2645 additions and 1846 deletions
+2 -2
View File
@@ -62,8 +62,8 @@ Some of the assets bundled with novelWriter were adapted from the following sour
* **Material Symbols** icons by Google Inc (Apache 2.0) * **Material Symbols** icons by Google Inc (Apache 2.0)
* **Remix** icons by RemixIcon (Apache 2.0) * **Remix** icons by RemixIcon (Apache 2.0)
* **Font Awesome** icons by Fonticons Inc (CC BY 4.0) * **Font Awesome** icons by Fonticons Inc (CC BY 4.0)
* **Tomorrow** syntax themes by Chris Kempson (MIT License) * **Tomorrow** themes by Chris Kempson (MIT License)
* **Owl** syntax themes by Sarah Drasner (MIT License) * **Owl** themes by Sarah Drasner (MIT License)
* **Solarized** themes by Ethan Schoonover (MIT License) * **Solarized** themes by Ethan Schoonover (MIT License)
* **Cyberpunk Night** theme by Anders Lemvigh (CC BY-SA 4.0) * **Cyberpunk Night** theme by Anders Lemvigh (CC BY-SA 4.0)
* **Dracula** theme by Zeno Rocha (MIT License) * **Dracula** theme by Zeno Rocha (MIT License)
+1
View File
@@ -75,6 +75,7 @@ storage for robustness.
more/counting more/counting
more/typography more/typography
more/dictionaries
more/customise more/customise
more/handling_errors more/handling_errors
more/project_format more/project_format
+185 -168
View File
@@ -1,94 +1,35 @@
.. _docs_more_custom: .. _docs_more_custom:
************** *************
Customisations Custom Themes
************** *************
.. _Enchant: https://rrthomas.github.io/enchant/ There are a few ways you can customise novelWriter yourself. Currently, you can relatively easily
.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/ add new GUI themes. You can also add new icon themes, although this is not as straightforward.
There are a few ways you can customise novelWriter yourself. Currently, you can add new GUI themes,
your own syntax themes, and install additional dictionaries.
.. _docs_more_custom_dict:
Spell Check Dictionaries
========================
novelWriter uses Enchant_ as the spell checking tool. Depending on your operating system, it may or
may not load all installed spell check dictionaries automatically.
Linux and MacOS
---------------
On Linux and MacOS, you generally only have to install hunspell, aspell or myspell dictionaries on
your system like you do for other applications. See your distro or OS documentation for how to do
this. These dictionaries should show up as available spell check languages in novelWriter.
Windows
-------
For Windows, English is included with the installation. For other languages you have to download
and add dictionaries yourself.
**Install Tool**
A small tool to assist with this can be found under **Tools > Add Dictionaries**. It will import
spell checking dictionaries from Free Office or Libre Office extensions. The dictionaries are then
installed in the install location for the Enchant library and should thus work for any application
that uses Enchant for spell checking.
**Manual Install**
If you prefer to do this manually or want to use a different source than the ones mentioned above,
You need to get compatible dictionary files for your language. You need two files files ending with
``.aff`` and ``.dic``. These files must then be copied to the following location:
``C:\Users\<USER>\AppData\Local\enchant\hunspell``
This assumes your user profile is stored at ``C:\Users\<USER>``. The last one or two folders may
not exist, so you may need to create them.
You can find the various dictionaries on the `Free Desktop`_ website.
.. note::
The Free Desktop link points to a repository, and what may look like file links inside the
dictionary folder are actually links to web pages. If you right-click and download those, you
get HTML files, not dictionaries!
In order to download the actual dictionary files, right-click the "plain" label at the end of
each line and download that.
.. _docs_more_custom_theme: .. _docs_more_custom_theme:
Syntax and GUI Themes Colour Themes
===================== =============
Adding your own GUI and syntax themes is relatively easy, although it requires that you manually Adding your own colour themes is relatively easy, although it requires that you manually edit
edit config files with colour values. The themes are defined by simple plain text config files with config files with colour values. The themes are defined by simple plain text config files with meta
meta data and colour settings. data and colour settings.
In order to make your own versions, first copy one of the existing files to your local computer and In order to make your own versions, first copy one of the existing files to your local computer and
modify it as you like. modify it as you like.
* The existing syntax themes are stored in The existing colour themes are stored in
`novelwriter/assets/syntax <https://github.com/vkbo/novelWriter/tree/main/novelwriter/assets/syntax>`_. `novelwriter/assets/themes <https://github.com/vkbo/novelWriter/tree/main/novelwriter/assets/themes>`_.
* The existing GUI themes are stored in
`novelwriter/assets/themes <https://github.com/vkbo/novelWriter/tree/main/novelwriter/assets/themes>`_.
* The existing icon themes are stored in
`novelwriter/assets/icons <https://github.com/vkbo/novelWriter/tree/main/novelwriter/assets/icons>`_.
Remember to also change the name of your theme by modifying the ``name`` setting at the top of the Remember to also change the name of your theme by modifying the ``name`` setting at the top of the
file, otherwise you may not be able to distinguish them in **Preferences**. file, otherwise you may not be able to distinguish them in **Preferences**.
For novelWriter to be able to locate the custom theme files, you must copy them to the For novelWriter to be able to locate the custom theme files, you must copy them to the
:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder there named :ref:`docs_technical_locations_data` location in your home or user area. There should be a folder
``syntax`` for syntax themes, just ``themes`` for GUI themes, and ``icons`` for icon themes. These there named ``themes`` for colour themes. These folders are created the first time you start
folders are created the first time you start novelWriter. novelWriter.
Once the files are copied there, they should show up in **Preferences** with the label you Once the files are copied there, they should show up in **Preferences** with the label you
set as ``name`` inside the file. set as ``name`` inside the file.
@@ -99,71 +40,123 @@ set as ``name`` inside the file.
files up to date. files up to date.
Custom GUI and Icons Theme The Theme File Format
-------------------------- ---------------------
A GUI theme ``.conf`` file consists of the following settings: A colour theme ``.conf`` file consists of the following settings:
.. code-block:: cfg .. code-block:: cfg
:caption: The theme file for the "Default Light Theme"
[Main] [Main]
name = My Custom Theme name = Default Light Theme
description = A description of my custom theme mode = light
author = Jane Doe description = The novelWriter standard light theme
credit = John Doe author = Veronica Berglyd Olsen
url = https://example.com credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0 license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Icons] [Base]
default = 100, 100, 100 default = #303030
faded = 100, 100, 100 faded = #6c6c6c
red = 255, 0, 0 red = #a62a2d
orange = 255, 128, 0 orange = #b36829
yellow = 255, 255, 0 yellow = #a68542
green = 0, 255, 0 green = #296629
aqua = 0, 255, 255 cyan = #269999
blue = 0, 0, 255 blue = #3a70a6
purple = 255, 0, 255 purple = #b35ab3
[Project] [Project]
root = 0, 255, 255 root = blue
folder = 255, 255, 0 folder = yellow
file = 100, 100, 100 file = default
title = 0, 255, 0 title = green
chapter = 255, 0, 0 chapter = red
scene = 0, 0, 255 scene = blue
note = 255, 255, 0 note = yellow
[Palette] [Palette]
window = 100, 100, 100 window = #efefef
windowtext = 100, 100, 100 windowtext = #000000
base = 100, 100, 100 base = #ffffff
alternatebase = 100, 100, 100 alternatebase = #e0e0e0
text = 100, 100, 100 text = #000000
tooltipbase = 100, 100, 100 tooltipbase = #ffffc0
tooltiptext = 100, 100, 100 tooltiptext = #15150d
button = 100, 100, 100 button = #efefef
buttontext = 100, 100, 100 buttontext = #000000
brighttext = 100, 100, 100 brighttext = #ffffff
highlight = 100, 100, 100 highlight = #3087c6
highlightedtext = 100, 100, 100 highlightedtext = #ffffff
link = 100, 100, 100 link = blue
linkvisited = 100, 100, 100 linkvisited = blue
[GUI] [GUI]
helptext = 0, 0, 0 helptext = #5c5c5c
fadedtext = 128, 128, 128 fadedtext = #6c6c6c
errortext = 255, 0, 0 errortext = red
In the Main section you must at least define the ``name`` settings. [Syntax]
background = #ffffff
text = #000000
link = blue
headertext = green
headertag = green:160
emphasis = orange
dialog = blue
altdialog = blue
note = yellow
hidden = faded
shortcode = green
keyword = red
tag = green
value = blue
optional = green
spellcheckline = red
errorline = green
replacetag = green
modifier = green
texthighlight = #c8c80060
The Palette values correspond to the Qt enum values for ``QPalette::ColorRole``, see the
`Qt documentation <https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum>`_ for more details. The Theme Sections
colour values are RGB numbers on the format ``r, g, b`` where each is an integer from ``0`` to --------------
``255``. Omitted values are not loaded and will use default values. If the ``helptext`` colour is
not defined, it is computed as a colour between the ``window`` and ``windowtext`` colour. .. _ColorRole: https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum
Additional shades of some of the colours are also computed. These are mainly used for 3D effects.
The theme file is made up of different sections depending on what part of novelWriter the theme
affects.
.. csv-table:: Theme Sections Overview
:header: "Section", "Description"
:class: "tight-table"
"``[Main]``", "Meta data about the theme, You must at least set ``name`` and ``mode``, and ``mode`` must be either ``light`` or ``dark``."
"``[Base]``", "The base colours of the theme. These are also selectable colours in various places inside the app, like for icon colours in **Preferences**."
"``[Project]``", "The colours used for icons and markers for the different project item types."
"``[Palette]``", "The colours used for styling the user interface. The values correspond to the ColorRole_ values in the Qt library."
"``[GUI]``", "The colours used for styling additional elements of the user interface."
"``[Syntax]``", "The colours used for syntax highlighting in documents."
Colour Value Formats
--------------------
There are several ways to enter colour values:
.. csv-table:: Colour Formats
:header: "Syntax", "Description"
:class: "tight-table"
"``#RRGGBB``", "A CSS style hexadecimal values, like ``#ff0000`` for red."
"``#RRGGBBAA``", "A CSS style hexadecimal values with transparency, like ``#ff00007f`` for half-transparent red."
"``name``", "A name referring to one of the colours already specified under the ``[Base]`` section, like ``red``. Note that you should not use named colours in the ``[Base]`` section itself as that may have unintended results."
"``name:alpha``", "A name referring to one of the colours already specified under the ``[Base]`` section, with a transparency value added. The alpha value must be in the range ``0`` to ``255``, like ``red:127`` for half-transparent red."
"``r, g, b``", "A set of red, green and blue numbers in the range ``0`` to ``255``, like ``255, 0, 0`` for red."
"``r, g, b, a``", "A set of red, green, blue and alpha numbers in the range ``0`` to ``255``, like ``255, 0, 0, 127`` for half-transparent red."
.. versionadded:: 2.5 .. versionadded:: 2.5
The ``fadedtext`` and ``errortext`` theme colour entries were added. The ``fadedtext`` and ``errortext`` theme colour entries were added.
@@ -172,60 +165,84 @@ Additional shades of some of the colours are also computed. These are mainly use
The ``icontheme`` setting was dropped as the icon theme is now its own setting. The ``icontheme`` setting was dropped as the icon theme is now its own setting.
The ``[Icons]`` and ``[Project]`` sections were added, and the ``status*`` settings removed. The ``[Icons]`` and ``[Project]`` sections were added, and the ``status*`` settings removed.
.. versionadded:: 2.8
The ``[Syntax]`` section was moved into the main theme file. Previously, these settings were in
their own file. The ``[Icons]`` section was renamed to ``[Base]``.
Custom Syntax Theme
-------------------
A syntax theme ``.conf`` file consists of the following settings: Icon Themes
===========
Icon themes are *not* straightforward to add, but if you want to make the effort, this section
describes how to do it.
The existing icon themes are stored in
`novelwriter/assets/icons <https://github.com/vkbo/novelWriter/tree/main/novelwriter/assets/icons>`_.
As with colour themes, remember to change the name of your theme by modifying the ``name`` setting
at the top of the file, otherwise you may not be able to distinguish them in **Preferences**.
For novelWriter to be able to locate the custom theme files, you must copy them to the
:ref:`docs_technical_locations_data` location in your home or user area. There should be a folder
there ``icons`` for icon themes. These folders are created the first time you start novelWriter.
The Icons File Format
---------------------
Icon themes are kept in files with the ``.icons`` file extension. The file format is a custom
format with entries on the form ``section:key = value``.
.. code-block:: cfg .. code-block:: cfg
:caption: The icons file for "Material Symbols - Rounded Medium" (truncated)
[Main] # Meta
name = My Syntax Theme meta:name = Material Symbols - Rounded Medium
author = Jane Doe meta:author = Google Inc
credit = John Doe meta:license = Apache 2.0
url = https://example.com
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax] # Icons
background = 255, 255, 255 icon:alert_error = <svg ...>
text = 0, 0, 0 icon:alert_info = <svg ...>
link = 0, 0, 0 icon:alert_question = <svg ...>
headertext = 0, 0, 0 icon:alert_warn = <svg ...>
headertag = 0, 0, 0 icon:cls_archive = <svg ...>
emphasis = 0, 0, 0 icon:cls_character = <svg ...>
dialog = 0, 0, 0 icon:cls_custom = <svg ...>
altdialog = 0, 0, 0 icon:cls_entity = <svg ...>
note = 0, 0, 0 icon:cls_none = <svg ...>
hidden = 0, 0, 0 icon:cls_novel = <svg ...>
shortcode = 0, 0, 0 icon:cls_object = <svg ...>
keyword = 0, 0, 0 icon:cls_plot = <svg ...>
tag = 0, 0, 0 icon:cls_template = <svg ...>
value = 0, 0, 0 icon:cls_timeline = <svg ...>
optional = 0, 0, 0 icon:cls_trash = <svg ...>
spellcheckline = 0, 0, 0 icon:cls_world = <svg ...>
errorline = 0, 0, 0
replacetag = 0, 0, 0
modifier = 0, 0, 0
texthighlight = 255, 255, 255, 128
In the Main section, you must define at least the ``name`` setting. The Syntax colour values are The icon keys are associated with icon placement locations inside novelWriter, and the template for
RGB(A) numbers of the format ``r, g, b, a`` where each is an integer from ``0`` to ``255``. The them is defined in the script that generates the default icon themes.
fourth value is the alpha channel, which can be omitted.
Omitted syntax colours default to black, except ``background`` which defaults to white, and The script can be found under
``texthighlight`` which defaults to white with half transparency. `utils/icon_themes.py <https://github.com/vkbo/novelWriter/blob/main/utils/icon_themes.py>`__
in the source code.
.. versionadded:: 2.2 This file includes all the code needed to generate the themes that are included in novelWriter. The
The ``shortcode`` syntax colour entry was added. icon keys are mapped to icon keys from the specific themes in JSON files in the ``icon_themes``
folder next to the script. This is the recommended way to generate these themes. Doing it manually
is not advisable.
.. versionadded:: 2.3
The ``optional`` syntax colour entry was added.
.. versionadded:: 2.4 Icon Value Format
The ``texthighlight`` syntax colour entry was added. -----------------
.. versionadded:: 2.5 As can be seen from the example, an icon is defined in the ``icon`` section with a key and an
The ``dialog``, ``altdialog``, ``note`` and ``tag`` syntax colour entries were added. in-line SVG XML block. The XML must fit on one line and obey the following rules:
``straightquotes``, ``doublequotes`` and ``singlequotes`` were removed.
#. It must be single colour, that is, the fill colour attribute must be able to colourise the
entire icon.
#. The fill colour attribute *must* be defined and must be set to: ``fill="#000000"``. This value
is replaced by the relevant theme colour when the icon is processed in novelWriter.
.. versionadded:: 2.7
The icon theme files were added. Previously, icons were stored as individual SVG files with a
config file mapping the file names to the internal icon keys.
+55
View File
@@ -0,0 +1,55 @@
.. _docs_more_dictionaries:
************************
Spell Check Dictionaries
************************
.. _Enchant: https://rrthomas.github.io/enchant/
.. _Free Desktop: https://cgit.freedesktop.org/libreoffice/dictionaries/tree/
Spell checking is provided by the Enchant_ library. Depending on your operating system, it may or
may not load all installed spell check dictionaries automatically.
Linux and MacOS
===============
On Linux and MacOS, you generally only have to install hunspell, aspell or myspell dictionaries on
your system like you do for other applications. See your distro or OS documentation for how to do
this. These dictionaries should show up as available spell check languages in novelWriter.
Windows
=======
For Windows, English is included with the installation. For other languages you have to download
and add dictionaries yourself.
**Install Tool**
A small tool to assist with this can be found under **Tools > Add Dictionaries**. It will import
spell checking dictionaries from Free Office or Libre Office extensions. The dictionaries are then
installed in the install location for the Enchant library and should thus work for any application
that uses Enchant for spell checking.
**Manual Install**
If you prefer to do this manually or want to use a different source than the ones mentioned above,
You need to get compatible dictionary files for your language. You need two files files ending with
``.aff`` and ``.dic``. These files must then be copied to the following location:
``C:\Users\<USER>\AppData\Local\enchant\hunspell``
This assumes your user profile is stored at ``C:\Users\<USER>``. The last one or two folders may
not exist, so you may need to create them.
You can find the various dictionaries on the `Free Desktop`_ website.
.. note::
The Free Desktop link points to a repository, and what may look like file links inside the
dictionary folder are actually links to web pages. If you right-click and download those, you
get HTML files, not dictionaries!
In order to download the actual dictionary files, right-click the "plain" label at the end of
each line and download that.
+1 -1
View File
@@ -55,7 +55,7 @@ edited from the **Project Word List** tool available from the **Tools** menu.
.. note:: .. note::
Generally, spell checking dictionaries are collected from your operating system, but on Windows Generally, spell checking dictionaries are collected from your operating system, but on Windows
they are not. See :ref:`docs_more_custom_dict` for how to add spell checking dictionaries on they are not. See :ref:`docs_more_dictionaries` for how to add spell checking dictionaries on
Windows. Windows.
+1 -1
View File
@@ -49,7 +49,7 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__version__ = "2.8 Alpha 0" __version__ = "2.8a0"
__hexversion__ = "0x020800a0" __hexversion__ = "0x020800a0"
__date__ = "2025-06-01" __date__ = "2025-06-01"
__status__ = "Stable" __status__ = "Stable"
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160L0 416c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 96c0 17.7-14.3 32-32 32L96 448c-17.7 0-32-14.3-32-32l0-256c0-17.7 14.3-32 32-32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L96 64z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M471.6 21.7c-21.9-21.9-57.3-21.9-79.2 0L362.3 51.7l97.9 97.9 30.1-30.1c21.9-21.9 21.9-57.3 0-79.2L471.6 21.7zm-299.2 220c-6.1 6.1-10.8 13.6-13.5 21.9l-29.6 88.8c-2.9 8.6-.6 18.1 5.8 24.6s15.9 8.7 24.6 5.8l88.8-29.6c8.2-2.7 15.7-7.4 21.9-13.5L437.7 172.3 339.7 74.3 172.4 241.7zM96 64C43 64 0 107 0 160L0 416c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-96c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 96c0 17.7-14.3 32-32 32L96 448c-17.7 0-32-14.3-32-32l0-256c0-17.7 14.3-32 32-32l96 0c17.7 0 32-14.3 32-32s-14.3-32-32-32L96 64z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" fill="#000000" height="128" width="128"><path d="M160 80c0-26.5 21.5-48 48-48l32 0c26.5 0 48 21.5 48 48l0 352c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48l0-352zM0 272c0-26.5 21.5-48 48-48l32 0c26.5 0 48 21.5 48 48l0 160c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48L0 272zM368 96l32 0c26.5 0 48 21.5 48 48l0 288c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48l0-288c0-26.5 21.5-48 48-48z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" fill="#000000" height="128" width="128"><path d="M160 80c0-26.5 21.5-48 48-48l32 0c26.5 0 48 21.5 48 48l0 352c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48l0-352zM0 272c0-26.5 21.5-48 48-48l32 0c26.5 0 48 21.5 48 48l0 160c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48L0 272zM368 96l32 0c26.5 0 48 21.5 48 48l0 288c0 26.5-21.5 48-48 48l-32 0c-26.5 0-48-21.5-48-48l0-288c0-26.5 21.5-48 48-48z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M361.5 1.2c5 2.1 8.6 6.6 9.6 11.9L391 121l107.9 19.8c5.3 1 9.8 4.6 11.9 9.6s1.5 10.7-1.6 15.2L446.9 256l62.3 90.3c3.1 4.5 3.7 10.2 1.6 15.2s-6.6 8.6-11.9 9.6L391 391 371.1 498.9c-1 5.3-4.6 9.8-9.6 11.9s-10.7 1.5-15.2-1.6L256 446.9l-90.3 62.3c-4.5 3.1-10.2 3.7-15.2 1.6s-8.6-6.6-9.6-11.9L121 391 13.1 371.1c-5.3-1-9.8-4.6-11.9-9.6s-1.5-10.7 1.6-15.2L65.1 256 2.8 165.7c-3.1-4.5-3.7-10.2-1.6-15.2s6.6-8.6 11.9-9.6L121 121 140.9 13.1c1-5.3 4.6-9.8 9.6-11.9s10.7-1.5 15.2 1.6L256 65.1 346.3 2.8c4.5-3.1 10.2-3.7 15.2-1.6zM160 256a96 96 0 1 1 192 0 96 96 0 1 1 -192 0zm224 0a128 128 0 1 0 -256 0 128 128 0 1 0 256 0z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" viewBox="-64 0 512 512" fill="#000000" height="128" width="128"><path d="M223.5 32C100 32 0 132.3 0 256S100 480 223.5 480c60.6 0 115.5-24.2 155.8-63.4c5-4.9 6.3-12.5 3.1-18.7s-10.1-9.7-17-8.5c-9.8 1.7-19.8 2.6-30.1 2.6c-96.9 0-175.5-78.8-175.5-176c0-65.8 36-123.1 89.3-153.3c6.1-3.5 9.2-10.5 7.7-17.3s-7.3-11.9-14.3-12.5c-6.3-.5-12.6-.8-19-.8z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="#000000" height="128" width="128"><path d="M448 256c0-106-86-192-192-192l0 384c106 0 192-86 192-192zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" fill="#000000" height="128" width="128"><path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 0 512 512" fill="#000000" height="128" width="128"><path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" viewBox="-64 0 512 512" fill="#000000" height="128" width="128"><path d="M0 48V487.7C0 501.1 10.9 512 24.3 512c5 0 9.9-1.5 14-4.4L192 400 345.7 507.6c4.1 2.9 9 4.4 14 4.4c13.4 0 24.3-10.9 24.3-24.3V48c0-26.5-21.5-48-48-48H48C21.5 0 0 21.5 0 48z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" viewBox="-64 0 512 512" fill="#000000" height="128" width="128"><path d="M0 48V487.7C0 501.1 10.9 512 24.3 512c5 0 9.9-1.5 14-4.4L192 400 345.7 507.6c4.1 2.9 9 4.4 14 4.4c13.4 0 24.3-10.9 24.3-24.3V48c0-26.5-21.5-48-48-48H48C21.5 0 0 21.5 0 48z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" viewBox="32 0 512 512" fill="#000000" height="128" width="128"><path d="M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" viewBox="32 0 512 512" fill="#000000" height="128" width="128"><path d="M88.7 223.8L0 375.8 0 96C0 60.7 28.7 32 64 32l117.5 0c17 0 33.3 6.7 45.3 18.7l26.5 26.5c12 12 28.3 18.7 45.3 18.7L416 96c35.3 0 64 28.7 64 64l0 32-336 0c-22.8 0-43.8 12.1-55.3 31.8zm27.6 16.1C122.1 230 132.6 224 144 224l400 0c11.5 0 22 6.1 27.7 16.1s5.7 22.2-.1 32.1l-112 192C453.9 474 443.4 480 432 480L32 480c-11.5 0-22-6.1-27.7-16.1s-5.7-22.2 .1-32.1l112-192z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" height="128" view
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M556.61-113.78v-63.74q0-10.83 4.13-20.31 4.13-9.47 11.83-17.17L781-423q10.7-10.7 23.67-15.54 12.98-4.85 26.24-4.85 14.26 0 27.24 5.35 12.98 5.34 23.68 16.04l37 37q9.69 10.7 15.04 23.67 5.35 12.98 5.35 26.24 0 13.83-4.85 27.03-4.85 13.19-15.54 23.89l-208 208q-7.7 7.69-17.18 11.54-9.48 3.85-20.3 3.85h-63.74q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54Zm279.96-187.96 31.34-33.35-37-37-32.34 32.35 38 38ZM166.78-140.78q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h181q21.09 0 40.39 7.98 19.31 7.98 34.26 22.94L480-730.74h313.22q44.3 0 75.15 30.85 30.85 30.85 30.85 75.15v57q0 21.96-17.89 35.22-17.89 13.26-40.85 9.13-9.78-.57-19.35.28-9.57.85-18.78 3.68-21.79 2.82-41.76 12.82-19.98 10-36.59 26.61L507.52-263.52q-14.95 14.95-22.93 33.98-7.98 19.02-7.98 40.67v1.31q0 19.82-13.48 33.3-13.48 13.48-33.3 13.48H166.78Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M556.61-113.78v-63.74q0-10.83 4.13-20.31 4.13-9.47 11.83-17.17L781-423q10.7-10.7 23.67-15.54 12.98-4.85 26.24-4.85 14.26 0 27.24 5.35 12.98 5.34 23.68 16.04l37 37q9.69 10.7 15.04 23.67 5.35 12.98 5.35 26.24 0 13.83-4.85 27.03-4.85 13.19-15.54 23.89l-208 208q-7.7 7.69-17.18 11.54-9.48 3.85-20.3 3.85h-63.74q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54Zm279.96-187.96 31.34-33.35-37-37-32.34 32.35 38 38ZM166.78-140.78q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h181q21.09 0 40.39 7.98 19.31 7.98 34.26 22.94L480-730.74h313.22q44.3 0 75.15 30.85 30.85 30.85 30.85 75.15v57q0 21.96-17.89 35.22-17.89 13.26-40.85 9.13-9.78-.57-19.35.28-9.57.85-18.78 3.68-21.79 2.82-41.76 12.82-19.98 10-36.59 26.61L507.52-263.52q-14.95 14.95-22.93 33.98-7.98 19.02-7.98 40.67v1.31q0 19.82-13.48 33.3-13.48 13.48-33.3 13.48H166.78Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M375.48-307q-114.09 0-193.55-79.46-79.45-79.45-79.45-193.54 0-114.09 79.45-193.54Q261.39-853 375.48-853q114.09 0 193.54 79.46 79.46 79.45 79.46 193.54 0 45.13-12.87 83.28T601-429.7l219.48 220.05q14.96 15.52 14.96 37.32 0 21.81-15.53 36.77-14.95 14.95-37.04 14.95t-37.04-14.95L526.91-354.48q-29.43 21.74-68.15 34.61Q420.04-307 375.48-307Zm0-106q69.91 0 118.45-48.54 48.55-48.55 48.55-118.46t-48.55-118.46Q445.39-747 375.48-747t-118.46 48.54Q208.48-649.91 208.48-580t48.54 118.46Q305.57-413 375.48-413Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M375.48-307q-114.09 0-193.55-79.46-79.45-79.45-79.45-193.54 0-114.09 79.45-193.54Q261.39-853 375.48-853q114.09 0 193.54 79.46 79.46 79.45 79.46 193.54 0 45.13-12.87 83.28T601-429.7l219.48 220.05q14.96 15.52 14.96 37.32 0 21.81-15.53 36.77-14.95 14.95-37.04 14.95t-37.04-14.95L526.91-354.48q-29.43 21.74-68.15 34.61Q420.04-307 375.48-307Zm0-106q69.91 0 118.45-48.54 48.55-48.55 48.55-118.46t-48.55-118.46Q445.39-747 375.48-747t-118.46 48.54Q208.48-649.91 208.48-580t48.54 118.46Q305.57-413 375.48-413Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M694.7-140.78q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-201.13q0-22.09 15.45-37.55 15.46-15.45 37.55-15.45h71.52q22.08 0 37.54 15.45 15.46 15.46 15.46 37.55v201.13q0 22.08-15.46 37.54t-37.54 15.46H694.7Zm-250.18 0q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54v-572.44q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46h70.96q22.09 0 37.54 15.46 15.46 15.46 15.46 37.54v572.44q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46h-70.96Zm-250.74 0q-22.08 0-37.54-15.46t-15.46-37.54v-372.44q0-22.08 15.46-37.54t37.54-15.46h71.52q22.09 0 37.55 15.46 15.45 15.46 15.45 37.54v372.44q0 22.08-15.45 37.54-15.46 15.46-37.55 15.46h-71.52Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M694.7-140.78q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-201.13q0-22.09 15.45-37.55 15.46-15.45 37.55-15.45h71.52q22.08 0 37.54 15.45 15.46 15.46 15.46 37.55v201.13q0 22.08-15.46 37.54t-37.54 15.46H694.7Zm-250.18 0q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54v-572.44q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46h70.96q22.09 0 37.54 15.46 15.46 15.46 15.46 37.54v572.44q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46h-70.96Zm-250.74 0q-22.08 0-37.54-15.46t-15.46-37.54v-372.44q0-22.08 15.46-37.54t37.54-15.46h71.52q22.09 0 37.55 15.46 15.45 15.46 15.45 37.54v372.44q0 22.08-15.45 37.54-15.46 15.46-37.55 15.46h-71.52Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-280q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-427q-22.09 0-37.54-15.46Q27-457.91 27-480t15.46-37.54Q57.91-533 80-533h80q22.09 0 37.54 15.46Q213-502.09 213-480t-15.46 37.54Q182.09-427 160-427H80Zm720 0q-22.09 0-37.54-15.46Q747-457.91 747-480t15.46-37.54Q777.91-533 800-533h80q22.09 0 37.54 15.46Q933-502.09 933-480t-15.46 37.54Q902.09-427 880-427h-80ZM480-747q-22.09 0-37.54-15.46Q427-777.91 427-800v-80q0-22.09 15.46-37.54Q457.91-933 480-933t37.54 15.46Q533-902.09 533-880v80q0 22.09-15.46 37.54Q502.09-747 480-747Zm0 720q-22.09 0-37.54-15.46Q427-57.91 427-80v-80q0-22.09 15.46-37.54Q457.91-213 480-213t37.54 15.46Q533-182.09 533-160v80q0 22.09-15.46 37.54Q502.09-27 480-27ZM216.96-668.39l-43-42Q158-725.35 158.5-747.43q.5-22.09 15.46-38.61Q189.35-802 211.43-802q22.09 0 37.61 15.96l42.57 43q14.96 15.39 14.67 36.76-.28 21.37-14.67 37.32-14.96 15.96-36.83 16.03-21.87.06-37.82-15.46Zm494 494.43-42.57-43q-14.96-15.39-14.96-36.97 0-21.59 14.96-37.11 14.96-15.96 36.83-15.46 21.87.5 37.82 15.46l43 41.43q15.96 14.96 15.46 37.04-.5 22.09-15.46 38.61Q770.65-158 748.57-158q-22.09 0-37.61-15.96Zm-42-494.43Q653-683.35 653.5-705.22q.5-21.87 15.46-37.82l41.43-43q14.96-15.96 37.04-15.46 22.09.5 38.61 15.46Q802-770.65 802-748.57q0 22.09-15.96 37.61l-43 42.57q-15.39 14.96-36.76 14.67-21.37-.28-37.32-14.67Zm-495 494.43Q158-189.35 158-211.43q0-22.09 15.96-37.61l43-42.57q15.39-14.96 36.97-14.96 21.59 0 37.11 14.96 15.96 14.96 15.46 36.83-.5 21.87-15.46 37.82l-41.43 43Q234.65-158 212.57-158.5q-22.09-.5-38.61-15.46Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480.57-111.52q-157.22 0-263.14-105.63Q111.52-322.78 111.52-480q0-128.39 77.57-229.89 77.56-101.5 214.17-129.24 18.65-5.39 33.74-1.59 15.09 3.81 25.04 13.37 10.52 9.57 14.13 23.83 3.61 14.26-1 30.35-9.65 23.74-15.6 47.08-5.96 23.35-5.96 48 0 93.96 66.11 159.79 66.11 65.82 160.63 65.82 24.78 0 49.63-5.32 24.85-5.33 46.02-14.55 15.52-4.17 28.72-.85 13.19 3.33 22.19 11.72 9.44 8.39 13.24 22.63 3.81 14.24-1.02 33.33Q814.96-276 713.72-193.76q-101.24 82.24-233.15 82.24Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M479.95-60.78q-86.91 0-163.3-32.91-76.39-32.92-133.22-89.74-56.82-56.83-89.74-133.27-32.91-76.44-32.91-163.35t32.91-163.3q32.92-76.39 89.74-133.22 56.83-56.82 133.27-89.74 76.44-32.91 163.35-32.91t163.3 32.91q76.39 32.92 133.22 89.74 56.82 56.83 89.74 133.27 32.91 76.44 32.91 163.35t-32.91 163.3q-32.92 76.39-89.74 133.22-56.83 56.82-133.27 89.74-76.44 32.91-163.35 32.91Zm46.83-110.13Q641.26-188.17 717.24-274t75.98-206q0-118.63-75.98-204.94-75.98-86.32-190.46-103.58v617.61Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M427-427H233.78q-22.08 0-37.54-15.46-15.46-15.45-15.46-37.54t15.46-37.54Q211.7-533 233.78-533H427v-193.22q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46t37.54 15.46Q533-748.3 533-726.22V-533h193.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54Q748.3-427 726.22-427H533v193.22q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46t-37.54-15.46Q427-211.7 427-233.78V-427Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M427-427H233.78q-22.08 0-37.54-15.46-15.46-15.45-15.46-37.54t15.46-37.54Q211.7-533 233.78-533H427v-193.22q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46t37.54 15.46Q533-748.3 533-726.22V-533h193.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54Q748.3-427 726.22-427H533v193.22q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46t-37.54-15.46Q427-211.7 427-233.78V-427Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M387-150.39 241.04-86.87q-53 22.65-100.87-8.48-47.87-31.13-47.87-88.83v-461.34q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h377.39q44.31 0 75.16 30.85 30.85 30.85 30.85 75.15v461.34q0 57.7-47.87 88.83-47.87 31.13-100.88 8.48L387-150.39Zm427.7-27.13q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-601h-441q-22.09 0-37.55-15.46-15.45-15.45-15.45-37.54t15.45-37.55q15.46-15.45 37.55-15.45h441q44.3 0 75.15 30.85 30.85 30.84 30.85 75.15v601q0 22.08-15.46 37.54t-37.54 15.46Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M387-150.39 241.04-86.87q-53 22.65-100.87-8.48-47.87-31.13-47.87-88.83v-461.34q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h377.39q44.31 0 75.16 30.85 30.85 30.85 30.85 75.15v461.34q0 57.7-47.87 88.83-47.87 31.13-100.88 8.48L387-150.39Zm427.7-27.13q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-601h-441q-22.09 0-37.55-15.46-15.45-15.45-15.45-37.54t15.45-37.55q15.46-15.45 37.55-15.45h441q44.3 0 75.15 30.85 30.85 30.84 30.85 75.15v601q0 22.08-15.46 37.54t-37.54 15.46Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M166.78-140.78q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.87 30.85-75.43 30.85-30.57 75.15-30.57h181q21.09 0 40.39 7.98 19.31 7.98 34.26 22.94L480-730.74h366.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54q-15.46 15.46-37.54 15.46H340.57q-69.35 0-121.57 43.52-52.22 43.53-52.22 110.31v226.95l70.52-226.82q10.83-33.35 38.55-53.65 27.72-20.31 62.76-20.31h471.91q54 0 85.7 43.52 31.69 43.53 15.61 94.53l-53.92 174.43q-14.78 45.78-46.17 68.63-31.39 22.85-78.31 22.85H166.78Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M166.78-140.78q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.87 30.85-75.43 30.85-30.57 75.15-30.57h181q21.09 0 40.39 7.98 19.31 7.98 34.26 22.94L480-730.74h366.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54q-15.46 15.46-37.54 15.46H340.57q-69.35 0-121.57 43.52-52.22 43.53-52.22 110.31v226.95l70.52-226.82q10.83-33.35 38.55-53.65 27.72-20.31 62.76-20.31h471.91q54 0 85.7 43.52 31.69 43.53 15.61 94.53l-53.92 174.43q-14.78 45.78-46.17 68.63-31.39 22.85-78.31 22.85H166.78Z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" height="128" view
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M560-120v-66q0-8 3-15.5t9-13.5l209-208q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q8 9 12.5 20t4.5 22q0 11-4 22.5T903-300L695-92q-6 6-13.5 9T666-80h-66q-17 0-28.5-11.5T560-120Zm263-184 37-39-37-37-38 38 38 38ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h320q33 0 56.5 23.5T880-640v83q0 18-14.5 29t-33.5 8h-11q-28 0-52.5 10T724-480L503-259q-11 11-17 25.5t-6 30.5v3q0 17-11.5 28.5T440-160H160Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M560-120v-66q0-8 3-15.5t9-13.5l209-208q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q8 9 12.5 20t4.5 22q0 11-4 22.5T903-300L695-92q-6 6-13.5 9T666-80h-66q-17 0-28.5-11.5T560-120Zm263-184 37-39-37-37-38 38 38 38ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h320q33 0 56.5 23.5T880-640v83q0 18-14.5 29t-33.5 8h-11q-28 0-52.5 10T724-480L503-259q-11 11-17 25.5t-6 30.5v3q0 17-11.5 28.5T440-160H160Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M380-320q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M380-320q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M680-160q-17 0-28.5-11.5T640-200v-200q0-17 11.5-28.5T680-440h80q17 0 28.5 11.5T800-400v200q0 17-11.5 28.5T760-160h-80Zm-240 0q-17 0-28.5-11.5T400-200v-560q0-17 11.5-28.5T440-800h80q17 0 28.5 11.5T560-760v560q0 17-11.5 28.5T520-160h-80Zm-240 0q-17 0-28.5-11.5T160-200v-360q0-17 11.5-28.5T200-600h80q17 0 28.5 11.5T320-560v360q0 17-11.5 28.5T280-160h-80Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M680-160q-17 0-28.5-11.5T640-200v-200q0-17 11.5-28.5T680-440h80q17 0 28.5 11.5T800-400v200q0 17-11.5 28.5T760-160h-80Zm-240 0q-17 0-28.5-11.5T400-200v-560q0-17 11.5-28.5T440-800h80q17 0 28.5 11.5T560-760v560q0 17-11.5 28.5T520-160h-80Zm-240 0q-17 0-28.5-11.5T160-200v-360q0-17 11.5-28.5T200-600h80q17 0 28.5 11.5T320-560v360q0 17-11.5 28.5T280-160h-80Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-280q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-440q-17 0-28.5-11.5T40-480q0-17 11.5-28.5T80-520h80q17 0 28.5 11.5T200-480q0 17-11.5 28.5T160-440H80Zm720 0q-17 0-28.5-11.5T760-480q0-17 11.5-28.5T800-520h80q17 0 28.5 11.5T920-480q0 17-11.5 28.5T880-440h-80ZM480-760q-17 0-28.5-11.5T440-800v-80q0-17 11.5-28.5T480-920q17 0 28.5 11.5T520-880v80q0 17-11.5 28.5T480-760Zm0 720q-17 0-28.5-11.5T440-80v-80q0-17 11.5-28.5T480-200q17 0 28.5 11.5T520-160v80q0 17-11.5 28.5T480-40ZM226-678l-43-42q-12-11-11.5-28t11.5-29q12-12 29-12t28 12l42 43q11 12 11 28t-11 28q-11 12-27.5 11.5T226-678Zm494 495-42-43q-11-12-11-28.5t11-27.5q11-12 27.5-11.5T734-282l43 42q12 11 11.5 28T777-183q-12 12-29 12t-28-12Zm-42-495q-12-11-11.5-27.5T678-734l42-43q11-12 28-11.5t29 11.5q12 12 12 29t-12 28l-43 42q-12 11-28 11t-28-11ZM183-183q-12-12-12-29t12-28l43-42q12-11 28.5-11t27.5 11q12 11 11.5 27.5T282-226l-42 43q-11 12-28 11.5T183-183Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-120q-151 0-255.5-104.5T120-480q0-138 90-239.5T440-838q13-2 23 3.5t16 14.5q6 9 6.5 21t-7.5 23q-17 26-25.5 55t-8.5 61q0 90 63 153t153 63q31 0 61.5-9t54.5-25q11-7 22.5-6.5T819-479q10 5 15.5 15t3.5 24q-14 138-117.5 229T480-120Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm40-83q119-15 199.5-104.5T800-480q0-123-80.5-212.5T520-797v634Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M440-440H240q-17 0-28.5-11.5T200-480q0-17 11.5-28.5T240-520h200v-200q0-17 11.5-28.5T480-760q17 0 28.5 11.5T520-720v200h200q17 0 28.5 11.5T760-480q0 17-11.5 28.5T720-440H520v200q0 17-11.5 28.5T480-200q-17 0-28.5-11.5T440-240v-200Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M440-440H240q-17 0-28.5-11.5T200-480q0-17 11.5-28.5T240-520h200v-200q0-17 11.5-28.5T480-760q17 0 28.5 11.5T520-720v200h200q17 0 28.5 11.5T760-480q0 17-11.5 28.5T720-440H520v200q0 17-11.5 28.5T480-200q-17 0-28.5-11.5T440-240v-200Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M400-160 232-88q-40 17-76-6.5T120-161v-519q0-33 23.5-56.5T200-760h400q33 0 56.5 23.5T680-680v519q0 43-36 66.5T568-88l-168-72Zm400 0q-17 0-28.5-11.5T760-200v-640H280q-17 0-28.5-11.5T240-880q0-17 11.5-28.5T280-920h480q33 0 56.5 23.5T840-840v640q0 17-11.5 28.5T800-160Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M400-160 232-88q-40 17-76-6.5T120-161v-519q0-33 23.5-56.5T200-760h400q33 0 56.5 23.5T680-680v519q0 43-36 66.5T568-88l-168-72Zm400 0q-17 0-28.5-11.5T760-200v-640H280q-17 0-28.5-11.5T240-880q0-17 11.5-28.5T280-920h480q33 0 56.5 23.5T840-840v640q0 17-11.5 28.5T800-160Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h360q17 0 28.5 11.5T880-680q0 17-11.5 28.5T840-640H314q-62 0-108 39t-46 99v262l79-263q8-26 29.5-41.5T316-560h516q41 0 64.5 32.5T909-457l-72 240q-8 26-29.5 41.5T760-160H160Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h360q17 0 28.5 11.5T880-680q0 17-11.5 28.5T840-640H314q-62 0-108 39t-46 99v262l79-263q8-26 29.5-41.5T316-560h516q41 0 64.5 32.5T909-457l-72 240q-8 26-29.5 41.5T760-160H160Z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" height="128" view
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M569.23-152.31v-42.92q0-6.46 2.23-12.04 2.23-5.58 7.46-10.81l203.62-202.61q5.92-5.16 12.31-7.62 6.38-2.46 12.77-2.46 6.61 0 13.38 2.58 6.77 2.57 11.92 7.73l37 37.77q4.93 5.92 7.5 12.31Q880-364 880-357.62q0 6.39-2.08 12.89-2.07 6.5-8 12.42L667.31-129.69q-5.23 5.23-10.81 7.46-5.58 2.23-12.04 2.23h-42.92q-13.92 0-23.12-9.19-9.19-9.19-9.19-23.12Zm238.39-167.07 37-38.24-37-37.76-38 38 38 38ZM184.62-200q-27.62 0-46.12-18.5Q120-237 120-264.62v-430.76q0-27.62 18.5-46.12Q157-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h311.53q27.62 0 46.12 18.5Q840-643 840-615.38v61.46q0 12.61-9.88 20.92-9.89 8.31-22.74 6.85H804.85q-22.62 0-44.43 8.46-21.8 8.46-40.27 24.61L493-266.69q-8.69 8.69-13.92 20.5t-5.23 24.73v.69q0 8.54-6.12 14.65-6.11 6.12-14.65 6.12H184.62Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M569.23-152.31v-42.92q0-6.46 2.23-12.04 2.23-5.58 7.46-10.81l203.62-202.61q5.92-5.16 12.31-7.62 6.38-2.46 12.77-2.46 6.61 0 13.38 2.58 6.77 2.57 11.92 7.73l37 37.77q4.93 5.92 7.5 12.31Q880-364 880-357.62q0 6.39-2.08 12.89-2.07 6.5-8 12.42L667.31-129.69q-5.23 5.23-10.81 7.46-5.58 2.23-12.04 2.23h-42.92q-13.92 0-23.12-9.19-9.19-9.19-9.19-23.12Zm238.39-167.07 37-38.24-37-37.76-38 38 38 38ZM184.62-200q-27.62 0-46.12-18.5Q120-237 120-264.62v-430.76q0-27.62 18.5-46.12Q157-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h311.53q27.62 0 46.12 18.5Q840-643 840-615.38v61.46q0 12.61-9.88 20.92-9.89 8.31-22.74 6.85H804.85q-22.62 0-44.43 8.46-21.8 8.46-40.27 24.61L493-266.69q-8.69 8.69-13.92 20.5t-5.23 24.73v.69q0 8.54-6.12 14.65-6.11 6.12-14.65 6.12H184.62Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M381.54-350.77q-95.92 0-162.58-66.65-66.65-66.66-66.65-162.58 0-95.92 66.65-162.58 66.66-66.65 162.58-66.65 95.92 0 162.58 66.65 66.65 66.66 66.65 162.58 0 41.69-14.77 80.69t-38.77 66.69l236.31 236.31q5.61 5.62 6 13.77.38 8.16-6 14.54-6.39 6.38-14.16 6.38-7.76 0-14.15-6.38L528.92-404.31q-30 25.54-69 39.54t-78.38 14Zm0-40q79.61 0 134.42-54.81 54.81-54.8 54.81-134.42 0-79.62-54.81-134.42-54.81-54.81-134.42-54.81-79.62 0-134.42 54.81-54.81 54.8-54.81 134.42 0 79.62 54.81 134.42 54.8 54.81 134.42 54.81Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M381.54-350.77q-95.92 0-162.58-66.65-66.65-66.66-66.65-162.58 0-95.92 66.65-162.58 66.66-66.65 162.58-66.65 95.92 0 162.58 66.65 66.65 66.66 66.65 162.58 0 41.69-14.77 80.69t-38.77 66.69l236.31 236.31q5.61 5.62 6 13.77.38 8.16-6 14.54-6.39 6.38-14.16 6.38-7.76 0-14.15-6.38L528.92-404.31q-30 25.54-69 39.54t-78.38 14Zm0-40q79.61 0 134.42-54.81 54.81-54.8 54.81-134.42 0-79.62-54.81-134.42-54.81-54.81-134.42-54.81-79.62 0-134.42 54.81-54.81 54.8-54.81 134.42 0 79.62 54.81 134.42 54.8 54.81 134.42 54.81Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M672.31-200q-13.73 0-23.02-9.29T640-232.31V-360q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29T760-360v127.69q0 13.73-9.29 23.02T727.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T420-232.31v-495.38q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29t9.29 23.02v495.38q0 13.73-9.29 23.02T507.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T200-232.31v-302.7q0-14.3 9.29-23.49 9.29-9.19 23.02-9.19h55.38q13.73 0 23.02 9.29 9.29 9.28 9.29 23.02v302.7q0 14.3-9.29 23.49-9.29 9.19-23.02 9.19h-55.38Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M672.31-200q-13.73 0-23.02-9.29T640-232.31V-360q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29T760-360v127.69q0 13.73-9.29 23.02T727.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T420-232.31v-495.38q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29t9.29 23.02v495.38q0 13.73-9.29 23.02T507.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T200-232.31v-302.7q0-14.3 9.29-23.49 9.29-9.19 23.02-9.19h55.38q13.73 0 23.02 9.29 9.29 9.28 9.29 23.02v302.7q0 14.3-9.29 23.49-9.29 9.19-23.02 9.19h-55.38Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-320q-66.85 0-113.42-46.58Q320-413.15 320-480t46.58-113.42Q413.15-640 480-640t113.42 46.58Q640-546.85 640-480t-46.58 113.42Q546.85-320 480-320ZM80-460q-8.5 0-14.25-5.76T60-480.03q0-8.51 5.75-14.24T80-500h100q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T180-460H80Zm700 0q-8.5 0-14.25-5.76T760-480.03q0-8.51 5.75-14.24T780-500h100q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T880-460H780ZM479.97-760q-8.51 0-14.24-5.75T460-780v-100q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-880v100q0 8.5-5.76 14.25T479.97-760Zm0 700q-8.51 0-14.24-5.75T460-80v-100q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-180v100q0 8.5-5.76 14.25T479.97-60ZM254.46-678.77l-57.61-55.85q-5.85-5.61-5.73-13.76.11-8.16 5.96-14.77 6.38-6.62 14.15-6.62 7.77 0 14.15 6.62L282-706.31q6.38 6.62 6.38 14.16 0 7.53-6 14.15t-13.65 6.12q-7.65-.5-14.27-6.89Zm480.16 481.92L678-253.69q-6.38-6.62-6.38-14.27 0-7.66 6.38-14.04 5.23-6.62 13.08-6.12 7.84.5 14.46 6.89l57.61 55.85q5.85 5.61 5.73 13.76-.11 8.16-5.96 14.77-6.38 6.62-14.15 6.62-7.77 0-14.15-6.62ZM678-677.62q-6.62-6-6.12-13.65t6.89-14.27l55.85-57.61q5.61-5.85 13.76-5.73 8.16.11 14.77 5.96 6.62 6.38 6.62 14.15 0 7.77-6.62 14.15L706.31-678q-6.62 6.38-14.16 6.38-7.53 0-14.15-6ZM196.85-196.68q-6.62-6.78-6.62-14.55 0-7.77 6.62-14.15L253.69-282q6.62-6.38 14.27-6.38 7.66 0 14.04 6.38 5.85 5.23 5.35 13.08-.5 7.84-6.12 14.46l-55.85 57.61q-6.38 6.62-14.15 6.5-7.77-.11-14.38-6.33Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M482.31-160Q349-160 255.65-253.35 162.31-346.69 162.31-480q0-98.77 56.54-181.42 56.53-82.66 160.38-118.89 12.23-4.31 21.46-2.27 9.23 2.04 15.23 7.96 6 5.93 7.66 15.24 1.65 9.3-2.5 19.53-7.77 19.08-11.27 38.85-3.5 19.77-3.5 41 0 106.92 74.54 181.46Q555.38-404 662.31-404q27.92 0 51.11-5.92 23.2-5.93 39.5-9.62 8.7-1.61 15.96.42 7.27 2.04 11.66 7.04 4.61 5 6.27 12.31 1.65 7.31-1.89 16.69-28.61 98-112.11 160.54Q589.31-160 482.31-160Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480.13-120q-74.67 0-140.41-28.34-65.73-28.34-114.36-76.92-48.63-48.58-76.99-114.26Q120-405.19 120-479.87q0-74.67 28.34-140.41 28.34-65.73 76.92-114.36 48.58-48.63 114.26-76.99Q405.19-840 479.87-840q74.67 0 140.41 28.34 65.73 28.34 114.36 76.92 48.63 48.58 76.99 114.26Q840-554.81 840-480.13q0 74.67-28.34 140.41-28.34 65.73-76.92 114.36-48.58 48.63-114.26 76.99Q554.81-120 480.13-120ZM500-160.69q123.62-8.08 211.81-98.35T800-480q0-130.69-87.42-220.19-87.43-89.5-212.58-99.12v638.62Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M460-460H260q-8.5 0-14.25-5.76T240-480.03q0-8.51 5.75-14.24T260-500h200v-200q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-700v200h200q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T700-460H500v200q0 8.5-5.76 14.25T479.97-240q-8.51 0-14.24-5.75T460-260v-200Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M460-460H260q-8.5 0-14.25-5.76T240-480.03q0-8.51 5.75-14.24T260-500h200v-200q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-700v200h200q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T700-460H500v200q0 8.5-5.76 14.25T479.97-240q-8.51 0-14.24-5.75T460-260v-200Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="m420-203.08-149.54 64.31q-32.31 13.92-61.38-5.35Q180-163.38 180-197.92v-457.46q0-27.62 18.5-46.12Q217-720 244.62-720h350.76q27.62 0 46.12 18.5Q660-683 660-655.38v457.46q0 34.54-29.08 53.8-29.07 19.27-61.38 5.35L420-203.08ZM760-220q-8.54 0-14.27-5.73T740-240v-535.39q0-9.23-7.69-16.92-7.69-7.69-16.93-7.69H320q-8.54 0-14.27-5.73T300-820q0-8.54 5.73-14.27T320-840h395.38q27.62 0 46.12 18.5Q780-803 780-775.39V-240q0 8.54-5.73 14.27T760-220Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="m420-203.08-149.54 64.31q-32.31 13.92-61.38-5.35Q180-163.38 180-197.92v-457.46q0-27.62 18.5-46.12Q217-720 244.62-720h350.76q27.62 0 46.12 18.5Q660-683 660-655.38v457.46q0 34.54-29.08 53.8-29.07 19.27-61.38 5.35L420-203.08ZM760-220q-8.54 0-14.27-5.73T740-240v-535.39q0-9.23-7.69-16.92-7.69-7.69-16.93-7.69H320q-8.54 0-14.27-5.73T300-820q0-8.54 5.73-14.27T320-840h395.38q27.62 0 46.12 18.5Q780-803 780-775.39V-240q0 8.54-5.73 14.27T760-220Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M180-200q-25.31 0-42.65-17.35Q120-234.69 120-260v-435.38q0-25.31 19.65-44.97Q159.31-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h349.23q8.54 0 14.27 5.73t5.73 14.27q0 8.54-5.73 14.27T813.08-640H297.85Q242-640 201-604.08q-41 35.93-41 90.54v248.92q0 8.47 4.23 13.85 4.23 5.39 11.15 9.23l76.7-256.84q6.46-20.62 23.73-33.43 17.27-12.81 38.65-12.81h512.92q32.54 0 52.2 25.97 19.65 25.96 10.19 57.03l-65.85 219.24q-5.69 19.07-21.8 30.73Q786-200 766.15-200H180Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M180-200q-25.31 0-42.65-17.35Q120-234.69 120-260v-435.38q0-25.31 19.65-44.97Q159.31-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h349.23q8.54 0 14.27 5.73t5.73 14.27q0 8.54-5.73 14.27T813.08-640H297.85Q242-640 201-604.08q-41 35.93-41 90.54v248.92q0 8.47 4.23 13.85 4.23 5.39 11.15 9.23l76.7-256.84q6.46-20.62 23.73-33.43 17.27-12.81 38.65-12.81h512.92q32.54 0 52.2 25.97 19.65 25.96 10.19 57.03l-65.85 219.24q-5.69 19.07-21.8 30.73Q786-200 766.15-200H180Z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" height="128" view
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M166.78-246.78v-466.44 466.44Zm0 106q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h181q21.23 0 40.46 7.98 19.24 7.98 34.19 22.94L480-730.74h313.22q44.3 0 75.15 30.85 30.85 30.85 30.85 75.15v51.17q0 22.09-15.46 37.55-15.46 15.45-37.54 15.45-22.09 0-37.55-15.45-15.45-15.46-15.45-37.55v-51.17H436.26l-88.48-88.48h-181v466.44h256.83q22.09 0 37.54 15.45 15.46 15.46 15.46 37.55 0 22.08-15.46 37.54-15.45 15.46-37.54 15.46H166.78Zm389.83 27v-63.74q0-10.83 4.13-20.31 4.13-9.47 11.83-17.17L781-423q10.7-10.8 23.77-15.59 13.07-4.8 26.14-4.8 14.26 0 27.27 5.35 13.01 5.34 23.65 16.04l37 37q9.59 10.7 14.99 23.77t5.4 26.14q0 13.83-4.85 27.03-4.85 13.19-15.54 23.89l-208 208q-7.62 7.69-17.14 11.54-9.52 3.85-20.34 3.85h-63.74q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54Zm311.3-221.31-37-37 37 37Zm-240 203h38l118.74-119.74-18-19-19-18-119.74 118.74v38Zm138.74-138.74-19-18 37 37-18-19Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M166.78-246.78v-466.44 466.44Zm0 106q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h181q21.23 0 40.46 7.98 19.24 7.98 34.19 22.94L480-730.74h313.22q44.3 0 75.15 30.85 30.85 30.85 30.85 75.15v51.17q0 22.09-15.46 37.55-15.46 15.45-37.54 15.45-22.09 0-37.55-15.45-15.45-15.46-15.45-37.55v-51.17H436.26l-88.48-88.48h-181v466.44h256.83q22.09 0 37.54 15.45 15.46 15.46 15.46 37.55 0 22.08-15.46 37.54-15.45 15.46-37.54 15.46H166.78Zm389.83 27v-63.74q0-10.83 4.13-20.31 4.13-9.47 11.83-17.17L781-423q10.7-10.8 23.77-15.59 13.07-4.8 26.14-4.8 14.26 0 27.27 5.35 13.01 5.34 23.65 16.04l37 37q9.59 10.7 14.99 23.77t5.4 26.14q0 13.83-4.85 27.03-4.85 13.19-15.54 23.89l-208 208q-7.62 7.69-17.14 11.54-9.52 3.85-20.34 3.85h-63.74q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54Zm311.3-221.31-37-37 37 37Zm-240 203h38l118.74-119.74-18-19-19-18-119.74 118.74v38Zm138.74-138.74-19-18 37 37-18-19Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M375.48-307q-114.09 0-193.55-79.46-79.45-79.45-79.45-193.54 0-114.09 79.45-193.54Q261.39-853 375.48-853q114.09 0 193.54 79.46 79.46 79.45 79.46 193.54 0 45.13-12.87 83.28T601-429.7l219.48 220.05q14.96 15.52 14.96 37.32 0 21.81-15.53 36.77-14.95 14.95-37.04 14.95t-37.04-14.95L526.91-354.48q-29.43 21.74-68.15 34.61Q420.04-307 375.48-307Zm0-106q69.91 0 118.45-48.54 48.55-48.55 48.55-118.46t-48.55-118.46Q445.39-747 375.48-747t-118.46 48.54Q208.48-649.91 208.48-580t48.54 118.46Q305.57-413 375.48-413Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M375.48-307q-114.09 0-193.55-79.46-79.45-79.45-79.45-193.54 0-114.09 79.45-193.54Q261.39-853 375.48-853q114.09 0 193.54 79.46 79.46 79.45 79.46 193.54 0 45.13-12.87 83.28T601-429.7l219.48 220.05q14.96 15.52 14.96 37.32 0 21.81-15.53 36.77-14.95 14.95-37.04 14.95t-37.04-14.95L526.91-354.48q-29.43 21.74-68.15 34.61Q420.04-307 375.48-307Zm0-106q69.91 0 118.45-48.54 48.55-48.55 48.55-118.46t-48.55-118.46Q445.39-747 375.48-747t-118.46 48.54Q208.48-649.91 208.48-580t48.54 118.46Q305.57-413 375.48-413Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M694.7-140.78q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-201.13q0-22.09 15.45-37.55 15.46-15.45 37.55-15.45h71.52q22.08 0 37.54 15.45 15.46 15.46 15.46 37.55v201.13q0 22.08-15.46 37.54t-37.54 15.46H694.7Zm-250.18 0q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54v-572.44q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46h70.96q22.09 0 37.54 15.46 15.46 15.46 15.46 37.54v572.44q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46h-70.96Zm-250.74 0q-22.08 0-37.54-15.46t-15.46-37.54v-372.44q0-22.08 15.46-37.54t37.54-15.46h71.52q22.09 0 37.55 15.46 15.45 15.46 15.45 37.54v372.44q0 22.08-15.45 37.54-15.46 15.46-37.55 15.46h-71.52Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M694.7-140.78q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-201.13q0-22.09 15.45-37.55 15.46-15.45 37.55-15.45h71.52q22.08 0 37.54 15.45 15.46 15.46 15.46 37.55v201.13q0 22.08-15.46 37.54t-37.54 15.46H694.7Zm-250.18 0q-22.09 0-37.54-15.46-15.46-15.46-15.46-37.54v-572.44q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46h70.96q22.09 0 37.54 15.46 15.46 15.46 15.46 37.54v572.44q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46h-70.96Zm-250.74 0q-22.08 0-37.54-15.46t-15.46-37.54v-372.44q0-22.08 15.46-37.54t37.54-15.46h71.52q22.09 0 37.55 15.46 15.45 15.46 15.45 37.54v372.44q0 22.08-15.45 37.54-15.46 15.46-37.55 15.46h-71.52Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-386q39.26 0 66.63-27.37Q574-440.74 574-480q0-39.26-27.37-66.63Q519.26-574 480-574q-39.26 0-66.63 27.37Q386-519.26 386-480q0 39.26 27.37 66.63Q440.74-386 480-386Zm0 106q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-427q-22.09 0-37.54-15.46Q27-457.91 27-480t15.46-37.54Q57.91-533 80-533h80q22.09 0 37.54 15.46Q213-502.09 213-480t-15.46 37.54Q182.09-427 160-427H80Zm720 0q-22.09 0-37.54-15.46Q747-457.91 747-480t15.46-37.54Q777.91-533 800-533h80q22.09 0 37.54 15.46Q933-502.09 933-480t-15.46 37.54Q902.09-427 880-427h-80ZM480-747q-22.09 0-37.54-15.46Q427-777.91 427-800v-80q0-22.09 15.46-37.54Q457.91-933 480-933t37.54 15.46Q533-902.09 533-880v80q0 22.09-15.46 37.54Q502.09-747 480-747Zm0 720q-22.09 0-37.54-15.46Q427-57.91 427-80v-80q0-22.09 15.46-37.54Q457.91-213 480-213t37.54 15.46Q533-182.09 533-160v80q0 22.09-15.46 37.54Q502.09-27 480-27ZM216.96-668.39l-43-42Q158-725.35 158.5-747.43q.5-22.09 15.46-38.61Q189.35-802 211.43-802q22.09 0 37.61 15.96l42.57 43q14.96 15.39 14.67 36.76-.28 21.37-14.67 37.32-14.96 15.96-36.83 16.03-21.87.06-37.82-15.46Zm494 494.43-42.57-43q-14.96-15.39-14.96-36.97 0-21.59 14.96-37.11 14.96-15.96 36.83-15.46 21.87.5 37.82 15.46l43 41.43q15.96 14.96 15.46 37.04-.5 22.09-15.46 38.61Q770.65-158 748.57-158q-22.09 0-37.61-15.96Zm-42-494.43Q653-683.35 653.5-705.22q.5-21.87 15.46-37.82l41.43-43q14.96-15.96 37.04-15.46 22.09.5 38.61 15.46Q802-770.65 802-748.57q0 22.09-15.96 37.61l-43 42.57q-15.39 14.96-36.76 14.67-21.37-.28-37.32-14.67Zm-495 494.43Q158-189.35 158-211.43q0-22.09 15.96-37.61l43-42.57q15.39-14.96 36.97-14.96 21.59 0 37.11 14.96 15.96 14.96 15.46 36.83-.5 21.87-15.46 37.82l-41.43 43Q234.65-158 212.57-158.5q-22.09-.5-38.61-15.46ZM480-480Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480.57-111.52q-157.22 0-263.14-105.63Q111.52-322.78 111.52-480q0-128.39 77.57-229.89 77.56-101.5 214.17-129.24 18.65-5.39 33.74-1.59 15.09 3.81 25.33 13.37 10.23 9.57 13.84 23.83 3.61 14.26-1 30.35-9.65 23.74-15.6 47.08-5.96 23.35-5.96 48 0 94.01 66.13 159.81t160.61 65.8q24.78 0 49.63-5.32 24.85-5.33 46.02-14.55 15.52-4.17 28.72-.85 13.19 3.33 21.93 11.72 9.7 8.39 13.5 22.63 3.81 14.24-1.02 33.33Q814.96-276 713.72-193.76q-101.24 82.24-233.15 82.24Zm0-106q73.3 0 133.97-37.2 60.68-37.19 93.24-98.8-14.35 2.74-28.41 4.04-14.07 1.31-27.85.18-120.74-9.61-205.82-93.29-85.09-83.67-95.83-208.93-.57-13.78.46-28.13 1.02-14.35 4.32-28.13-61.04 33.13-98.8 94.09-37.76 60.95-37.76 133.69 0 108.74 76.87 185.61 76.86 76.87 185.61 76.87Zm-17.35-245.13Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M479.95-60.78q-86.91 0-163.3-32.91-76.39-32.92-133.22-89.74-56.82-56.83-89.74-133.27-32.91-76.44-32.91-163.35t32.91-163.3q32.92-76.39 89.74-133.22 56.83-56.82 133.27-89.74 76.44-32.91 163.35-32.91t163.3 32.91q76.39 32.92 133.22 89.74 56.82 56.83 89.74 133.27 32.91 76.44 32.91 163.35t-32.91 163.3q-32.92 76.39-89.74 133.22-56.83 56.82-133.27 89.74-76.44 32.91-163.35 32.91Zm46.83-110.13Q641.26-188.17 717.24-274t75.98-206q0-118.63-75.98-204.94-75.98-86.32-190.46-103.58v617.61Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M427-427H233.78q-22.08 0-37.54-15.46-15.46-15.45-15.46-37.54t15.46-37.54Q211.7-533 233.78-533H427v-193.22q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46t37.54 15.46Q533-748.3 533-726.22V-533h193.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54Q748.3-427 726.22-427H533v193.22q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46t-37.54-15.46Q427-211.7 427-233.78V-427Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M427-427H233.78q-22.08 0-37.54-15.46-15.46-15.45-15.46-37.54t15.46-37.54Q211.7-533 233.78-533H427v-193.22q0-22.08 15.46-37.54 15.45-15.46 37.54-15.46t37.54 15.46Q533-748.3 533-726.22V-533h193.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54Q748.3-427 726.22-427H533v193.22q0 22.08-15.46 37.54-15.45 15.46-37.54 15.46t-37.54-15.46Q427-211.7 427-233.78V-427Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M387-150.39 241.04-86.87q-53 22.65-100.87-8.48-47.87-31.13-47.87-88.83v-461.34q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h377.39q44.31 0 75.16 30.85 30.85 30.85 30.85 75.15v461.34q0 57.7-47.87 88.83-47.87 31.13-100.88 8.48L387-150.39Zm0-115.7 188.69 82.61v-462.04H198.3v462.04L387-266.09Zm427.7 88.57q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-601h-441q-22.09 0-37.55-15.46-15.45-15.45-15.45-37.54t15.45-37.55q15.46-15.45 37.55-15.45h441q44.3 0 75.15 30.85 30.85 30.84 30.85 75.15v601q0 22.08-15.46 37.54t-37.54 15.46Zm-427.7-468H198.3h377.39H387Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M387-150.39 241.04-86.87q-53 22.65-100.87-8.48-47.87-31.13-47.87-88.83v-461.34q0-44.3 30.85-75.15 30.85-30.85 75.15-30.85h377.39q44.31 0 75.16 30.85 30.85 30.85 30.85 75.15v461.34q0 57.7-47.87 88.83-47.87 31.13-100.88 8.48L387-150.39Zm0-115.7 188.69 82.61v-462.04H198.3v462.04L387-266.09Zm427.7 88.57q-22.09 0-37.55-15.46-15.45-15.46-15.45-37.54v-601h-441q-22.09 0-37.55-15.46-15.45-15.45-15.45-37.54t15.45-37.55q15.46-15.45 37.55-15.45h441q44.3 0 75.15 30.85 30.85 30.84 30.85 75.15v601q0 22.08-15.46 37.54t-37.54 15.46Zm-427.7-468H198.3h377.39H387Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M166.78-140.78q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.87 30.85-75.43 30.85-30.57 75.15-30.57h181q21.23 0 40.46 7.98 19.24 7.98 34.19 22.94L480-730.74h366.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54q-15.46 15.46-37.54 15.46H436.26l-88.48-88.48h-181v469.26l70.52-226.82q10.83-33.35 38.55-53.65 27.72-20.31 62.76-20.31h471.91q54 0 85.7 43.52 31.69 43.53 15.61 94.53l-53.92 174.43q-14.78 45.78-46.17 68.63-31.39 22.85-78.31 22.85H166.78Zm112.83-106h471.35l59-191.96H338.61l-59 191.96ZM166.78-470.91v-242.31 242.31Zm112.83 224.13 59-191.96-59 191.96Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M166.78-140.78q-44.3 0-75.15-30.85-30.85-30.85-30.85-75.15v-466.44q0-44.87 30.85-75.43 30.85-30.57 75.15-30.57h181q21.23 0 40.46 7.98 19.24 7.98 34.19 22.94L480-730.74h366.22q22.08 0 37.54 15.46 15.46 15.45 15.46 37.54t-15.46 37.54q-15.46 15.46-37.54 15.46H436.26l-88.48-88.48h-181v469.26l70.52-226.82q10.83-33.35 38.55-53.65 27.72-20.31 62.76-20.31h471.91q54 0 85.7 43.52 31.69 43.53 15.61 94.53l-53.92 174.43q-14.78 45.78-46.17 68.63-31.39 22.85-78.31 22.85H166.78Zm112.83-106h471.35l59-191.96H338.61l-59 191.96ZM166.78-470.91v-242.31 242.31Zm112.83 224.13 59-191.96-59 191.96Z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" height="128" view
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-240v-480 480Zm0 80q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h320q33 0 56.5 23.5T880-640v80q0 17-11.5 28.5T840-520q-17 0-28.5-11.5T800-560v-80H447l-80-80H160v480h280q17 0 28.5 11.5T480-200q0 17-11.5 28.5T440-160H160Zm400 40v-66q0-8 3-15.5t9-13.5l209-208q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q8 9 12.5 20t4.5 22q0 11-4 22.5T903-300L695-92q-6 6-13.5 9T666-80h-66q-17 0-28.5-11.5T560-120Zm300-223-37-37 37 37ZM620-140h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-240v-480 480Zm0 80q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h320q33 0 56.5 23.5T880-640v80q0 17-11.5 28.5T840-520q-17 0-28.5-11.5T800-560v-80H447l-80-80H160v480h280q17 0 28.5 11.5T480-200q0 17-11.5 28.5T440-160H160Zm400 40v-66q0-8 3-15.5t9-13.5l209-208q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q8 9 12.5 20t4.5 22q0 11-4 22.5T903-300L695-92q-6 6-13.5 9T666-80h-66q-17 0-28.5-11.5T560-120Zm300-223-37-37 37 37ZM620-140h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M380-320q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M380-320q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M680-160q-17 0-28.5-11.5T640-200v-200q0-17 11.5-28.5T680-440h80q17 0 28.5 11.5T800-400v200q0 17-11.5 28.5T760-160h-80Zm-240 0q-17 0-28.5-11.5T400-200v-560q0-17 11.5-28.5T440-800h80q17 0 28.5 11.5T560-760v560q0 17-11.5 28.5T520-160h-80Zm-240 0q-17 0-28.5-11.5T160-200v-360q0-17 11.5-28.5T200-600h80q17 0 28.5 11.5T320-560v360q0 17-11.5 28.5T280-160h-80Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M680-160q-17 0-28.5-11.5T640-200v-200q0-17 11.5-28.5T680-440h80q17 0 28.5 11.5T800-400v200q0 17-11.5 28.5T760-160h-80Zm-240 0q-17 0-28.5-11.5T400-200v-560q0-17 11.5-28.5T440-800h80q17 0 28.5 11.5T560-760v560q0 17-11.5 28.5T520-160h-80Zm-240 0q-17 0-28.5-11.5T160-200v-360q0-17 11.5-28.5T200-600h80q17 0 28.5 11.5T320-560v360q0 17-11.5 28.5T280-160h-80Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM80-440q-17 0-28.5-11.5T40-480q0-17 11.5-28.5T80-520h80q17 0 28.5 11.5T200-480q0 17-11.5 28.5T160-440H80Zm720 0q-17 0-28.5-11.5T760-480q0-17 11.5-28.5T800-520h80q17 0 28.5 11.5T920-480q0 17-11.5 28.5T880-440h-80ZM480-760q-17 0-28.5-11.5T440-800v-80q0-17 11.5-28.5T480-920q17 0 28.5 11.5T520-880v80q0 17-11.5 28.5T480-760Zm0 720q-17 0-28.5-11.5T440-80v-80q0-17 11.5-28.5T480-200q17 0 28.5 11.5T520-160v80q0 17-11.5 28.5T480-40ZM226-678l-43-42q-12-11-11.5-28t11.5-29q12-12 29-12t28 12l42 43q11 12 11 28t-11 28q-11 12-27.5 11.5T226-678Zm494 495-42-43q-11-12-11-28.5t11-27.5q11-12 27.5-11.5T734-282l43 42q12 11 11.5 28T777-183q-12 12-29 12t-28-12Zm-42-495q-12-11-11.5-27.5T678-734l42-43q11-12 28-11.5t29 11.5q12 12 12 29t-12 28l-43 42q-12 11-28 11t-28-11ZM183-183q-12-12-12-29t12-28l43-42q12-11 28.5-11t27.5 11q12 11 11.5 27.5T282-226l-42 43q-11 12-28 11.5T183-183Zm297-297Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-120q-151 0-255.5-104.5T120-480q0-138 90-239.5T440-838q13-2 23 3.5t16 14.5q6 9 6.5 21t-7.5 23q-17 26-25.5 55t-8.5 61q0 90 63 153t153 63q31 0 61.5-9t54.5-25q11-7 22.5-6.5T819-479q10 5 15.5 15t3.5 24q-14 138-117.5 229T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm40-83q119-15 199.5-104.5T800-480q0-123-80.5-212.5T520-797v634Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M440-440H240q-17 0-28.5-11.5T200-480q0-17 11.5-28.5T240-520h200v-200q0-17 11.5-28.5T480-760q17 0 28.5 11.5T520-720v200h200q17 0 28.5 11.5T760-480q0 17-11.5 28.5T720-440H520v200q0 17-11.5 28.5T480-200q-17 0-28.5-11.5T440-240v-200Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M440-440H240q-17 0-28.5-11.5T200-480q0-17 11.5-28.5T240-520h200v-200q0-17 11.5-28.5T480-760q17 0 28.5 11.5T520-720v200h200q17 0 28.5 11.5T760-480q0 17-11.5 28.5T720-440H520v200q0 17-11.5 28.5T480-200q-17 0-28.5-11.5T440-240v-200Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M400-160 232-88q-40 17-76-6.5T120-161v-519q0-33 23.5-56.5T200-760h400q33 0 56.5 23.5T680-680v519q0 43-36 66.5T568-88l-168-72Zm0-88 200 86v-518H200v518l200-86Zm400 88q-17 0-28.5-11.5T760-200v-640H280q-17 0-28.5-11.5T240-880q0-17 11.5-28.5T280-920h480q33 0 56.5 23.5T840-840v640q0 17-11.5 28.5T800-160ZM400-680H200h400-200Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M400-160 232-88q-40 17-76-6.5T120-161v-519q0-33 23.5-56.5T200-760h400q33 0 56.5 23.5T680-680v519q0 43-36 66.5T568-88l-168-72Zm0-88 200 86v-518H200v518l200-86Zm400 88q-17 0-28.5-11.5T760-200v-640H280q-17 0-28.5-11.5T240-880q0-17 11.5-28.5T280-920h480q33 0 56.5 23.5T840-840v640q0 17-11.5 28.5T800-160ZM400-680H200h400-200Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h360q17 0 28.5 11.5T880-680q0 17-11.5 28.5T840-640H447l-80-80H160v480l79-263q8-26 29.5-41.5T316-560h516q41 0 64.5 32.5T909-457l-72 240q-8 26-29.5 41.5T760-160H160Zm84-80h516l72-240H316l-72 240Zm-84-262v-218 218Zm84 262 72-240-72 240Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h360q17 0 28.5 11.5T880-680q0 17-11.5 28.5T840-640H447l-80-80H160v480l79-263q8-26 29.5-41.5T316-560h516q41 0 64.5 32.5T909-457l-72 240q-8 26-29.5 41.5T760-160H160Zm84-80h516l72-240H316l-72 240Zm-84-262v-218 218Zm84 262 72-240-72 240Z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" height="128" view
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-240v-480 480Zm24.62 40q-27.62 0-46.12-18.5Q120-237 120-264.62v-430.76q0-27.62 18.5-46.12Q157-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h311.53q27.62 0 46.12 18.5Q840-643 840-615.38v67.69q0 8.54-5.73 14.27T820-527.69q-8.54 0-14.27-5.73T800-547.69v-67.69q0-9.24-7.69-16.93-7.69-7.69-16.93-7.69H447.77l-80-80H184.62q-10.77 0-17.7 6.92-6.92 6.93-6.92 17.7v430.76q0 9.24 7.69 16.93 7.69 7.69 16.93 7.69h269.23q8.53 0 14.27 5.73 5.73 5.73 5.73 14.27t-5.73 14.27q-5.74 5.73-14.27 5.73H184.62Zm384.61 47.69v-42.92q0-6.46 2.23-12.04 2.23-5.58 7.46-10.81l203.62-202.61q5.92-5.93 12.31-8 6.38-2.08 12.77-2.08 6.61 0 13.38 2.58 6.77 2.57 11.92 7.73l37 37.77q4.93 5.92 7.5 12.31Q880-364 880-357.62q0 6.39-2.46 12.89-2.46 6.5-7.62 12.42L667.31-129.69q-5.23 5.23-10.81 7.46-5.58 2.23-12.04 2.23h-42.92q-13.92 0-23.12-9.19-9.19-9.19-9.19-23.12Zm275.39-205.31-37-37.76 37 37.76Zm-240 202.24h38l138.69-138.93-18.77-19-18.23-19.54-139.69 139.47v38Zm157.92-157.93-18.23-19.54 37 38.54-18.77-19Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M160-240v-480 480Zm24.62 40q-27.62 0-46.12-18.5Q120-237 120-264.62v-430.76q0-27.62 18.5-46.12Q157-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h311.53q27.62 0 46.12 18.5Q840-643 840-615.38v67.69q0 8.54-5.73 14.27T820-527.69q-8.54 0-14.27-5.73T800-547.69v-67.69q0-9.24-7.69-16.93-7.69-7.69-16.93-7.69H447.77l-80-80H184.62q-10.77 0-17.7 6.92-6.92 6.93-6.92 17.7v430.76q0 9.24 7.69 16.93 7.69 7.69 16.93 7.69h269.23q8.53 0 14.27 5.73 5.73 5.73 5.73 14.27t-5.73 14.27q-5.74 5.73-14.27 5.73H184.62Zm384.61 47.69v-42.92q0-6.46 2.23-12.04 2.23-5.58 7.46-10.81l203.62-202.61q5.92-5.93 12.31-8 6.38-2.08 12.77-2.08 6.61 0 13.38 2.58 6.77 2.57 11.92 7.73l37 37.77q4.93 5.92 7.5 12.31Q880-364 880-357.62q0 6.39-2.46 12.89-2.46 6.5-7.62 12.42L667.31-129.69q-5.23 5.23-10.81 7.46-5.58 2.23-12.04 2.23h-42.92q-13.92 0-23.12-9.19-9.19-9.19-9.19-23.12Zm275.39-205.31-37-37.76 37 37.76Zm-240 202.24h38l138.69-138.93-18.77-19-18.23-19.54-139.69 139.47v38Zm157.92-157.93-18.23-19.54 37 38.54-18.77-19Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M381.54-350.77q-95.92 0-162.58-66.65-66.65-66.66-66.65-162.58 0-95.92 66.65-162.58 66.66-66.65 162.58-66.65 95.92 0 162.58 66.65 66.65 66.66 66.65 162.58 0 41.69-14.77 80.69t-38.77 66.69l236.31 236.31q5.61 5.62 6 13.77.38 8.16-6 14.54-6.39 6.38-14.16 6.38-7.76 0-14.15-6.38L528.92-404.31q-30 25.54-69 39.54t-78.38 14Zm0-40q79.61 0 134.42-54.81 54.81-54.8 54.81-134.42 0-79.62-54.81-134.42-54.81-54.81-134.42-54.81-79.62 0-134.42 54.81-54.81 54.8-54.81 134.42 0 79.62 54.81 134.42 54.8 54.81 134.42 54.81Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M381.54-350.77q-95.92 0-162.58-66.65-66.65-66.66-66.65-162.58 0-95.92 66.65-162.58 66.66-66.65 162.58-66.65 95.92 0 162.58 66.65 66.65 66.66 66.65 162.58 0 41.69-14.77 80.69t-38.77 66.69l236.31 236.31q5.61 5.62 6 13.77.38 8.16-6 14.54-6.39 6.38-14.16 6.38-7.76 0-14.15-6.38L528.92-404.31q-30 25.54-69 39.54t-78.38 14Zm0-40q79.61 0 134.42-54.81 54.81-54.8 54.81-134.42 0-79.62-54.81-134.42-54.81-54.81-134.42-54.81-79.62 0-134.42 54.81-54.81 54.8-54.81 134.42 0 79.62 54.81 134.42 54.8 54.81 134.42 54.81Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M672.31-200q-13.73 0-23.02-9.29T640-232.31V-360q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29T760-360v127.69q0 13.73-9.29 23.02T727.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T420-232.31v-495.38q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29t9.29 23.02v495.38q0 13.73-9.29 23.02T507.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T200-232.31v-302.7q0-14.3 9.29-23.49 9.29-9.19 23.02-9.19h55.38q13.73 0 23.02 9.29 9.29 9.28 9.29 23.02v302.7q0 14.3-9.29 23.49-9.29 9.19-23.02 9.19h-55.38Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M672.31-200q-13.73 0-23.02-9.29T640-232.31V-360q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29T760-360v127.69q0 13.73-9.29 23.02T727.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T420-232.31v-495.38q0-13.73 9.29-23.02t23.02-9.29h55.38q13.73 0 23.02 9.29t9.29 23.02v495.38q0 13.73-9.29 23.02T507.69-200h-55.38Zm-220 0q-13.73 0-23.02-9.29T200-232.31v-302.7q0-14.3 9.29-23.49 9.29-9.19 23.02-9.19h55.38q13.73 0 23.02 9.29 9.29 9.28 9.29 23.02v302.7q0 14.3-9.29 23.49-9.29 9.19-23.02 9.19h-55.38Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 40q-66.85 0-113.42-46.58Q320-413.15 320-480t46.58-113.42Q413.15-640 480-640t113.42 46.58Q640-546.85 640-480t-46.58 113.42Q546.85-320 480-320ZM80-460q-8.5 0-14.25-5.76T60-480.03q0-8.51 5.75-14.24T80-500h100q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T180-460H80Zm700 0q-8.5 0-14.25-5.76T760-480.03q0-8.51 5.75-14.24T780-500h100q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T880-460H780ZM479.97-760q-8.51 0-14.24-5.75T460-780v-100q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-880v100q0 8.5-5.76 14.25T479.97-760Zm0 700q-8.51 0-14.24-5.75T460-80v-100q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-180v100q0 8.5-5.76 14.25T479.97-60ZM254.46-678.77l-57.61-55.85q-5.85-5.61-5.73-13.76.11-8.16 5.96-14.77 6.38-6.62 14.15-6.62 7.77 0 14.15 6.62L282-706.31q6.38 6.62 6.38 14.16 0 7.53-6 14.15t-13.65 6.12q-7.65-.5-14.27-6.89Zm480.16 481.92L678-253.69q-6.38-6.62-6.38-14.27 0-7.66 6.38-14.04 5.23-6.62 13.08-6.12 7.84.5 14.46 6.89l57.61 55.85q5.85 5.61 5.73 13.76-.11 8.16-5.96 14.77-6.38 6.62-14.15 6.62-7.77 0-14.15-6.62ZM678-677.62q-6.62-6-6.12-13.65t6.89-14.27l55.85-57.61q5.61-5.85 13.76-5.73 8.16.11 14.77 5.96 6.62 6.38 6.62 14.15 0 7.77-6.62 14.15L706.31-678q-6.62 6.38-14.16 6.38-7.53 0-14.15-6ZM196.85-196.68q-6.62-6.78-6.62-14.55 0-7.77 6.62-14.15L253.69-282q6.62-6.38 14.27-6.38 7.66 0 14.04 6.38 5.85 5.23 5.35 13.08-.5 7.84-6.12 14.46l-55.85 57.61q-6.38 6.62-14.15 6.5-7.77-.11-14.38-6.33ZM480-480Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M482.31-160Q349-160 255.65-253.35 162.31-346.69 162.31-480q0-98.77 56.54-181.42 56.53-82.66 160.38-118.89 12.23-4.31 21.46-2.27 9.23 2.04 15.23 7.96 6 5.93 7.66 15.24 1.65 9.3-2.5 19.53-7.77 19.08-11.27 38.66t-3.5 41.19q0 106.67 74.66 181.33Q555.64-404 662.31-404q27.92 0 51.11-5.92 23.2-5.93 39.5-9.62 8.7-1.61 15.96.42 7.27 2.04 11.54 7.04 4.73 5 6.39 12.31 1.65 7.31-1.89 16.69-28.61 98-112.11 160.54Q589.31-160 482.31-160Zm0-40q88 0 158-48.5t102-126.5q-20 5-40 8t-40 3q-123 0-209.5-86.5T366.31-660q0-20 3-40t8-40q-78 32-126.5 102t-48.5 158q0 116 82 198t198 82Zm-10-270Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M480.13-120q-74.67 0-140.41-28.34-65.73-28.34-114.36-76.92-48.63-48.58-76.99-114.26Q120-405.19 120-479.87q0-74.67 28.34-140.41 28.34-65.73 76.92-114.36 48.58-48.63 114.26-76.99Q405.19-840 479.87-840q74.67 0 140.41 28.34 65.73 28.34 114.36 76.92 48.63 48.58 76.99 114.26Q840-554.81 840-480.13q0 74.67-28.34 140.41-28.34 65.73-76.92 114.36-48.58 48.63-114.26 76.99Q554.81-120 480.13-120ZM500-160.69q123.62-8.08 211.81-98.35T800-480q0-130.69-87.42-220.19-87.43-89.5-212.58-99.12v638.62Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M460-460H260q-8.5 0-14.25-5.76T240-480.03q0-8.51 5.75-14.24T260-500h200v-200q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-700v200h200q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T700-460H500v200q0 8.5-5.76 14.25T479.97-240q-8.51 0-14.24-5.75T460-260v-200Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M460-460H260q-8.5 0-14.25-5.76T240-480.03q0-8.51 5.75-14.24T260-500h200v-200q0-8.5 5.76-14.25t14.27-5.75q8.51 0 14.24 5.75T500-700v200h200q8.5 0 14.25 5.76t5.75 14.27q0 8.51-5.75 14.24T700-460H500v200q0 8.5-5.76 14.25T479.97-240q-8.51 0-14.24-5.75T460-260v-200Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="m420-203.08-149.54 64.31q-32.31 13.92-61.38-5.35Q180-163.38 180-197.92v-457.46q0-27.62 18.5-46.12Q217-720 244.62-720h350.76q27.62 0 46.12 18.5Q660-683 660-655.38v457.46q0 34.54-29.08 53.8-29.07 19.27-61.38 5.35L420-203.08Zm0-44.92 165.38 71.38q12.31 5.39 23.47-2.3 11.15-7.7 11.15-20.77v-455.69q0-9.24-7.69-16.93-7.69-7.69-16.93-7.69H244.62q-9.24 0-16.93 7.69-7.69 7.69-7.69 16.93v455.69q0 13.07 11.15 20.77 11.16 7.69 23.47 2.3L420-248Zm340 28q-8.54 0-14.27-5.73T740-240v-535.39q0-9.23-7.69-16.92-7.69-7.69-16.93-7.69H320q-8.54 0-14.27-5.73T300-820q0-8.54 5.73-14.27T320-840h395.38q27.62 0 46.12 18.5Q780-803 780-775.39V-240q0 8.54-5.73 14.27T760-220ZM420-680H220h400-200Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="m420-203.08-149.54 64.31q-32.31 13.92-61.38-5.35Q180-163.38 180-197.92v-457.46q0-27.62 18.5-46.12Q217-720 244.62-720h350.76q27.62 0 46.12 18.5Q660-683 660-655.38v457.46q0 34.54-29.08 53.8-29.07 19.27-61.38 5.35L420-203.08Zm0-44.92 165.38 71.38q12.31 5.39 23.47-2.3 11.15-7.7 11.15-20.77v-455.69q0-9.24-7.69-16.93-7.69-7.69-16.93-7.69H244.62q-9.24 0-16.93 7.69-7.69 7.69-7.69 16.93v455.69q0 13.07 11.15 20.77 11.16 7.69 23.47 2.3L420-248Zm340 28q-8.54 0-14.27-5.73T740-240v-535.39q0-9.23-7.69-16.92-7.69-7.69-16.93-7.69H320q-8.54 0-14.27-5.73T300-820q0-8.54 5.73-14.27T320-840h395.38q27.62 0 46.12 18.5Q780-803 780-775.39V-240q0 8.54-5.73 14.27T760-220ZM420-680H220h400-200Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M180-200q-25.31 0-42.65-17.35Q120-234.69 120-260v-435.38q0-25.31 19.65-44.97Q159.31-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h349.23q8.54 0 14.27 5.73t5.73 14.27q0 8.54-5.73 14.27T813.08-640H447.77l-80-80H184.62q-10.77 0-17.7 6.92-6.92 6.93-6.92 17.7V-240q0-10.77 4.23-8.08 4.23 2.7 11.15 6.54l76.7-256.84q6.46-20.62 23.73-33.43 17.27-12.81 38.65-12.81h512.92q32.54 0 52.2 25.97 19.65 25.96 10.19 57.03l-65.85 219.24q-5.69 19.07-21.8 30.73Q786-200 766.15-200H180Zm37.08-40h546q8.46 0 15-4.62 6.54-4.61 8.84-13.07l64.31-215.39q3.85-12.3-3.85-21.92-7.69-9.62-20-9.62H314.46q-8.46 0-15 4.62-6.54 4.62-8.84 13.08L217.08-240ZM160-498.15V-720v221.85ZM217.08-240l73.54-246.92q2.3-8.46 3.84-13.08l1.54-4.62-2.69 9.62q-2.69 9.62-6.54 21.92l-64.31 215.39q-2.31 8.46-3.84 13.07l-1.54 4.62Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" height="128" viewBox="0 -960 960 960" width="128" fill="#000000"><path d="M180-200q-25.31 0-42.65-17.35Q120-234.69 120-260v-435.38q0-25.31 19.65-44.97Q159.31-760 184.62-760H357q12.92 0 25.12 5.23 12.19 5.23 20.88 13.92L463.85-680h349.23q8.54 0 14.27 5.73t5.73 14.27q0 8.54-5.73 14.27T813.08-640H447.77l-80-80H184.62q-10.77 0-17.7 6.92-6.92 6.93-6.92 17.7V-240q0-10.77 4.23-8.08 4.23 2.7 11.15 6.54l76.7-256.84q6.46-20.62 23.73-33.43 17.27-12.81 38.65-12.81h512.92q32.54 0 52.2 25.97 19.65 25.96 10.19 57.03l-65.85 219.24q-5.69 19.07-21.8 30.73Q786-200 766.15-200H180Zm37.08-40h546q8.46 0 15-4.62 6.54-4.61 8.84-13.07l64.31-215.39q3.85-12.3-3.85-21.92-7.69-9.62-20-9.62H314.46q-8.46 0-15 4.62-6.54 4.62-8.84 13.08L217.08-240ZM160-498.15V-720v221.85ZM217.08-240l73.54-246.92q2.3-8.46 3.84-13.08l1.54-4.62-2.69 9.62q-2.69 9.62-6.54 21.92l-64.31 215.39q-2.31 8.46-3.84 13.07l-1.54 4.62Z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 2
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M21 15.2426V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5511 3 20.9925V9H9C9.55228 9 10 8.55228 10 8V2H20.0017C20.5531 2 21 2.45531 21 2.9918V6.75736L12.0012 15.7562L11.995 19.995L16.2414 20.0012L21 15.2426ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761ZM3 7L8 2.00318V7H3Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M21 15.2426V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5511 3 20.9925V9H9C9.55228 9 10 8.55228 10 8V2H20.0017C20.5531 2 21 2.45531 21 2.9918V6.75736L12.0012 15.7562L11.995 19.995L16.2414 20.0012L21 15.2426ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761ZM3 7L8 2.00318V7H3Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M16 2L21 7V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918C3 2.44405 3.44495 2 3.9934 2H16ZM13.529 14.4464L15.7395 16.6569L17.1537 15.2426L14.9432 13.0322C15.8492 11.4983 15.6432 9.48951 14.3252 8.17157C12.7631 6.60948 10.2305 6.60948 8.66839 8.17157C7.1063 9.73367 7.1063 12.2663 8.66839 13.8284C9.98633 15.1464 11.9951 15.3524 13.529 14.4464ZM12.911 12.4142C12.13 13.1953 10.8637 13.1953 10.0826 12.4142C9.30156 11.6332 9.30156 10.3668 10.0826 9.58579C10.8637 8.80474 12.13 8.80474 12.911 9.58579C13.6921 10.3668 13.6921 11.6332 12.911 12.4142Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M16 2L21 7V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918C3 2.44405 3.44495 2 3.9934 2H16ZM13.529 14.4464L15.7395 16.6569L17.1537 15.2426L14.9432 13.0322C15.8492 11.4983 15.6432 9.48951 14.3252 8.17157C12.7631 6.60948 10.2305 6.60948 8.66839 8.17157C7.1063 9.73367 7.1063 12.2663 8.66839 13.8284C9.98633 15.1464 11.9951 15.3524 13.529 14.4464ZM12.911 12.4142C12.13 13.1953 10.8637 13.1953 10.0826 12.4142C9.30156 11.6332 9.30156 10.3668 10.0826 9.58579C10.8637 8.80474 12.13 8.80474 12.911 9.58579C13.6921 10.3668 13.6921 11.6332 12.911 12.4142Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M3 12H7V21H3V12ZM17 8H21V21H17V8ZM10 2H14V21H10V2Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M3 12H7V21H3V12ZM17 8H21V21H17V8ZM10 2H14V21H10V2Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M11.3807 2.01886C9.91573 3.38768 9 5.3369 9 7.49999C9 11.6421 12.3579 15 16.5 15C18.6631 15 20.6123 14.0843 21.9811 12.6193C21.6613 17.8537 17.3149 22 12 22C6.47715 22 2 17.5228 2 12C2 6.68514 6.14629 2.33869 11.3807 2.01886Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M12 21.9967C6.47715 21.9967 2 17.5196 2 11.9967C2 6.47386 6.47715 1.9967 12 1.9967C17.5228 1.9967 22 6.47386 22 11.9967C22 17.5196 17.5228 21.9967 12 21.9967ZM12 19.9967V3.9967C7.58172 3.9967 4 7.57843 4 11.9967C4 16.415 7.58172 19.9967 12 19.9967Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M5 2H19C19.5523 2 20 2.44772 20 3V22.1433C20 22.4194 19.7761 22.6434 19.5 22.6434C19.4061 22.6434 19.314 22.6168 19.2344 22.5669L12 18.0313L4.76559 22.5669C4.53163 22.7136 4.22306 22.6429 4.07637 22.4089C4.02647 22.3293 4 22.2373 4 22.1433V3C4 2.44772 4.44772 2 5 2Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M5 2H19C19.5523 2 20 2.44772 20 3V22.1433C20 22.4194 19.7761 22.6434 19.5 22.6434C19.4061 22.6434 19.314 22.6168 19.2344 22.5669L12 18.0313L4.76559 22.5669C4.53163 22.7136 4.22306 22.6429 4.07637 22.4089C4.02647 22.3293 4 22.2373 4 22.1433V3C4 2.44772 4.44772 2 5 2Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M22 11V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V11H22ZM22 9H2V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V9Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M22 11V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V11H22ZM22 9H2V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5H21C21.5523 5 22 5.44772 22 6V9Z" /></svg>
@@ -56,6 +56,9 @@ icon:sb_outline = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 2
icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M21 6.75736L19 8.75736V4H10V9H5V20H19V17.2426L21 15.2426V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V6.75736ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761Z" /></svg> icon:sb_project = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M21 6.75736L19 8.75736V4H10V9H5V20H19V17.2426L21 15.2426V21.0082C21 21.556 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5501 3 20.9932V8L9.00319 2H19.9978C20.5513 2 21 2.45531 21 2.9918V6.75736ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761Z" /></svg>
icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM13.529 14.4464C11.9951 15.3524 9.98633 15.1464 8.66839 13.8284C7.1063 12.2663 7.1063 9.73367 8.66839 8.17157C10.2305 6.60948 12.7631 6.60948 14.3252 8.17157C15.6432 9.48951 15.8492 11.4983 14.9432 13.0322L17.1537 15.2426L15.7395 16.6569L13.529 14.4464ZM12.911 12.4142C13.6921 11.6332 13.6921 10.3668 12.911 9.58579C12.13 8.80474 10.8637 8.80474 10.0826 9.58579C9.30156 10.3668 9.30156 11.6332 10.0826 12.4142C10.8637 13.1953 12.13 13.1953 12.911 12.4142Z" /></svg> icon:sb_search = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM13.529 14.4464C11.9951 15.3524 9.98633 15.1464 8.66839 13.8284C7.1063 12.2663 7.1063 9.73367 8.66839 8.17157C10.2305 6.60948 12.7631 6.60948 14.3252 8.17157C15.6432 9.48951 15.8492 11.4983 14.9432 13.0322L17.1537 15.2426L15.7395 16.6569L13.529 14.4464ZM12.911 12.4142C13.6921 11.6332 13.6921 10.3668 12.911 9.58579C12.13 8.80474 10.8637 8.80474 10.0826 9.58579C9.30156 10.3668 9.30156 11.6332 10.0826 12.4142C10.8637 13.1953 12.13 13.1953 12.911 12.4142Z" /></svg>
icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M3 12H7V21H3V12ZM17 8H21V21H17V8ZM10 2H14V21H10V2Z" /></svg> icon:sb_stats = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M3 12H7V21H3V12ZM17 8H21V21H17V8ZM10 2H14V21H10V2Z" /></svg>
icon:theme_light = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M12 18C8.68629 18 6 15.3137 6 12C6 8.68629 8.68629 6 12 6C15.3137 6 18 8.68629 18 12C18 15.3137 15.3137 18 12 18ZM12 16C14.2091 16 16 14.2091 16 12C16 9.79086 14.2091 8 12 8C9.79086 8 8 9.79086 8 12C8 14.2091 9.79086 16 12 16ZM11 1H13V4H11V1ZM11 20H13V23H11V20ZM3.51472 4.92893L4.92893 3.51472L7.05025 5.63604L5.63604 7.05025L3.51472 4.92893ZM16.9497 18.364L18.364 16.9497L20.4853 19.0711L19.0711 20.4853L16.9497 18.364ZM19.0711 3.51472L20.4853 4.92893L18.364 7.05025L16.9497 5.63604L19.0711 3.51472ZM5.63604 16.9497L7.05025 18.364L4.92893 20.4853L3.51472 19.0711L5.63604 16.9497ZM23 11V13H20V11H23ZM4 11V13H1V11H4Z" /></svg>
icon:theme_dark = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M10 7C10 10.866 13.134 14 17 14C18.9584 14 20.729 13.1957 21.9995 11.8995C22 11.933 22 11.9665 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C12.0335 2 12.067 2 12.1005 2.00049C10.8043 3.27098 10 5.04157 10 7ZM4 12C4 16.4183 7.58172 20 12 20C15.0583 20 17.7158 18.2839 19.062 15.7621C18.3945 15.9187 17.7035 16 17 16C12.0294 16 8 11.9706 8 7C8 6.29648 8.08133 5.60547 8.2379 4.938C5.71611 6.28423 4 8.9417 4 12Z" /></svg>
icon:theme_auto = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M12 21.9967C6.47715 21.9967 2 17.5196 2 11.9967C2 6.47386 6.47715 1.9967 12 1.9967C17.5228 1.9967 22 6.47386 22 11.9967C22 17.5196 17.5228 21.9967 12 21.9967ZM12 19.9967C16.4183 19.9967 20 16.415 20 11.9967C20 7.57843 16.4183 3.9967 12 3.9967C7.58172 3.9967 4 7.57843 4 11.9967C4 16.415 7.58172 19.9967 12 19.9967ZM12 17.9967V5.9967C15.3137 5.9967 18 8.683 18 11.9967C18 15.3104 15.3137 17.9967 12 17.9967Z" /></svg>
icon:add = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" /></svg> icon:add = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z" /></svg>
icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M5 2H19C19.5523 2 20 2.44772 20 3V22.1433C20 22.4194 19.7761 22.6434 19.5 22.6434C19.4061 22.6434 19.314 22.6168 19.2344 22.5669L12 18.0313L4.76559 22.5669C4.53163 22.7136 4.22306 22.6429 4.07637 22.4089C4.02647 22.3293 4 22.2373 4 22.1433V3C4 2.44772 4.44772 2 5 2ZM18 4H6V19.4324L12 15.6707L18 19.4324V4Z" /></svg> icon:bookmarks = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M5 2H19C19.5523 2 20 2.44772 20 3V22.1433C20 22.4194 19.7761 22.6434 19.5 22.6434C19.4061 22.6434 19.314 22.6168 19.2344 22.5669L12 18.0313L4.76559 22.5669C4.53163 22.7136 4.22306 22.6429 4.07637 22.4089C4.02647 22.3293 4 22.2373 4 22.1433V3C4 2.44772 4.44772 2 5 2ZM18 4H6V19.4324L12 15.6707L18 19.4324V4Z" /></svg>
icon:browse = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5ZM20 11H4V19H20V11ZM20 9V7H11.5858L9.58579 5H4V9H20Z" /></svg> icon:browse = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000" height="128" width="128"><path d="M12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5ZM20 11H4V19H20V11ZM20 9V7H11.5858L9.58579 5H4V9H20Z" /></svg>
@@ -1,28 +0,0 @@
[Main]
name = Cyberpunk Night
author = Anders Lemvigh
url = https://github.com/alemvigh
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 0, 0, 0
text = 150, 150, 150
link = 77, 77, 255
headertext = 255, 255, 255
headertag = 50, 0, 180
emphasis = 0, 255, 255
dialog = 0, 255, 0
altdialog = 0, 140, 255
note = 150, 150, 150
hidden = 77, 77, 100
shortcode = 255, 255, 0
keyword = 255, 100, 255
tag = 255, 150, 10
value = 255, 150, 10
spellcheckline = 242, 72, 23
errorline = 186, 218, 4
replacetag = 0, 0, 180
modifier = 144, 142, 176
optional = 180, 180, 180
texthighlight = 255, 255, 132, 96
@@ -1,42 +0,0 @@
[Main]
name = Default Dark
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
##
# Colours:
# Background = 2a2a2a : 42, 42, 42
# Foreground = cccccc : 204, 204, 204
# Comment = 969696 : 150, 150, 150
# Red = e64250 : 230, 66, 80
# Orange = f99157 : 249, 145, 87
# Yellow = ffff84 : 255, 255, 132
# Green = 99cc99 : 153, 204, 153
# Blue = 288ed7 : 102, 153, 204
# Purple = 8c56d7 : 140, 86, 215
##
[Syntax]
background = 42, 42, 42
text = 204, 204, 204
link = 102, 153, 204
headertext = 153, 204, 153
headertag = 153, 204, 153, 160
emphasis = 249, 145, 87
dialog = 102, 153, 204
altdialog = 102, 153, 204
note = 255, 255, 216
hidden = 150, 150, 150
shortcode = 153, 204, 153
keyword = 230, 66, 80
tag = 153, 204, 153
value = 102, 153, 204
optional = 153, 204, 153
spellcheckline = 230, 66, 80
errorline = 153, 204, 153
replacetag = 153, 204, 153
modifier = 153, 204, 153
texthighlight = 255, 255, 132, 96
@@ -1,42 +0,0 @@
[Main]
name = Default Light
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
##
# Colours:
# Background = ffffff : 255, 255, 255
# Foreground = 000000 : 0, 0, 0
# Comment = 646464 : 100, 100, 100
# Red = c83232 : 200, 50, 50
# Orange = ae6e1e : 174, 110, 30
# Yellow = 969600 : 150, 150, 0
# Green = 006400 : 0, 100, 0
# Blue = 0000c8 : 0, 0, 200
# Purple = 6400ae : 100, 0, 174
##
[Syntax]
background = 255, 255, 255
text = 0, 0, 0
link = 0, 0, 200
headertext = 0, 100, 0
headertag = 0, 100, 0, 160
emphasis = 174, 110, 30
dialog = 0, 0, 200
altdialog = 0, 0, 200
note = 100, 100, 0
hidden = 100, 100, 100
shortcode = 0, 100, 0
keyword = 200, 50, 50
tag = 0, 100, 0
value = 0, 0, 200
optional = 0, 100, 0
spellcheckline = 200, 50, 50
errorline = 0, 100, 0
replacetag = 0, 100, 0
modifier = 0, 100, 0
texthighlight = 150, 150, 0, 96
-44
View File
@@ -1,44 +0,0 @@
[Main]
name = Dracula
author = Veronica Berglyd Olsen (adaptation)
credit = Zeno Rocha
url = https://draculatheme.com
license = MIT
licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE
##
# Colours:
# Background = 282a36 : 40, 42, 54
# Foreground = f8f8f2 : 248, 248, 242
# Comment = 6272a4 : 98, 114, 164
# Cyan = 8be9fd : 139, 233, 253
# Green = 50fa7b : 80, 250, 123
# Orange = ffb86c : 255, 184, 108
# Pink = ff79c6 : 255, 121, 198
# Purple = bd93f9 : 189, 147, 249
# Red = ff5555 : 255, 85, 85
# Yellow = f1fa8c : 241, 250, 140
##
[Syntax]
# See: https://spec.draculatheme.com/
background = 40, 42, 54
text = 248, 248, 242
link = 255, 121, 198
headertext = 189, 147, 249
headertag = 189, 147, 249, 160
emphasis = 255, 184, 108
dialog = 80, 250, 123
altdialog = 241, 250, 140
note = 255, 204, 233
hidden = 98, 114, 164
shortcode = 139, 233, 253
keyword = 255, 121, 198
tag = 255, 184, 108
value = 255, 184, 108
optional = 80, 250, 123
spellcheckline = 255, 85, 85
errorline = 80, 250, 123
replacetag = 241, 250, 140
modifier = 139, 233, 253
texthighlight = 241, 250, 140, 96
-29
View File
@@ -1,29 +0,0 @@
[Main]
name = Grey Dark
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 54, 54, 54
text = 200, 200, 200
link = 200, 200, 200
headertext = 225, 225, 225
headertag = 225, 225, 225, 160
emphasis = 200, 200, 200
dialog = 200, 200, 200
altdialog = 200, 200, 200
note = 200, 200, 200
hidden = 150, 150, 150
shortcode = 225, 225, 225
keyword = 225, 225, 225
tag = 200, 200, 200
value = 200, 200, 200
optional = 225, 225, 225
spellcheckline = 200, 46, 0
errorline = 46, 200, 0
replacetag = 225, 225, 225
modifier = 225, 225, 225
texthighlight = 255, 255, 255, 64
-29
View File
@@ -1,29 +0,0 @@
[Main]
name = Grey Light
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Syntax]
background = 255, 255, 255
text = 20, 20, 20
link = 20, 20, 20
headertext = 0, 0, 0
headertag = 0, 0, 0, 160
emphasis = 20, 20, 20
dialog = 20, 20, 20
altdialog = 20, 20, 20
note = 20, 20, 20
hidden = 100, 100, 100
shortcode = 0, 0, 0
keyword = 0, 0, 0
tag = 20, 20, 20
value = 20, 20, 20
optional = 0, 0, 0
spellcheckline = 200, 0, 0
errorline = 0, 150, 0
replacetag = 0, 0, 0
modifier = 0, 0, 0
texthighlight = 0, 0, 0, 64
-49
View File
@@ -1,49 +0,0 @@
##
# Theme: Light Owl
# Source: https://github.com/sdras/night-owl-vscode-theme
# Credit: Sarah Drasner
##
# Colours:
# Background = fbfbfb : 251, 251, 251
# Current Line = e0e0e0 : 224, 224, 224
# Selection = e0e0e0 : 224, 224, 224
# Foreground = 403f52 : 64, 63, 82
# Comment = 989fb1 : 152, 159, 177
# Red = de3d3a : 222, 61, 58
# Orange = e0af05 : 224, 175, 5
# Yellow = daaa01 : 218, 170, 1
# Green = 08916a : 8, 145, 106
# Aqua = 2aa298 : 42, 162, 152
# Blue = 288ed7 : 40, 142, 215
# Purple = 964ac1 : 150, 74, 193
##
[Main]
name = Light Owl
author = Veronica Berglyd Olsen (adaptation)
credit = Sarah Drasner (color theme)
url = https://github.com/sdras/night-owl-vscode-theme
license = MIT License
licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE
[Syntax]
background = 251, 251, 251
text = 64, 63, 82
link = 40, 142, 215
headertext = 40, 142, 215
headertag = 40, 142, 215, 160
emphasis = 224, 175, 5
dialog = 8, 145, 106
altdialog = 218, 170, 1
note = 128, 118, 75
hidden = 152, 159, 177
shortcode = 40, 142, 215
keyword = 222, 61, 58
tag = 150, 74, 193
value = 224, 175, 5
optional = 40, 142, 215
spellcheckline = 222, 61, 58
errorline = 8, 145, 106
replacetag = 42, 162, 152
modifier = 8, 145, 106
texthighlight = 218, 170, 1, 96
-49
View File
@@ -1,49 +0,0 @@
##
# Theme: Night Owl
# Source: https://github.com/sdras/night-owl-vscode-theme
# Credit: Sarah Drasner
##
# Colours:
# Background = 011627 : 1, 22, 39
# Current Line = 32374d : 50, 55, 77
# Selection = 32374d : 50, 55, 77
# Foreground = d6deeb : 214, 222, 235
# Comment = 637777 : 99, 119, 119
# Red = f78c6c : 247, 140, 108
# Orange = ecc48d : 236, 196, 141
# Yellow = ffeb95 : 255, 235, 149
# Green = addb67 : 173, 219, 103
# Aqua = 7fdbca : 127, 219, 202
# Blue = 82aaff : 130, 170, 255
# Purple = c792ea : 199, 146, 234
##
[Main]
name = Night Owl
author = Veronica Berglyd Olsen (adaptation)
credit = Sarah Drasner (color theme)
url = https://github.com/sdras/night-owl-vscode-theme
license = MIT License
licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE
[Syntax]
background = 1, 22, 39
text = 214, 222, 235
link = 130, 170, 255
headertext = 130, 170, 255
headertag = 130, 170, 255, 160
emphasis = 236, 196, 141
dialog = 173, 219, 103
altdialog = 255, 235, 149
note = 255, 249, 202
hidden = 99, 119, 119
shortcode = 130, 170, 255
keyword = 247, 140, 108
tag = 199, 146, 234
value = 255, 235, 149
optional = 130, 170, 255
spellcheckline = 247, 140, 108
errorline = 173, 219, 103
replacetag = 127, 219, 202
modifier = 173, 219, 103
texthighlight = 255, 235, 149, 96
-42
View File
@@ -1,42 +0,0 @@
[Main]
name = Snazzy Light
author = Veronica Berglyd Olsen (adaptation)
credit = Florian Reuschel (color theme)
url = https://github.com/loilo/vscode-snazzy-light
license = MIT License
licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE
##
# Colours:
# Background = fafbfc : 250, 251, 252
# Foreground = 565869 : 86, 88, 105
# Comment = 9194a2 : 145, 148, 162
# Yellow = cf9c00 : 207, 156, 0
# Red = ff5c57 : 255, 92, 87
# Pink = f767bb : 247, 103, 187
# Blue = 09a1ed : 9, 161, 237
# Cyan = 13bbb7 : 19, 187, 183
# Green = 2dae58 : 45, 174, 88
##
[Syntax]
background = 250, 251, 252
text = 86, 88, 105
link = 9, 161, 237
headertext = 45, 174, 88
headertag = 45, 174, 88, 160
emphasis = 247, 103, 187
dialog = 9, 161, 237
altdialog = 207, 156, 0
note = 120, 187, 185
hidden = 145, 148, 162
shortcode = 247, 103, 187
keyword = 9, 161, 237
tag = 45, 174, 88
value = 207, 156, 0
optional = 207, 156, 0
spellcheckline = 255, 92, 87
errorline = 45, 174, 88
replacetag = 19, 187, 183
modifier = 247, 103, 187
texthighlight = 207, 156, 0, 96
@@ -1,29 +0,0 @@
[Main]
name = Solarized Dark
author = nullbasis
credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/
license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
[Syntax]
background = 7, 54, 66
text = 253, 246, 227
link = 38, 139, 210
headertext = 147, 161, 161
headertag = 42, 161, 152
emphasis = 38, 139, 210
dialog = 42, 161, 152
altdialog = 42, 161, 152
note = 101, 161, 156
hidden = 147, 161, 161
shortcode = 147, 161, 161
keyword = 133, 153, 0
tag = 203, 75, 22
value = 203, 75, 22
optional = 147, 161, 161
spellcheckline = 203, 75, 22
errorline = 220, 50, 47
replacetag = 133, 153, 0
modifier = 181, 137, 0
texthighlight = 181, 137, 0, 96
@@ -1,29 +0,0 @@
[Main]
name = Solarized Light
author = nullbasis
credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/
license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
[Syntax]
background = 253, 246, 227
text = 0, 43, 54
link = 38, 139, 210
headertext = 88, 110, 117
headertag = 42, 161, 152
emphasis = 38, 139, 210
dialog = 42, 161, 152
altdialog = 42, 161, 152
note = 27, 102, 96
hidden = 88, 110, 117
shortcode = 88, 110, 117
keyword = 133, 153, 0
tag = 203, 75, 22
value = 203, 75, 22
optional = 88, 110, 117
spellcheckline = 203, 75, 22
errorline = 220, 50, 47
replacetag = 133, 153, 0
modifier = 181, 137, 0
texthighlight = 181, 137, 0, 96
-39
View File
@@ -1,39 +0,0 @@
[Main]
name = Tango
author = Veronica Berglyd Olsen (adaptation)
##
# Colours:
# Background = 30302f : 48, 48, 47
# Foreground = eeeeec : 238, 238, 236
# Comment = 8f8f8d : 143, 143, 141
# Red = ef2929 : 239, 41, 41
# Orange = c4a000 : 196, 160, 0
# Yellow = fce94f : 252, 233, 79
# Green = 8ae234 : 138, 226, 52
# Cyan = 34e2e2 : 52, 226, 226
# Blue = 3465a4 : 114, 159, 207
# Purple = ad7fa8 : 173, 127, 168
##
[Syntax]
background = 48, 48, 47
text = 238, 238, 236
link = 114, 159, 207
headertext = 114, 159, 207
headertag = 114, 159, 207, 160
emphasis = 196, 160, 0
dialog = 138, 226, 52
altdialog = 252, 233, 79
note = 252, 242, 164
hidden = 143, 143, 141
shortcode = 114, 159, 207
keyword = 239, 41, 41
tag = 173, 127, 168
value = 239, 41, 41
optional = 114, 159, 207
spellcheckline = 239, 41, 41
errorline = 138, 226, 52
replacetag = 52, 226, 226
modifier = 114, 159, 207
texthighlight = 252, 233, 79, 96
-49
View File
@@ -1,49 +0,0 @@
##
# Theme: Tomorrow
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = ffffff : 255, 255, 255
# Current Line = efefef : 239, 239, 239
# Selection = d6d6d6 : 214, 214, 214
# Foreground = 4d4d4c : 77, 77, 76
# Comment = 8e908c : 142, 144, 140
# Red = c82829 : 240, 40, 41
# Orange = f5871f : 245, 135, 31
# Yellow = eab700 : 234, 183, 0
# Green = 718c00 : 113, 140, 0
# Aqua = 3e999f : 62, 153, 159
# Blue = 4271ae : 66, 113, 174
# Purple = 8959a8 : 137, 89, 168
##
[Main]
name = Tomorrow
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 255, 255, 255
text = 77, 77, 76
link = 66, 113, 174
headertext = 66, 113, 174
headertag = 66, 113, 174, 160
emphasis = 245, 135, 31
dialog = 113, 140, 0
altdialog = 234, 183, 0
note = 115, 90, 0
hidden = 142, 144, 140
shortcode = 66, 113, 174
keyword = 240, 40, 41
tag = 137, 89, 168
value = 234, 183, 0
optional = 66, 113, 174
spellcheckline = 240, 40, 41
errorline = 113, 140, 0
replacetag = 62, 153, 159
modifier = 245, 135, 31
texthighlight = 234, 183, 0, 96
@@ -1,49 +0,0 @@
##
# Theme: Tomorrow Night
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = 1d1f21 : 29, 31, 33
# Current Line = 282a2e : 40, 42, 46
# Selection = 373b41 : 55, 59. 65
# Foreground = c5c8c6 : 197, 200, 198
# Comment = 969896 : 150, 152, 150
# Red = cc6666 : 204, 102, 102
# Orange = de935f : 222, 147, 95
# Yellow = f0c674 : 240, 198, 116
# Green = b5bd68 : 181, 189, 104
# Aqua = 8abeb7 : 138, 190, 183
# Blue = 81a2be : 129, 162, 190
# Purple = b294bb : 178, 148, 187
##
[Main]
name = Tomorrow Night
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 29, 31, 33
text = 197, 200, 198
link = 197, 200, 198, 160
headertext = 129, 162, 190
headertag = 94, 119, 139
emphasis = 222, 147, 95
dialog = 181, 189, 104
altdialog = 240, 198, 116
note = 240, 219, 178
hidden = 150, 152, 150
shortcode = 129, 162, 190
keyword = 204, 102, 102
tag = 178, 148, 187
value = 240, 198, 116
optional = 129, 162, 190
spellcheckline = 204, 102, 102
errorline = 181, 189, 104
replacetag = 138, 190, 183
modifier = 222, 147, 95
texthighlight = 240, 198, 116, 96
@@ -1,49 +0,0 @@
##
# Theme: Tomorrow Night Blue
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = 002451 : 0, 36, 81
# Current Line = 00346e : 0, 52, 110
# Selection = 003f8e : 0, 63, 142
# Foreground = ffffff : 255, 255, 255
# Comment = 7285b7 : 114, 133, 183
# Red = ff9da4 : 255, 157, 164
# Orange = ffc58f : 255, 197, 143
# Yellow = ffeead : 255, 238, 173
# Green = d1f1a9 : 209, 241, 169
# Aqua = 99ffff : 153, 255, 255
# Blue = bbdaff : 187, 218, 255
# Purple = ebbbff : 235, 187, 255
##
[Main]
name = Tomorrow Night Blue
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 0, 36, 81
text = 255, 255, 255
link = 187, 218, 255
headertext = 187, 218, 255
headertag = 187, 218, 255, 160
emphasis = 255, 197, 143
dialog = 209, 241, 169
altdialog = 255, 238, 173
note = 255, 247, 214
hidden = 114, 133, 183
shortcode = 187, 218, 255
keyword = 255, 157, 164
tag = 235, 187, 255
value = 255, 238, 173
optional = 187, 218, 255
spellcheckline = 255, 157, 164
errorline = 209, 241, 169
replacetag = 153, 255, 255
modifier = 255, 197, 143
texthighlight = 255, 238, 173, 96
@@ -1,49 +0,0 @@
##
# Theme: Tomorrow Night Bright
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = 000000 : 0, 0, 0
# Current Line = 2a2a2a : 42, 42, 42
# Selection = 424242 : 66, 66, 66
# Foreground = eaeaea : 234, 234, 234
# Comment = 969896 : 150, 152, 150
# Red = d54e53 : 213, 78, 83
# Orange = e78c45 : 231, 140, 69
# Yellow = e7c547 : 231, 197, 71
# Green = b9ca4a : 185, 202, 74
# Aqua = 70c0b1 : 112, 192, 177
# Blue = 7aa6da : 122, 166, 218
# Purple = c397d8 : 195, 151, 216
##
[Main]
name = Tomorrow Night Bright
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 0, 0, 0
text = 234, 234, 234
link = 122, 166, 218
headertext = 122, 166, 218
headertag = 122, 166, 218, 160
emphasis = 231, 140, 69
dialog = 185, 202, 74
altdialog = 231, 197, 71
note = 231, 214, 150
hidden = 150, 152, 150
shortcode = 122, 166, 218
keyword = 213, 78, 83
tag = 195, 151, 216
value = 231, 197, 71
optional = 122, 166, 218
spellcheckline = 213, 78, 83
errorline = 185, 202, 74
replacetag = 112, 192, 177
modifier = 231, 140, 69
texthighlight = 231, 197, 71, 128
@@ -1,49 +0,0 @@
##
# Theme: Tomorrow Night Eighties
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = 2d2d2d : 45, 45, 45
# Current Line = 393939 : 57, 57, 57
# Selection = 515151 : 81, 81, 81
# Foreground = cccccc : 204, 204, 204
# Comment = 999999 : 153, 153, 153
# Red = f2777a : 242, 119, 122
# Orange = f99157 : 249, 145, 57
# Yellow = ffcc66 : 255, 204, 102
# Green = 99cc99 : 153, 204, 153
# Aqua = 66cccc : 102, 204, 204
# Blue = 6699cc : 102, 153, 204
# Purple = cc99cc : 204, 153, 204
##
[Main]
name = Tomorrow Night Eighties
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
[Syntax]
background = 45, 45, 45
text = 204, 204, 204
link = 102, 153, 204
headertext = 102, 153, 204
headertag = 102, 153, 204, 160
emphasis = 249, 145, 57
dialog = 153, 204, 153
altdialog = 255, 204, 102
note = 255, 230, 179
hidden = 153, 153, 153
shortcode = 102, 153, 204
keyword = 242, 119, 122
tag = 204, 153, 204
value = 255, 204, 102
optional = 102, 153, 204
spellcheckline = 242, 119, 122
errorline = 153, 204, 153
replacetag = 102, 204, 204
modifier = 249, 145, 57
texthighlight = 255, 204, 102, 96
+2 -2
View File
@@ -75,8 +75,8 @@ more contributions are listed on the project's Members page.</p>
<li><b>Material Symbols</b> icons by Google Inc (Apache 2.0)</li> <li><b>Material Symbols</b> icons by Google Inc (Apache 2.0)</li>
<li><b>Remix</b> icons by RemixIcon (Apache 2.0)</li> <li><b>Remix</b> icons by RemixIcon (Apache 2.0)</li>
<li><b>Font Awesome</b> icons by Fonticons Inc (CC BY 4.0)</li> <li><b>Font Awesome</b> icons by Fonticons Inc (CC BY 4.0)</li>
<li><b>Tomorrow</b> syntax themes by Chris Kempson (MIT License)</li> <li><b>Tomorrow</b> themes by Chris Kempson (MIT License)</li>
<li><b>Owl</b> syntax themes by Sarah Drasner (MIT License)</li> <li><b>Owl</b> themes by Sarah Drasner (MIT License)</li>
<li><b>Solarized</b> themes by Ethan Schoonover (MIT License)</li> <li><b>Solarized</b> themes by Ethan Schoonover (MIT License)</li>
<li><b>Cyberpunk Night</b> theme by Anders Lemvigh (CC BY-SA 4.0)</li> <li><b>Cyberpunk Night</b> theme by Anders Lemvigh (CC BY-SA 4.0)</li>
<li><b>Dracula</b> theme by Zeno Rocha (MIT License)</li> <li><b>Dracula</b> theme by Zeno Rocha (MIT License)</li>
+57 -34
View File
@@ -1,48 +1,71 @@
[Main] [Main]
name = Cyberpunk Night name = Cyberpunk Night
mode = dark
description = A taste of the future 80s description = A taste of the future 80s
author = Anders Lemvigh author = Anders Lemvigh
url = https://github.com/alemvigh url = https://github.com/alemvigh
license = CC BY-SA 4.0 license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Icons] [Base]
default = 136, 136, 136 default = #888888
faded = 97, 97, 97 faded = #616161
red = 242, 72, 23 red = #f24817
orange = 255, 150, 10 orange = #ff960a
yellow = 255, 255, 0 yellow = #ffff00
green = 0, 255, 0 green = #00ff00
aqua = 0, 255, 255 cyan = #00ffff
blue = 77, 77, 255 blue = #4d4dff
purple = 50, 0, 180 purple = #320064
[Project] [Project]
root = 77, 77, 255 root = #4d4dff
folder = 255, 255, 0 folder = #ffff00
file = 136, 136, 136 file = #888888
title = 0, 255, 0 title = #00ff00
chapter = 242, 72, 23 chapter = #f24817
scene = 77, 77, 255 scene = #4d4dff
note = 255, 255, 0 note = #ffff00
[Palette] [Palette]
window = 0, 0, 0 window = #000000
windowtext = 150, 150, 150 windowtext = #969696
base = 0, 0, 0 base = #000000
alternatebase = 40, 40, 40 alternatebase = #282828
text = 150, 150, 150 text = #969696
tooltipbase = 40, 20, 70 tooltipbase = #281446
tooltiptext = 255, 255, 255 tooltiptext = #ffffff
button = 5, 0, 10 button = #05000a
buttontext = 150, 150, 150 buttontext = #969696
brighttext = 255, 255, 255 brighttext = #ffffff
highlight = 50, 30, 80 highlight = #321e50
highlightedtext = 255, 255, 255 highlightedtext = #ffffff
link = 77, 77, 255 link = #4d4dff
linkvisited = 50, 0, 80 linkvisited = #320050
[GUI] [GUI]
helptext = 97, 97, 97 helptext = #616161
fadedtext = 97, 97, 97 fadedtext = #616161
errortext = 242, 72, 23 errortext = #f24817
[Syntax]
background = #000000
text = #969696
link = #4d4dff
headertext = #ffffff
headertag = #320064
emphasis = #00ffff
dialog = #00ff00
altdialog = #008cff
note = #969696
hidden = #4d4d64
shortcode = #ffff00
keyword = #ff64ff
tag = #ff960a
value = #ff960a
spellcheckline = #f24817
errorline = #bada04
replacetag = #0000b4
modifier = #908eb0
optional = #b4b4b4
texthighlight = #ffff8460
-3
View File
@@ -1,3 +0,0 @@
[Main]
name = Qt Default Theme
description = Qt standard colours
+57 -34
View File
@@ -1,5 +1,6 @@
[Main] [Main]
name = Default Dark Theme name = Default Dark Theme
mode = dark
description = The novelWriter standard dark theme description = The novelWriter standard dark theme
author = Veronica Berglyd Olsen author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen credit = Veronica Berglyd Olsen
@@ -7,43 +8,65 @@ url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0 license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Icons] [Base]
default = 184, 184, 184 default = #cccccc
faded = 148, 148, 148 faded = #949494
red = 242, 119, 122 red = #f2777a
orange = 249, 145, 57 orange = #f99139
yellow = 255, 204, 102 yellow = #ffcc66
green = 153, 204, 153 green = #99cc99
aqua = 102, 204, 204 cyan = #66cccc
blue = 102, 153, 204 blue = #6699cc
purple = 204, 153, 204 purple = #cc99cc
[Project] [Project]
root = 102, 153, 204 root = blue
folder = 255, 204, 102 folder = yellow
file = 184, 184, 184 file = default
title = 153, 204, 153 title = green
chapter = 242, 119, 122 chapter = red
scene = 102, 153, 204 scene = blue
note = 255, 204, 102 note = yellow
[Palette] [Palette]
window = 54, 54, 54 window = #363636
windowtext = 204, 204, 204 windowtext = default
base = 62, 62, 62 base = #3e3e3e
alternatebase = 78, 78, 78 alternatebase = #4e4e4e
text = 204, 204, 204 text = default
tooltipbase = 255, 255, 192 tooltipbase = #ffffc0
tooltiptext = 21, 21, 13 tooltiptext = #15150d
button = 62, 62, 62 button = #3e3e3e
buttontext = 204, 204, 204 buttontext = default
brighttext = 62, 62, 62 brighttext = #3e3e3e
highlight = 44, 152, 247 highlight = #2c98f7
highlightedtext = 255, 255, 255 highlightedtext = #ffffff
link = 102, 153, 204 link = blue
linkvisited = 102, 153, 204 linkvisited = blue
[GUI] [GUI]
helptext = 164, 164, 164 helptext = #a4a4a4
fadedtext = 148, 148, 148 fadedtext = faded
errortext = 255, 164, 164 errortext = red
[Syntax]
background = #363636
text = default
link = blue
headertext = green
headertag = green:160
emphasis = orange
dialog = blue
altdialog = blue
note = yellow
hidden = faded
shortcode = green
keyword = red
tag = green
value = blue
optional = green
spellcheckline = red
errorline = green
replacetag = green
modifier = green
texthighlight = yellow:72
+57 -34
View File
@@ -1,5 +1,6 @@
[Main] [Main]
name = Default Light Theme name = Default Light Theme
mode = light
description = The novelWriter standard light theme description = The novelWriter standard light theme
author = Veronica Berglyd Olsen author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen credit = Veronica Berglyd Olsen
@@ -7,43 +8,65 @@ url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0 license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Icons] [Base]
default = 72, 72, 72 default = #303030
faded = 108, 108, 108 faded = #6c6c6c
red = 240, 40, 41 red = #a62a2d
orange = 245, 135, 31 orange = #b36829
yellow = 234, 183, 0 yellow = #a68542
green = 113, 140, 0 green = #296629
aqua = 62, 153, 159 cyan = #269999
blue = 66, 113, 174 blue = #3a70a6
purple = 137, 89, 168 purple = #b35ab3
[Project] [Project]
root = 66, 113, 174 root = blue
folder = 234, 183, 0 folder = yellow
file = 72, 72, 72 file = default
title = 113, 140, 0 title = green
chapter = 240, 40, 41 chapter = red
scene = 66, 113, 174 scene = blue
note = 234, 183, 0 note = yellow
[Palette] [Palette]
window = 239, 239, 239 window = #efefef
windowtext = 0, 0, 0 windowtext = #000000
base = 255, 255, 255 base = #ffffff
alternatebase = 224, 224, 224 alternatebase = #e0e0e0
text = 0, 0, 0 text = #000000
tooltipbase = 255, 255, 220 tooltipbase = #ffffc0
tooltiptext = 0, 0, 0 tooltiptext = #15150d
button = 239, 239, 239 button = #efefef
buttontext = 0, 0, 0 buttontext = #000000
brighttext = 255, 255, 255 brighttext = #ffffff
highlight = 48, 135, 198 highlight = #3087c6
highlightedtext = 255, 255, 255 highlightedtext = #ffffff
link = 66, 113, 174 link = blue
linkvisited = 66, 113, 174 linkvisited = blue
[GUI] [GUI]
helptext = 92, 92, 92 helptext = #5c5c5c
fadedtext = 108, 108, 108 fadedtext = #6c6c6c
errortext = 255, 92, 92 errortext = red
[Syntax]
background = #ffffff
text = #000000
link = blue
headertext = green
headertag = green:160
emphasis = orange
dialog = blue
altdialog = blue
note = yellow
hidden = faded
shortcode = green
keyword = red
tag = green
value = blue
optional = green
spellcheckline = red
errorline = green
replacetag = green
modifier = green
texthighlight = #c8c80060
+59 -35
View File
@@ -1,5 +1,6 @@
[Main] [Main]
name = Dracula name = Dracula
mode = dark
description = A dark theme with bright colours description = A dark theme with bright colours
author = Veronica Berglyd Olsen (adaptation) author = Veronica Berglyd Olsen (adaptation)
credit = Zeno Rocha credit = Zeno Rocha
@@ -9,9 +10,10 @@ licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE
## ##
# Colours: # Colours:
# See: https://spec.draculatheme.com/
# Background = 282a36 : 40, 42, 54 # Background = 282a36 : 40, 42, 54
# Foreground = f8f8f2 : 248, 248, 242 # Foreground = f8f8f2 : 248, 248, 242
# Current Line = 44475A : 68, 71, 90 # Current Line = 44475a : 68, 71, 90
# Comment = 6272a4 : 98, 114, 164 # Comment = 6272a4 : 98, 114, 164
# Cyan = 8be9fd : 139, 233, 253 # Cyan = 8be9fd : 139, 233, 253
# Green = 50fa7b : 80, 250, 123 # Green = 50fa7b : 80, 250, 123
@@ -22,43 +24,65 @@ licenseurl = https://github.com/dracula/dracula-theme/blob/main/LICENSE
# Yellow = f1fa8c : 241, 250, 140 # Yellow = f1fa8c : 241, 250, 140
## ##
[Icons] [Base]
default = 230, 230, 224 default = #e6e6e0
faded = 98, 114, 164 faded = #6272a4
red = 255, 85, 85 red = #ff5555
orange = 255, 184, 108 orange = #ffb86c
yellow = 241, 250, 140 yellow = #f1fa8c
green = 80, 250, 123 green = #50fa7b
aqua = 139, 233, 253 cyan = #8be9fd
blue = 147, 207, 249 blue = #93cff9
purple = 189, 147, 249 purple = #bd93f9
[Project] [Project]
root = 189, 147, 249 root = #bd93f9
folder = 241, 250, 140 folder = #f1fa8c
file = 230, 230, 224 file = #e6e6e0
title = 80, 250, 123 title = #50fa7b
chapter = 255, 85, 85 chapter = #ff5555
scene = 139, 233, 253 scene = #8be9fd
note = 241, 250, 140 note = #f1fa8c
[Palette] [Palette]
window = 68, 71, 90 window = #44475a
windowtext = 248, 248, 242 windowtext = #f8f8f2
base = 40, 42, 54 base = #282a36
alternatebase = 51, 54, 69 alternatebase = #333645
text = 248, 248, 242 text = #f8f8f2
tooltipbase = 241, 250, 140 tooltipbase = #f1fa8c
tooltiptext = 40, 42, 54 tooltiptext = #282a36
button = 80, 83, 105 button = #505369
buttontext = 248, 248, 242 buttontext = #f8f8f2
brighttext = 68, 71, 90 brighttext = #44475a
highlight = 166, 129, 218 highlight = #a681da
highlightedtext = 248, 248, 242 highlightedtext = #f8f8f2
link = 139, 233, 253 link = #8be9fd
linkvisited = 139, 233, 253 linkvisited = #8be9fd
[GUI] [GUI]
helptext = 204, 172, 249 helptext = #ccacf9
fadedtext = 98, 114, 164 fadedtext = #6272a4
errortext = 255, 85, 85 errortext = #ff5555
[Syntax]
background = #282a36
text = #f8f8f2
link = #ff79c6
headertext = #bd93f9
headertag = #bd93f9a0
emphasis = #ffb86c
dialog = #50fa7b
altdialog = #f1fa8c
note = #ffcce9
hidden = #6272a4
shortcode = #8be9fd
keyword = #ff79c6
tag = #ffb86c
value = #ffb86c
optional = #50fa7b
spellcheckline = #ff5555
errorline = #50fa7b
replacetag = #f1fa8c
modifier = #8be9fd
texthighlight = #f1fa8c60
+71
View File
@@ -0,0 +1,71 @@
[Main]
name = Grey Dark
mode = dark
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Base]
default = #cccccc
faded = #949494
red = #f2777a
orange = #f99139
yellow = #ffcc66
green = #99cc99
cyan = #66cccc
blue = #6699cc
purple = #cc99cc
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #363636
windowtext = default
base = #3e3e3e
alternatebase = #4e4e4e
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #3e3e3e
buttontext = default
brighttext = #3e3e3e
highlight = #2c98f7
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #a4a4a4
fadedtext = faded
errortext = red
[Syntax]
background = #363636
text = #c8c8c8
link = #c8c8c8
headertext = #e1e1e1
headertag = #e1e1e1a0
emphasis = #c8c8c8
dialog = #c8c8c8
altdialog = #c8c8c8
note = #c8c8c8
hidden = faded
shortcode = #e1e1e1
keyword = #e1e1e1
tag = #c8c8c8
value = #c8c8c8
optional = #e1e1e1
spellcheckline = red
errorline = green
replacetag = #e1e1e1
modifier = #e1e1e1
texthighlight = #ffffff40
+71
View File
@@ -0,0 +1,71 @@
[Main]
name = Grey Light
mode = light
author = Veronica Berglyd Olsen
credit = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter
license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Base]
default = #303030
faded = #6c6c6c
red = #a62a2d
orange = #b36829
yellow = #a68542
green = #296629
cyan = #269999
blue = #3a70a6
purple = #b35ab3
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #efefef
windowtext = default
base = #ffffff
alternatebase = #e0e0e0
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #efefef
buttontext = default
brighttext = #ffffff
highlight = #3087c6
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #5c5c5c
fadedtext = faded
errortext = red
[Syntax]
background = #ffffff
text = #141414
link = #141414
headertext = #000000
headertag = #000000a0
emphasis = #141414
dialog = #141414
altdialog = #141414
note = #141414
hidden = faded
shortcode = #000000
keyword = #000000
tag = #141414
value = #141414
optional = #000000
spellcheckline = red
errorline = green
replacetag = #000000
modifier = #000000
texthighlight = #00000040
+91
View File
@@ -0,0 +1,91 @@
[Main]
name = Light Owl
mode = light
author = Veronica Berglyd Olsen (adaptation)
credit = Sarah Drasner (color theme)
url = https://github.com/sdras/night-owl-vscode-theme
license = MIT License
licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE
##
# Theme: Light Owl
# Source: https://github.com/sdras/night-owl-vscode-theme
# Credit: Sarah Drasner
##
# Colours:
# Background = #fbfbfb
# Current Line = #e0e0e0
# Selection = #e0e0e0
# Foreground = #403f52
# Comment = #989fb1
# Red = #de3d3a
# Orange = #e0af05
# Yellow = #daaa01
# Green = #08916a
# Aqua = #2aa298
# Blue = #288ed7
# Purple = #964ac1
##
[Base]
default = #403f52
faded = #989fb1
red = #de3d3a
orange = #e0af05
yellow = #daaa01
green = #08916a
cyan = #2aa298
blue = #288ed7
purple = #964ac1
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #e0e0e0
windowtext = default
base = #fbfbfb
alternatebase = #e0e0e0
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #e0e0e0
buttontext = default
brighttext = #e0e0e0
highlight = #3087c6
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = blue
fadedtext = faded
errortext = red
[Syntax]
background = #fbfbfb
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #80764b
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = orange
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = green
texthighlight = yellow:96
+91
View File
@@ -0,0 +1,91 @@
[Main]
name = Night Owl
mode = dark
author = Veronica Berglyd Olsen (adaptation)
credit = Sarah Drasner (color theme)
url = https://github.com/sdras/night-owl-vscode-theme
license = MIT License
licenseurl = https://github.com/sdras/night-owl-vscode-theme/blob/master/LICENSE
##
# Theme: Night Owl
# Source: https://github.com/sdras/night-owl-vscode-theme
# Credit: Sarah Drasner
##
# Colours:
# Background = #011627
# Current Line = #32374d
# Selection = #32374d
# Foreground = #d6deeb
# Comment = #637777
# Red = #f78c6c
# Orange = #ecc48d
# Yellow = #ffeb95
# Green = #addb67
# Aqua = #7fdbca
# Blue = #82aaff
# Purple = #c792ea
##
[Base]
default = #d6deeb
faded = #637777
red = #f78c6c
orange = #ecc48d
yellow = #addb67
green = #addb67
cyan = #7fdbca
blue = #82aaff
purple = #c792ea
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #32374d
windowtext = default
base = #011627
alternatebase = #32374d
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #32374d
buttontext = default
brighttext = #32374d
highlight = #2c98f7
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = blue
fadedtext = faded
errortext = red
[Syntax]
background = #011627
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #fff9ca
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = orange
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = green
texthighlight = yellow:96
+70 -34
View File
@@ -1,48 +1,84 @@
[Main] [Main]
name = Snazzy Light name = Snazzy Light
mode = light
author = Veronica Berglyd Olsen (adaptation) author = Veronica Berglyd Olsen (adaptation)
credit = Florian Reuschel (color theme) credit = Florian Reuschel (color theme)
url = https://github.com/loilo/vscode-snazzy-light url = https://github.com/loilo/vscode-snazzy-light
license = MIT License license = MIT License
licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE licenseurl = https://github.com/loilo/vscode-snazzy-light/blob/master/LICENSE
[Icons] ##
default = 86, 88, 105 # Colours:
faded = 84, 85, 84 # Background = fafbfc : 250, 251, 252
red = 255, 92, 87 # Foreground = 565869 : 86, 88, 105
orange = 245, 185, 0 # Comment = 9194a2 : 145, 148, 162
yellow = 207, 156, 0 # Yellow = cf9c00 : 207, 156, 0
green = 45, 174, 88 # Red = ff5c57 : 255, 92, 87
aqua = 19, 187, 183 # Pink = f767bb : 247, 103, 187
blue = 9, 161, 237 # Blue = 09a1ed : 9, 161, 237
purple = 247, 103, 187 # Cyan = 13bbb7 : 19, 187, 183
# Green = 2dae58 : 45, 174, 88
##
[Base]
default = #565869
faded = #545554
red = #ff5c57
orange = #f5b900
yellow = #cf9c00
green = #2dae58
cyan = #13bbb7
blue = #09a1ed
purple = #f767bb
[Project] [Project]
root = 9, 161, 237 root = #09a1ed
folder = 207, 156, 0 folder = #cf9c00
file = 84, 85, 84 file = #545554
title = 45, 174, 88 title = #2dae58
chapter = 255, 92, 87 chapter = #ff5c57
scene = 9, 161, 237 scene = #09a1ed
note = 207, 156, 0 note = #cf9c00
[Palette] [Palette]
window = 243, 244, 245 window = #f3f4f5
windowtext = 86, 88, 105 windowtext = #565869
base = 250, 251, 252 base = #fafbfc
alternatebase = 234, 234, 235 alternatebase = #eaeaeb
text = 86, 88, 105 text = #565869
tooltipbase = 245, 233, 194 tooltipbase = #f5e9c2
tooltiptext = 86, 88, 105 tooltiptext = #565869
button = 250, 251, 252 button = #fafbfc
buttontext = 86, 88, 105 buttontext = #565869
brighttext = 255, 255, 255 brighttext = #ffffff
highlight = 9, 161, 237 highlight = #09a1ed
highlightedtext = 255, 255, 255 highlightedtext = #ffffff
link = 9, 161, 237 link = #09a1ed
linkvisited = 9, 161, 237 linkvisited = #09a1ed
[GUI] [GUI]
helptext = 9, 161, 237 helptext = #09a1ed
fadedtext = 84, 85, 84 fadedtext = #545554
errortext = 255, 92, 87 errortext = #ff5c57
[Syntax]
background = #fafbfc
text = #565869
link = #09a1ed
headertext = #2dae58
headertag = #2dae58a0
emphasis = #f767bb
dialog = #09a1ed
altdialog = #cf9c00
note = #78bbb9
hidden = #9194a2
shortcode = #f767bb
keyword = #09a1ed
tag = #2dae58
value = #cf9c00
optional = #cf9c00
spellcheckline = #ff5c57
errorline = #2dae58
replacetag = #13bbb7
modifier = #f767bb
texthighlight = #cf9c0060
+76 -34
View File
@@ -1,48 +1,90 @@
[Main] [Main]
name = Solarized Dark name = Solarized Dark
mode = dark
author = nullbasis author = nullbasis
credit = Ethan Schoonover credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/ url = https://ethanschoonover.com/solarized/
license = MIT license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
[Icons] ##
default = 230, 223, 206 # base03: #002b36
faded = 166, 161, 149 # base02: #073642
red = 220, 50, 47 # base01: #586e75
orange = 203, 75, 22 # base00: #657b83
yellow = 181, 137, 0 # base0: #839496
green = 133, 153, 0 # base1: #93a1a1
aqua = 42, 161, 152 # base2: #eee8d5
blue = 38, 139, 210 # base3: #fdf6e3
purple = 211, 54, 130 # yellow: #b58900
# orange: #cb4b16
# red: #dc322f
# magenta: #d33682
# violet: #6c71c4
# blue: #268bd2
# cyan: #2aa198
# green: #859900
##
[Base]
default = #e6dfce
faded = #a6a195
red = #dc322f
orange = #cb4b16
yellow = #b58900
green = #859900
cyan = #2aa198
blue = #268bd2
purple = #6c71c4
[Project] [Project]
root = 42, 161, 152 root = #2aa198
folder = 42, 161, 152 folder = #2aa198
file = 230, 223, 206 file = #e6dfce
title = 133, 153, 0 title = #859900
chapter = 220, 50, 47 chapter = #dc322f
scene = 38, 139, 210 scene = #268bd2
note = 181, 137, 0 note = #b58900
[Palette] [Palette]
window = 0, 43, 54 window = #002b36
windowtext = 253, 246, 227 windowtext = #fdf6e3
base = 7, 54, 66 base = #073642
alternatebase = 88, 110, 117 alternatebase = #586e75
text = 253, 246, 227 text = #fdf6e3
tooltipbase = 133, 153, 0 tooltipbase = #859900
tooltiptext = 0, 43, 54 tooltiptext = #002b36
button = 7, 54, 66 button = #073642
buttontext = 253, 246, 227 buttontext = #fdf6e3
brighttext = 7, 54, 66 brighttext = #073642
highlight = 42, 161, 152 highlight = #2aa198
highlightedtext = 0, 43, 54 highlightedtext = #002b36
link = 38, 139, 210 link = #268bd2
linkvisited = 38, 139, 210 linkvisited = #268bd2
[GUI] [GUI]
helptext = 101, 123, 131 helptext = #657b83
fadedtext = 101, 123, 131 fadedtext = #657b83
errortext = 220, 50, 47 errortext = #dc322f
[Syntax]
background = #073642
text = #fdf6e3
link = #268bd2
headertext = #93a1a1
headertag = #2aa198
emphasis = #268bd2
dialog = #2aa198
altdialog = #2aa198
note = #65a19c
hidden = #93a1a1
shortcode = #93a1a1
keyword = #859900
tag = #cb4b16
value = #cb4b16
optional = #93a1a1
spellcheckline = #cb4b16
errorline = #dc322f
replacetag = #859900
modifier = #b58900
texthighlight = #b5890060
+76 -34
View File
@@ -1,48 +1,90 @@
[Main] [Main]
name = Solarized Light name = Solarized Light
mode = light
author = nullbasis author = nullbasis
credit = Ethan Schoonover credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/ url = https://ethanschoonover.com/solarized/
license = MIT license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
[Icons] ##
default = 59, 68, 71 # base03: #002b36
faded = 78, 91, 95 # base02: #073642
red = 220, 50, 47 # base01: #586e75
orange = 203, 75, 22 # base00: #657b83
yellow = 181, 137, 0 # base0: #839496
green = 133, 153, 0 # base1: #93a1a1
aqua = 42, 161, 152 # base2: #eee8d5
blue = 38, 139, 210 # base3: #fdf6e3
purple = 211, 54, 130 # yellow: #b58900
# orange: #cb4b16
# red: #dc322f
# magenta: #d33682
# violet: #6c71c4
# blue: #268bd2
# cyan: #2aa198
# green: #859900
##
[Base]
default = #3b4447
faded = #4e5b5f
red = #dc322f
orange = #cb4b16
yellow = #b58900
green = #859900
cyan = #2aa198
blue = #268bd2
purple = #6c71c4
[Project] [Project]
root = 42, 161, 152 root = #2aa198
folder = 42, 161, 152 folder = #2aa198
file = 59, 68, 71 file = #3b4447
title = 133, 153, 0 title = #859900
chapter = 220, 50, 47 chapter = #dc322f
scene = 38, 139, 210 scene = #268bd2
note = 181, 137, 0 note = #b58900
[Palette] [Palette]
window = 238, 232, 213 window = #eee8d5
windowtext = 0, 43, 54 windowtext = #002b36
base = 253, 246, 227 base = #fdf6e3
alternatebase = 147, 161, 161 alternatebase = #93a1a1
text = 0, 43, 54 text = #002b36
tooltipbase = 133, 153, 0 tooltipbase = #859900
tooltiptext = 0, 43, 54 tooltiptext = #002b36
button = 238, 232, 213 button = #eee8d5
buttontext = 0, 43, 54 buttontext = #002b36
brighttext = 253, 246, 227 brighttext = #fdf6e3
highlight = 42, 161, 152 highlight = #2aa198
highlightedtext = 253, 246, 227 highlightedtext = #fdf6e3
link = 38, 139, 210 link = #268bd2
linkvisited = 38, 139, 210 linkvisited = #268bd2
[GUI] [GUI]
helptext = 78, 91, 95 helptext = #4e5b5f
fadedtext = 78, 91, 95 fadedtext = #4e5b5f
errortext = 220, 50, 47 errortext = #dc322f
[Syntax]
background = #fdf6e3
text = #002b36
link = #268bd2
headertext = #586e75
headertag = #2aa198
emphasis = #268bd2
dialog = #2aa198
altdialog = #2aa198
note = #1b6660
hidden = #586e75
shortcode = #586e75
keyword = #859900
tag = #cb4b16
value = #cb4b16
optional = #586e75
spellcheckline = #cb4b16
errorline = #dc322f
replacetag = #859900
modifier = #b58900
texthighlight = #b5890060
+81
View File
@@ -0,0 +1,81 @@
[Main]
name = Tango Dark
mode = dark
author = Veronica Berglyd Olsen (adaptation)
##
# Colours:
# Background = 30302f : 48, 48, 47
# Foreground = eeeeec : 238, 238, 236
# Comment = 8f8f8d : 143, 143, 141
# Red = ef2929 : 239, 41, 41
# Orange = c4a000 : 196, 160, 0
# Yellow = fce94f : 252, 233, 79
# Green = 8ae234 : 138, 226, 52
# Cyan = 34e2e2 : 52, 226, 226
# Blue = 3465a4 : 114, 159, 207
# Purple = ad7fa8 : 173, 127, 168
##
[Base]
default = #eeeeec
faded = #babdb6
red = #ef2929
orange = #fcaf3e
yellow = #fce94f
green = #8ae234
cyan = #34e2e2
blue = #729fcf
purple = #ad7fa8
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #555753
windowtext = default
base = #2e3436
alternatebase = #888a85
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #555753
buttontext = default
brighttext = #555753
highlight = #2c98f7
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #e9b96e
fadedtext = faded
errortext = red
[Syntax]
background = #2e3436
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #fcf2a4
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = red
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = blue
texthighlight = yellow:96
@@ -0,0 +1,81 @@
[Main]
name = Tango Light
mode = light
author = Veronica Berglyd Olsen (adaptation)
##
# Colours:
# Background = #eeeeec
# Foreground = #2e3436
# Comment = 8f8f8d : 143, 143, 141
# Red = #a40000
# Orange = c4a000 : 196, 160, 0
# Yellow = fce94f : 252, 233, 79
# Green = 8ae234 : 138, 226, 52
# Cyan = 34e2e2 : 52, 226, 226
# Blue = 3465a4 : 114, 159, 207
# Purple = ad7fa8 : 173, 127, 168
##
[Base]
default = #2e3436
faded = #888a85
red = #a40000
orange = #ce5c00
yellow = #c4a000
green = #4e9a06
cyan = #069a9a
blue = #204a87
purple = #5c3566
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #d3d7cf
windowtext = default
base = #eeeeec
alternatebase = #e0e0e0
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #d3d7cf
buttontext = default
brighttext = #d3d7cf
highlight = #3087c6
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #8f5902
fadedtext = faded
errortext = red
[Syntax]
background = #eeeeec
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #a78800
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = red
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = blue
texthighlight = yellow:96
+91
View File
@@ -0,0 +1,91 @@
[Main]
name = Tomorrow
mode = light
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
##
# Theme: Tomorrow
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = #ffffff
# Current Line = #efefef
# Selection = #d6d6d6
# Foreground = #4d4d4c
# Comment = #8e908c
# Red = #c82829
# Orange = #f5871f
# Yellow = #eab700
# Green = #718c00
# Aqua = #3e999f
# Blue = #4271ae
# Purple = #8959a8
##
[Base]
default = #484848
faded = #6c6c6c
red = #c82829
orange = #f5871f
yellow = #eab700
green = #718c00
cyan = #3e999f
blue = #4271ae
purple = #8959a8
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #efefef
windowtext = #000000
base = #ffffff
alternatebase = #d6d6d6
text = #000000
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #efefef
buttontext = #000000
brighttext = #efefef
highlight = blue
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #5c5c5c
fadedtext = faded
errortext = red
[Syntax]
background = #ffffff
text = #4d4d4c
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #b38c00
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = yellow
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = orange
texthighlight = yellow:96
@@ -0,0 +1,91 @@
[Main]
name = Tomorrow Night
mode = dark
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
##
# Theme: Tomorrow Night
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = #1d1f21
# Current Line = #282a2e
# Selection = #373b41
# Foreground = #c5c8c6
# Comment = #969896
# Red = #cc6666
# Orange = #de935f
# Yellow = #f0c674
# Green = #b5bd68
# Aqua = #8abeb7
# Blue = #81a2be
# Purple = #b294bb
##
[Base]
default = #c5c8c6
faded = #969896
red = #cc6666
orange = #de935f
yellow = #f0c674
green = #b5bd68
cyan = #8abeb7
blue = #81a2be
purple = #b294bb
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #282a2e
windowtext = default
base = #1d1f21
alternatebase = #373b41
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #1d1f21
buttontext = default
brighttext = #1d1f21
highlight = #2c98f7
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #a4a4a4
fadedtext = #949494
errortext = red
[Syntax]
background = #1d1f21
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #f0dbb2
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = yellow
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = orange
texthighlight = yellow:96
@@ -0,0 +1,91 @@
[Main]
name = Tomorrow Night Blue
mode = dark
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
##
# Theme: Tomorrow Night Blue
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = #002451
# Current Line = #00346e
# Selection = #003f8e
# Foreground = #ffffff
# Comment = #7285b7
# Red = #ff9da4
# Orange = #ffc58f
# Yellow = #ffeead
# Green = #d1f1a9
# Aqua = #99ffff
# Blue = #bbdaff
# Purple = #ebbbff
##
[Base]
default = #ffffff
faded = #7285b7
red = #ff9da4
orange = #ffc58f
yellow = #ffeead
green = #d1f1a9
cyan = #99ffff
blue = #bbdaff
purple = #ebbbff
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #00346e
windowtext = default
base = #002451
alternatebase = #003f8e
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #002451
buttontext = default
brighttext = #002451
highlight = #2c98f7
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #a4a4a4
fadedtext = #949494
errortext = red
[Syntax]
background = #002451
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #fff4cc
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = yellow
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = orange
texthighlight = yellow:96
@@ -0,0 +1,91 @@
[Main]
name = Tomorrow Night Bright
mode = dark
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
##
# Theme: Tomorrow Night Bright
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = #000000
# Current Line = #2a2a2a
# Selection = #424242
# Foreground = #eaeaea
# Comment = #969896
# Red = #d54e53
# Orange = #e78c45
# Yellow = #e7c547
# Green = #b9ca4a
# Aqua = #70c0b1
# Blue = #7aa6da
# Purple = #c397d8
##
[Base]
default = #eaeaea
faded = #969896
red = #d54e53
orange = #e78c45
yellow = #e7c547
green = #b9ca4a
cyan = #70c0b1
blue = #7aa6da
purple = #c397d8
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #2a2a2a
windowtext = default
base = #000000
alternatebase = #424242
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #000000
buttontext = default
brighttext = #000000
highlight = #2c98f7
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #a4a4a4
fadedtext = #949494
errortext = red
[Syntax]
background = #000000
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #e7d696
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = yellow
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = orange
texthighlight = yellow:96
@@ -0,0 +1,91 @@
[Main]
name = Tomorrow Night Eighties
mode = dark
author = Veronica Berglyd Olsen (adaptation)
credit = Chris Kempson (color theme)
url = https://github.com/chriskempson/tomorrow-theme
license = MIT License
licenseurl = https://github.com/chriskempson/tomorrow-theme/blob/master/LICENSE.md
##
# Theme: Tomorrow Night Eighties
# Source: https://github.com/chriskempson/tomorrow-theme
# Credit: Chris Kempson
##
# Colours:
# Background = #2d2d2d
# Current Line = #393939
# Selection = #515151
# Foreground = #cccccc
# Comment = #999999
# Red = #f2777a
# Orange = #f99157
# Yellow = #ffcc66
# Green = #99cc99
# Aqua = #66cccc
# Blue = #6699cc
# Purple = #cc99cc
##
[Base]
default = #cccccc
faded = #999999
red = #f2777a
orange = #f99157
yellow = #ffcc66
green = #99cc99
cyan = #66cccc
blue = #6699cc
purple = #cc99cc
[Project]
root = blue
folder = yellow
file = default
title = green
chapter = red
scene = blue
note = yellow
[Palette]
window = #393939
windowtext = default
base = #2d2d2d
alternatebase = #515151
text = default
tooltipbase = #ffffc0
tooltiptext = #15150d
button = #2d2d2d
buttontext = default
brighttext = #2d2d2d
highlight = #2c98f7
highlightedtext = #ffffff
link = blue
linkvisited = blue
[GUI]
helptext = #a4a4a4
fadedtext = #949494
errortext = red
[Syntax]
background = #2d2d2d
text = default
link = blue
headertext = blue
headertag = blue:160
emphasis = orange
dialog = green
altdialog = yellow
note = #ffe6b3
hidden = faded
shortcode = blue
keyword = red
tag = purple
value = yellow
optional = blue
spellcheckline = red
errorline = green
replacetag = cyan
modifier = orange
texthighlight = yellow:96
+11
View File
@@ -31,6 +31,7 @@ import xml.etree.ElementTree as ET
from configparser import ConfigParser from configparser import ConfigParser
from datetime import datetime from datetime import datetime
from enum import Enum
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, TypeGuard, TypeVar from typing import TYPE_CHECKING, Any, Literal, TypeGuard, TypeVar
from urllib.parse import urljoin from urllib.parse import urljoin
@@ -676,6 +677,9 @@ def openExternalPath(path: Path) -> bool:
# Classes # Classes
## ##
_T_Enum = TypeVar("_T_Enum", bound=Enum)
class NWConfigParser(ConfigParser): class NWConfigParser(ConfigParser):
"""Common: Adapted Config Parser """Common: Adapted Config Parser
@@ -736,3 +740,10 @@ class NWConfigParser(ConfigParser):
for i in range(min(len(data), len(result))): for i in range(min(len(data), len(result))):
result[i] = checkInt(data[i].strip(), result[i]) result[i] = checkInt(data[i].strip(), result[i])
return result return result
def rdEnum(self, section: str, option: str, default: _T_Enum) -> _T_Enum:
"""Read enum value."""
if self.has_option(section, option):
data = self.get(section, option, fallback="")
return type(default).__members__.get(data.upper(), default)
return default
+31 -28
View File
@@ -45,6 +45,7 @@ from novelwriter.common import (
formatTimeStamp, processDialogSymbols, simplified formatTimeStamp, processDialogSymbols, simplified
) )
from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode from novelwriter.constants import nwFiles, nwHtmlUnicode, nwQuotes, nwUnicode
from novelwriter.enum import nwTheme
from novelwriter.error import formatException, logException from novelwriter.error import formatException, logException
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -55,8 +56,8 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEF_GUI = "default" DEF_GUI_DARK = "default_dark"
DEF_SYNTAX = "default_light" DEF_GUI_LIGHT = "default_light"
DEF_ICONS = "material_rounded_normal" DEF_ICONS = "material_rounded_normal"
DEF_TREECOL = "theme" DEF_TREECOL = "theme"
@@ -69,22 +70,22 @@ class Config:
"_manuals", "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths", "_manuals", "_nwLangPath", "_qLocale", "_qtLangPath", "_qtTrans", "_recentPaths",
"_recentProjects", "_splash", "allowOpenDial", "altDialogClose", "altDialogOpen", "_recentProjects", "_splash", "allowOpenDial", "altDialogClose", "altDialogOpen",
"appHandle", "appName", "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj", "appHandle", "appName", "askBeforeBackup", "askBeforeExit", "autoSaveDoc", "autoSaveProj",
"autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "dialogLine", "autoScroll", "autoScrollPos", "autoSelect", "backupOnClose", "cursorWidth", "darkTheme",
"dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash", "dialogLine", "dialogStyle", "doJustify", "doReplace", "doReplaceDQuote", "doReplaceDash",
"doReplaceDots", "doReplaceSQuote", "emphLabels", "fmtApostrophe", "fmtDQuoteClose", "doReplaceDots", "doReplaceSQuote", "emphLabels", "fmtApostrophe", "fmtDQuoteClose",
"fmtDQuoteOpen", "fmtPadAfter", "fmtPadBefore", "fmtPadThin", "fmtSQuoteClose", "fmtDQuoteOpen", "fmtPadAfter", "fmtPadBefore", "fmtPadThin", "fmtSQuoteClose",
"fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "guiSyntax", "guiTheme", "fmtSQuoteOpen", "focusWidth", "guiFont", "guiLocale", "hasEnchant", "hideFocusFooter",
"hasEnchant", "hideFocusFooter", "hideHScroll", "hideVScroll", "highlightEmph", "hostName", "hideHScroll", "hideVScroll", "highlightEmph", "hostName", "iconColDocs", "iconColTree",
"iconColDocs", "iconColTree", "iconTheme", "incNotesWCount", "isDebug", "kernelVer", "iconTheme", "incNotesWCount", "isDebug", "kernelVer", "lastNotes", "lightTheme",
"lastNotes", "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", "narratorDialog", "mainPanePos", "mainWinSize", "memInfo", "narratorBreak", "narratorDialog", "nativeFont",
"nativeFont", "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", "outlinePanePos", "osDarwin", "osLinux", "osType", "osUnknown", "osWindows", "outlinePanePos",
"prefsWinSize", "scrollPastEnd", "searchCase", "searchLoop", "searchMatchCap", "prefsWinSize", "scrollPastEnd", "searchCase", "searchLoop", "searchMatchCap",
"searchNextFile", "searchProjCase", "searchProjRegEx", "searchProjWord", "searchRegEx", "searchNextFile", "searchProjCase", "searchProjRegEx", "searchProjWord", "searchRegEx",
"searchWord", "showEditToolBar", "showFullPath", "showLineEndings", "showMultiSpaces", "searchWord", "showEditToolBar", "showFullPath", "showLineEndings", "showMultiSpaces",
"showSessionTime", "showTabsNSpaces", "showViewerPanel", "spellLanguage", "stopWhenIdle", "showSessionTime", "showTabsNSpaces", "showViewerPanel", "spellLanguage", "stopWhenIdle",
"tabWidth", "textFont", "textMargin", "textWidth", "useCharCount", "userIdleTime", "tabWidth", "textFont", "textMargin", "textWidth", "themeMode", "useCharCount",
"verPyQtString", "verPyQtValue", "verPyString", "verQtString", "verQtValue", "userIdleTime", "verPyQtString", "verPyQtValue", "verPyString", "verQtString",
"viewComments", "viewPanePos", "viewSynopsis", "welcomeWinSize", "verQtValue", "viewComments", "viewPanePos", "viewSynopsis", "welcomeWinSize",
) )
LANG_NW = 1 LANG_NW = 1
@@ -153,14 +154,15 @@ class Config:
# General GUI Settings # General GUI Settings
self.guiLocale = self._qLocale.name() self.guiLocale = self._qLocale.name()
self.guiTheme = DEF_GUI # GUI theme self.lightTheme = DEF_GUI_LIGHT # Light GUI theme
self.guiSyntax = DEF_SYNTAX # Syntax theme self.darkTheme = DEF_GUI_DARK # Dark GUI theme
self.guiFont = QFont() # Main GUI font self.themeMode = nwTheme.AUTO # Colour theme mode
self.hideVScroll = False # Hide vertical scroll bars on main widgets self.guiFont = QFont() # Main GUI font
self.hideHScroll = False # Hide horizontal scroll bars on main widgets self.hideVScroll = False # Hide vertical scroll bars on main widgets
self.lastNotes = "0x0" # The latest release notes that have been shown self.hideHScroll = False # Hide horizontal scroll bars on main widgets
self.nativeFont = True # Use native font dialog self.lastNotes = "0x0" # The latest release notes that have been shown
self.useCharCount = False # Use character count as primary count self.nativeFont = True # Use native font dialog
self.useCharCount = False # Use character count as primary count
# Icons # Icons
self.iconTheme = DEF_ICONS # Icons theme self.iconTheme = DEF_ICONS # Icons theme
@@ -403,7 +405,7 @@ class Config:
else: else:
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont) font = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont)
self.guiFont = fontMatcher(font) self.guiFont = fontMatcher(font)
logger.debug("GUI font set to: %s", describeFont(font)) logger.debug("Main font set to: %s", describeFont(font))
QApplication.setFont(self.guiFont) QApplication.setFont(self.guiFont)
return return
@@ -551,11 +553,10 @@ class Config:
self._confPath.mkdir(exist_ok=True) self._confPath.mkdir(exist_ok=True)
self._dataPath.mkdir(exist_ok=True) self._dataPath.mkdir(exist_ok=True)
# Also create the syntax, themes and icons folders if possible # Also create the themes and icons folders if possible
if self._dataPath.is_dir(): if self._dataPath.is_dir():
(self._dataPath / "cache").mkdir(exist_ok=True) (self._dataPath / "cache").mkdir(exist_ok=True)
(self._dataPath / "icons").mkdir(exist_ok=True) (self._dataPath / "icons").mkdir(exist_ok=True)
(self._dataPath / "syntax").mkdir(exist_ok=True)
(self._dataPath / "themes").mkdir(exist_ok=True) (self._dataPath / "themes").mkdir(exist_ok=True)
self._recentPaths.loadCache() self._recentPaths.loadCache()
@@ -626,8 +627,9 @@ class Config:
# Main # Main
sec = "Main" sec = "Main"
self.setGuiFont(conf.rdStr(sec, "font", "")) self.setGuiFont(conf.rdStr(sec, "font", ""))
self.guiTheme = conf.rdStr(sec, "theme", self.guiTheme) self.lightTheme = conf.rdStr(sec, "lighttheme", self.lightTheme)
self.guiSyntax = conf.rdStr(sec, "syntax", self.guiSyntax) self.darkTheme = conf.rdStr(sec, "darktheme", self.darkTheme)
self.themeMode = conf.rdEnum(sec, "thememode", self.themeMode)
self.iconTheme = conf.rdStr(sec, "icons", self.iconTheme) self.iconTheme = conf.rdStr(sec, "icons", self.iconTheme)
self.iconColTree = conf.rdStr(sec, "iconcoltree", self.iconColTree) self.iconColTree = conf.rdStr(sec, "iconcoltree", self.iconColTree)
self.iconColDocs = conf.rdBool(sec, "iconcoldocs", self.iconColDocs) self.iconColDocs = conf.rdBool(sec, "iconcoldocs", self.iconColDocs)
@@ -721,7 +723,7 @@ class Config:
# Check Values # Check Values
# ============ # ============
self._prepareFont(self.guiFont, "GUI") self._prepareFont(self.guiFont, "main")
self._prepareFont(self.textFont, "document") self._prepareFont(self.textFont, "document")
# If we're using straight quotes, disable auto-replace # If we're using straight quotes, disable auto-replace
@@ -751,8 +753,9 @@ class Config:
conf["Main"] = { conf["Main"] = {
"font": self.guiFont.toString(), "font": self.guiFont.toString(),
"theme": str(self.guiTheme), "lighttheme": str(self.lightTheme),
"syntax": str(self.guiSyntax), "darktheme": str(self.darkTheme),
"thememode": self.themeMode.name,
"icons": str(self.iconTheme), "icons": str(self.iconTheme),
"iconcoltree": str(self.iconColTree), "iconcoltree": str(self.iconColTree),
"iconcoldocs": str(self.iconColDocs), "iconcoldocs": str(self.iconColDocs),
+13 -2
View File
@@ -28,7 +28,8 @@ from typing import Final
from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
from novelwriter.enum import ( from novelwriter.enum import (
nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape,
nwTheme
) )
@@ -449,10 +450,20 @@ class nwLabels:
"orange": QT_TRANSLATE_NOOP("Constant", "Orange"), "orange": QT_TRANSLATE_NOOP("Constant", "Orange"),
"yellow": QT_TRANSLATE_NOOP("Constant", "Yellow"), "yellow": QT_TRANSLATE_NOOP("Constant", "Yellow"),
"green": QT_TRANSLATE_NOOP("Constant", "Green"), "green": QT_TRANSLATE_NOOP("Constant", "Green"),
"aqua": QT_TRANSLATE_NOOP("Constant", "Aqua"), "cyan": QT_TRANSLATE_NOOP("Constant", "Cyan"),
"blue": QT_TRANSLATE_NOOP("Constant", "Blue"), "blue": QT_TRANSLATE_NOOP("Constant", "Blue"),
"purple": QT_TRANSLATE_NOOP("Constant", "Purple"), "purple": QT_TRANSLATE_NOOP("Constant", "Purple"),
} }
THEME_MODE_ICON: Final[dict[nwTheme, str]] = {
nwTheme.AUTO: "theme_auto",
nwTheme.LIGHT: "theme_light",
nwTheme.DARK: "theme_dark",
}
THEME_MODE_LABEL: Final[dict[nwTheme, str]] = {
nwTheme.AUTO: QT_TRANSLATE_NOOP("Constant", "System Theme"),
nwTheme.LIGHT: QT_TRANSLATE_NOOP("Constant", "Light Theme"),
nwTheme.DARK: QT_TRANSLATE_NOOP("Constant", "Dark Theme"),
}
class nwHeadFmt: class nwHeadFmt:
+29 -29
View File
@@ -35,7 +35,7 @@ from PyQt6.QtWidgets import (
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact from novelwriter.common import compact, describeFont, processDialogSymbols, uniqueCompact
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX, DEF_TREECOL from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS, DEF_TREECOL
from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm
@@ -165,22 +165,34 @@ class GuiPreferences(NDialog):
) )
# Colour Theme # Colour Theme
self.guiTheme = NComboBox(self) self.lightTheme = NComboBox(self)
self.guiTheme.setMinimumWidth(200) self.lightTheme.setMinimumWidth(200)
for theme, name in SHARED.theme.listThemes(): self.darkTheme = NComboBox(self)
self.guiTheme.addItem(name, theme) self.darkTheme.setMinimumWidth(200)
self.guiTheme.setCurrentData(CONFIG.guiTheme, DEF_GUI) for key, theme in SHARED.theme.colourThemes.items():
if theme.dark:
self.darkTheme.addItem(theme.name, key)
else:
self.lightTheme.addItem(theme.name, key)
self.lightTheme.setCurrentData(CONFIG.lightTheme, DEF_GUI_LIGHT)
self.darkTheme.setCurrentData(CONFIG.darkTheme, DEF_GUI_DARK)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Colour theme"), self.guiTheme, self.tr("Light colour theme"), self.lightTheme,
self.tr("User interface colour theme."), stretch=(3, 2)
)
self.mainForm.addRow(
self.tr("Dark colour theme"), self.darkTheme,
self.tr("User interface colour theme."), stretch=(3, 2) self.tr("User interface colour theme."), stretch=(3, 2)
) )
# Icon Theme # Icon Theme
self.iconTheme = NComboBox(self) self.iconTheme = NComboBox(self)
self.iconTheme.setMinimumWidth(200) self.iconTheme.setMinimumWidth(200)
for theme, name in SHARED.theme.iconCache.listThemes(): for key, theme in SHARED.theme.iconCache.iconThemes.items():
self.iconTheme.addItem(name, theme) self.iconTheme.addItem(theme.name, key)
self.iconTheme.setCurrentData(CONFIG.iconTheme, DEF_ICONS) self.iconTheme.setCurrentData(CONFIG.iconTheme, DEF_ICONS)
self.mainForm.addRow( self.mainForm.addRow(
@@ -242,18 +254,6 @@ class GuiPreferences(NDialog):
self.sidebar.addButton(title, section) self.sidebar.addButton(title, section)
self.mainForm.addGroupLabel(title, section) self.mainForm.addGroupLabel(title, section)
# Document Colour Theme
self.guiSyntax = NComboBox(self)
self.guiSyntax.setMinimumWidth(200)
for syntax, name in SHARED.theme.listSyntax():
self.guiSyntax.addItem(name, syntax)
self.guiSyntax.setCurrentData(CONFIG.guiSyntax, DEF_SYNTAX)
self.mainForm.addRow(
self.tr("Document colour theme"), self.guiSyntax,
self.tr("Colour theme for the editor and viewer."), stretch=(3, 2)
)
# Document Font Family # Document Font Family
self.textFont = QLineEdit(self) self.textFont = QLineEdit(self)
self.textFont.setReadOnly(True) self.textFont.setReadOnly(True)
@@ -966,18 +966,23 @@ class GuiPreferences(NDialog):
# Appearance # Appearance
guiLocale = self.guiLocale.currentData() guiLocale = self.guiLocale.currentData()
guiTheme = self.guiTheme.currentData() lightTheme = self.lightTheme.currentData()
darkTheme = self.darkTheme.currentData()
iconTheme = self.iconTheme.currentData() iconTheme = self.iconTheme.currentData()
useCharCount = self.useCharCount.isChecked() useCharCount = self.useCharCount.isChecked()
updateTheme |= CONFIG.guiTheme != guiTheme updateTheme |= CONFIG.lightTheme != lightTheme
updateTheme |= CONFIG.darkTheme != darkTheme
updateTheme |= CONFIG.iconTheme != iconTheme updateTheme |= CONFIG.iconTheme != iconTheme
needsRestart |= CONFIG.guiLocale != guiLocale needsRestart |= CONFIG.guiLocale != guiLocale
needsRestart |= CONFIG.guiFont != self._guiFont needsRestart |= CONFIG.guiFont != self._guiFont
refreshTree |= CONFIG.useCharCount != useCharCount refreshTree |= CONFIG.useCharCount != useCharCount
updateSyntax |= CONFIG.lightTheme != lightTheme
updateSyntax |= CONFIG.darkTheme != darkTheme
CONFIG.guiLocale = guiLocale CONFIG.guiLocale = guiLocale
CONFIG.guiTheme = guiTheme CONFIG.lightTheme = lightTheme
CONFIG.darkTheme = darkTheme
CONFIG.iconTheme = iconTheme CONFIG.iconTheme = iconTheme
CONFIG.hideVScroll = self.hideVScroll.isChecked() CONFIG.hideVScroll = self.hideVScroll.isChecked()
CONFIG.hideHScroll = self.hideHScroll.isChecked() CONFIG.hideHScroll = self.hideHScroll.isChecked()
@@ -986,11 +991,6 @@ class GuiPreferences(NDialog):
CONFIG.setGuiFont(self._guiFont) CONFIG.setGuiFont(self._guiFont)
# Document Style # Document Style
guiSyntax = self.guiSyntax.currentData()
updateSyntax |= CONFIG.guiSyntax != guiSyntax
CONFIG.guiSyntax = guiSyntax
CONFIG.showFullPath = self.showFullPath.isChecked() CONFIG.showFullPath = self.showFullPath.isChecked()
CONFIG.incNotesWCount = self.incNotesWCount.isChecked() CONFIG.incNotesWCount = self.incNotesWCount.isChecked()
CONFIG.setTextFont(self._textFont) CONFIG.setTextFont(self._textFont)
+7
View File
@@ -156,6 +156,13 @@ class nwFocus(Enum):
OUTLINE = 3 OUTLINE = 3
class nwTheme(Enum):
AUTO = 0
LIGHT = 1
DARK = 2
class nwOutline(Enum): class nwOutline(Enum):
TITLE = 0 TITLE = 0
+43 -6
View File
@@ -27,12 +27,13 @@ import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal, pyqtSlot
from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget
from novelwriter import SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import qtLambda from novelwriter.common import qtLambda
from novelwriter.enum import nwView from novelwriter.constants import nwLabels, trConst
from novelwriter.enum import nwTheme, nwView
from novelwriter.extensions.eventfilters import StatusTipFilter from novelwriter.extensions.eventfilters import StatusTipFilter
from novelwriter.extensions.modified import NIconToolButton from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON
@@ -77,9 +78,9 @@ class GuiSideBar(QWidget):
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View"))) self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE)) self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE))
self.tbBuild = NIconToolButton(self, iSz) self.tbTheme = NIconToolButton(self, iSz)
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) self.tbTheme.setToolTip(self.tr("Switch Colour Theme"))
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) self.tbTheme.clicked.connect(self._cycleColurTheme)
self.tbDetails = NIconToolButton(self, iSz) self.tbDetails = NIconToolButton(self, iSz)
self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details"))) self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
@@ -89,6 +90,10 @@ class GuiSideBar(QWidget):
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics"))) self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog) self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
self.tbBuild = NIconToolButton(self, iSz)
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
# Settings Menu # Settings Menu
self.tbSettings = NIconToolButton(self, iSz) self.tbSettings = NIconToolButton(self, iSz)
self.tbSettings.setToolTip(self.tr("Settings")) self.tbSettings.setToolTip(self.tr("Settings"))
@@ -111,6 +116,7 @@ class GuiSideBar(QWidget):
self.outerBox.addStretch(1) self.outerBox.addStretch(1)
self.outerBox.addWidget(self.tbDetails) self.outerBox.addWidget(self.tbDetails)
self.outerBox.addWidget(self.tbStats) self.outerBox.addWidget(self.tbStats)
self.outerBox.addWidget(self.tbTheme)
self.outerBox.addWidget(self.tbSettings) self.outerBox.addWidget(self.tbSettings)
self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.setSpacing(6) self.outerBox.setSpacing(6)
@@ -133,6 +139,7 @@ class GuiSideBar(QWidget):
self.tbBuild.setStyleSheet(buttonStyle) self.tbBuild.setStyleSheet(buttonStyle)
self.tbDetails.setStyleSheet(buttonStyle) self.tbDetails.setStyleSheet(buttonStyle)
self.tbStats.setStyleSheet(buttonStyle) self.tbStats.setStyleSheet(buttonStyle)
self.tbTheme.setStyleSheet(buttonStyle)
self.tbSettings.setStyleSheet(buttonStyle) self.tbSettings.setStyleSheet(buttonStyle)
self.tbProject.setThemeIcon("sb_project") self.tbProject.setThemeIcon("sb_project")
@@ -144,6 +151,36 @@ class GuiSideBar(QWidget):
self.tbStats.setThemeIcon("sb_stats") self.tbStats.setThemeIcon("sb_stats")
self.tbSettings.setThemeIcon("settings") self.tbSettings.setThemeIcon("settings")
self._setThemeModeIcon()
return
##
# Private Slots
##
@pyqtSlot()
def _cycleColurTheme(self) -> None:
"""Go to nex colour theme."""
match CONFIG.themeMode:
case nwTheme.AUTO:
CONFIG.themeMode = nwTheme.LIGHT
case nwTheme.LIGHT:
CONFIG.themeMode = nwTheme.DARK
case nwTheme.DARK:
CONFIG.themeMode = nwTheme.AUTO
self.mainGui.checkThemeUpdate()
self._setThemeModeIcon()
return
##
# Internal Functions
##
def _setThemeModeIcon(self) -> None:
"""Set the theme button icon."""
self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode])
self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode]))
return return
+3 -3
View File
@@ -143,9 +143,9 @@ class GuiMainStatus(QStatusBar):
self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx)) self.idlePixmap = SHARED.theme.getPixmap("timer_off", (iPx, iPx))
self.timeIcon.setPixmap(self.timePixmap) self.timeIcon.setPixmap(self.timePixmap)
colNone = SHARED.theme.getIconColor("default").darker(150) colNone = SHARED.theme.getBaseColor("default").darker(150)
colSaved = SHARED.theme.getIconColor("green").darker(150) colSaved = SHARED.theme.getBaseColor("green").darker(150)
colUnsaved = SHARED.theme.getIconColor("red").darker(150) colUnsaved = SHARED.theme.getBaseColor("red").darker(150)
self.docIcon.setColors(colNone, colSaved, colUnsaved) self.docIcon.setColors(colNone, colSaved, colUnsaved)
self.projIcon.setColors(colNone, colSaved, colUnsaved) self.projIcon.setColors(colNone, colSaved, colUnsaved)
+334 -319
View File
@@ -26,21 +26,23 @@ from __future__ import annotations
import logging import logging
from configparser import ConfigParser
from dataclasses import dataclass
from math import ceil from math import ceil
from typing import TYPE_CHECKING, Final from typing import TYPE_CHECKING, Final
from PyQt6.QtCore import QSize, Qt from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import ( from PyQt6.QtGui import (
QColor, QFont, QFontDatabase, QFontMetrics, QIcon, QPainter, QPainterPath, QColor, QFont, QFontDatabase, QFontMetrics, QGuiApplication, QIcon,
QPalette, QPixmap QPainter, QPainterPath, QPalette, QPixmap
) )
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.common import NWConfigParser, minmax from novelwriter.common import checkInt, minmax
from novelwriter.config import DEF_GUI, DEF_ICONS, DEF_SYNTAX from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent
@@ -54,9 +56,18 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton"
STYLES_BIG_TOOLBUTTON = "bigToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton"
@dataclass
class ThemeEntry:
name: str
dark: bool
path: Path
class ThemeMeta: class ThemeMeta:
name: str = "" name: str = ""
mode: str = ""
description: str = "" description: str = ""
author: str = "" author: str = ""
credit: str = "" credit: str = ""
@@ -96,21 +107,20 @@ class GuiTheme:
""" """
__slots__ = ( __slots__ = (
"_availSyntax", "_availThemes", "_guiPalette", "_styleSheets", "_syntaxList", "_themeList", "_allThemes", "_currentTheme", "_darkThemes", "_guiPalette", "_lightThemes", "_meta",
"baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "baseButtonHeight",
"fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText",
"getHeaderDecorationNarrow", "getIcon", "getIconColor", "getItemIcon", "getPixmap", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration",
"getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon",
"helpText", "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText",
"textNWidth", "themeMeta", "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth",
) )
def __init__(self) -> None: def __init__(self) -> None:
self.iconCache = GuiIcons(self) # Theme Objects
self.iconCache = GuiIcons(self)
# GUI Theme self.syntaxTheme = SyntaxColors()
self.themeMeta = ThemeMeta()
self.isDarkTheme = False self.isDarkTheme = False
# Special Text Colours # Special Text Colours
@@ -118,23 +128,19 @@ class GuiTheme:
self.fadedText = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0)
self.errorText = QColor(255, 0, 0) self.errorText = QColor(255, 0, 0)
# Syntax Theme # Theme Data
self.syntaxMeta = ThemeMeta() self._meta = ThemeMeta()
self.syntaxTheme = SyntaxColors() self._currentTheme = ""
# Load Themes
self._guiPalette = QPalette() self._guiPalette = QPalette()
self._themeList: list[tuple[str, str]] = [] self._allThemes: dict[str, ThemeEntry] = {}
self._syntaxList: list[tuple[str, str]] = []
self._availThemes: dict[str, Path] = {}
self._availSyntax: dict[str, Path] = {}
self._styleSheets: dict[str, str] = {} self._styleSheets: dict[str, str] = {}
self._svgColors: dict[str, bytes] = {}
self._qColors: dict[str, QColor] = {}
# Icon Functions # Icon Functions
self.getIcon = self.iconCache.getIcon self.getIcon = self.iconCache.getIcon
self.getPixmap = self.iconCache.getPixmap self.getPixmap = self.iconCache.getPixmap
self.getItemIcon = self.iconCache.getItemIcon self.getItemIcon = self.iconCache.getItemIcon
self.getIconColor = self.iconCache.getIconColor
self.getToggleIcon = self.iconCache.getToggleIcon self.getToggleIcon = self.iconCache.getToggleIcon
self.getDecoration = self.iconCache.getDecoration self.getDecoration = self.iconCache.getDecoration
self.getHeaderDecoration = self.iconCache.getHeaderDecoration self.getHeaderDecoration = self.iconCache.getHeaderDecoration
@@ -178,19 +184,19 @@ class GuiTheme:
logger.debug("Text 'N' Height: %d", self.textNHeight) logger.debug("Text 'N' Height: %d", self.textNHeight)
logger.debug("Text 'N' Width: %d", self.textNWidth) logger.debug("Text 'N' Width: %d", self.textNWidth)
# Process Themes
_listConf(self._availSyntax, CONFIG.assetPath("syntax"), ".conf")
_listConf(self._availThemes, CONFIG.assetPath("themes"), ".conf")
_listConf(self._availSyntax, CONFIG.dataPath("syntax"), ".conf")
_listConf(self._availThemes, CONFIG.dataPath("themes"), ".conf")
self.loadTheme()
self.loadSyntax()
return return
## ##
# Methods # Properties
##
@property
def colourThemes(self) -> dict[str, ThemeEntry]:
"""Return a dictionary of all themes."""
return self._allThemes
##
# Getters
## ##
def getTextWidth(self, text: str, font: QFont | None = None) -> int: def getTextWidth(self, text: str, font: QFont | None = None) -> int:
@@ -203,30 +209,105 @@ class GuiTheme:
qMetrics = QFontMetrics(self.guiFont) qMetrics = QFontMetrics(self.guiFont)
return ceil(qMetrics.boundingRect(text).width()) return ceil(qMetrics.boundingRect(text).width())
def getBaseColor(self, name: str) -> QColor:
"""Return a base color."""
return QColor(self._qColors.get(name) or QtBlack)
def getRawBaseColor(self, name: str) -> bytes:
"""Return a base color."""
return self._svgColors.get(name, self._svgColors.get("default", b"#000000"))
## ##
# Theme Methods # Theme Methods
## ##
def loadTheme(self) -> bool: def initThemes(self) -> None:
"""Load the currently specified GUI theme.""" """Initialise themes."""
theme = CONFIG.guiTheme CONFIG.splashMessage("Scanning for colour themes ...")
if theme not in self._availThemes: themes: list[Path] = []
logger.error("Could not find GUI theme '%s'", theme) _listContent(themes, CONFIG.assetPath("themes"), ".conf")
theme = DEF_GUI _listContent(themes, CONFIG.dataPath("themes"), ".conf")
CONFIG.guiTheme = theme self._scanThemes(themes)
if not (file := self._availThemes.get(theme)): self.iconCache.initIcons()
self.loadTheme()
return
def isDesktopDarkMode(self) -> bool:
"""Check if the desktop is in dark mode."""
if CONFIG.verQtValue >= 0x060500 and (hint := QGuiApplication.styleHints()):
return hint.colorScheme() == Qt.ColorScheme.Dark
palette = QPalette()
text = palette.windowText().color()
window = palette.window().color()
return text.lightnessF() > window.lightnessF()
def parseColor(self, value: str, default: QColor = QtBlack) -> QColor:
"""Parse a string as a colour value."""
if value in self._qColors:
# Named colour
return self._qColors[value]
elif value.startswith("#") and len(value) == 7:
# Assume #RRGGBB
return QColor.fromString(value)
elif value.startswith("#") and len(value) == 9:
# Assume #RRGGBBAA and convert to #AARRGGBB
return QColor.fromString(f"#{value[7:9]}{value[1:7]}")
elif ":" in value:
# Colour name and alpha
name, _, alpha = value.partition(":")
color = QColor(self._qColors.get(name.strip(), default))
color.setAlpha(checkInt(alpha, 255))
return color
elif "," in value:
# Integer red, green, blue, alpha
data = value.split(",")
result = [0, 0, 0, 255]
for i in range(min(len(data), 4)):
result[i] = checkInt(data[i].strip(), result[i])
return QColor(*result)
return default
def loadTheme(self, force: bool = False) -> bool:
"""Load the currently specified GUI theme. The boolean return
can be used to determine if the GUI needs refreshing.
"""
match CONFIG.themeMode:
case nwTheme.LIGHT:
darkMode = False
case nwTheme.DARK:
darkMode = True
case _:
darkMode = self.isDesktopDarkMode()
theme = CONFIG.darkTheme if darkMode else CONFIG.lightTheme
if theme not in self._allThemes:
logger.error("Could not find theme '%s'", theme)
if darkMode:
theme = DEF_GUI_DARK
CONFIG.darkTheme = DEF_GUI_DARK
else:
theme = DEF_GUI_LIGHT
CONFIG.lightTheme = DEF_GUI_LIGHT
if theme == self._currentTheme and not force:
logger.info("Theme '%s' is already loaded", theme)
return False
entry = self._allThemes.get(theme)
if not entry:
logger.error("Could not load GUI theme") logger.error("Could not load GUI theme")
return False return False
CONFIG.splashMessage("Loading GUI theme ...") CONFIG.splashMessage(f"Loading colour theme: {entry.name}")
logger.info("Loading GUI theme '%s'", theme) logger.info("Loading GUI theme '%s'", theme)
parser = NWConfigParser() parser = ConfigParser()
try: try:
with open(file, mode="r", encoding="utf-8") as fo: parser.read(entry.path, encoding="utf-8")
parser.read_file(fo)
except Exception: except Exception:
logger.error("Could not read file: %s", file) logger.error("Could not read file: %s", entry.path)
logException() logException()
return False return False
@@ -237,39 +318,40 @@ class GuiTheme:
sec = "Main" sec = "Main"
meta = ThemeMeta() meta = ThemeMeta()
if parser.has_section(sec): if parser.has_section(sec):
meta.name = parser.rdStr(sec, "name", "") meta.name = parser.get(sec, "name", fallback="")
meta.description = parser.rdStr(sec, "description", "N/A") meta.mode = parser.get(sec, "mode", fallback="light")
meta.author = parser.rdStr(sec, "author", "N/A") meta.description = parser.get(sec, "description", fallback="N/A")
meta.credit = parser.rdStr(sec, "credit", "N/A") meta.author = parser.get(sec, "author", fallback="N/A")
meta.url = parser.rdStr(sec, "url", "") meta.credit = parser.get(sec, "credit", fallback="N/A")
meta.license = parser.rdStr(sec, "license", "N/A") meta.url = parser.get(sec, "url", fallback="")
meta.licenseUrl = parser.rdStr(sec, "licenseurl", "") meta.license = parser.get(sec, "license", fallback="N/A")
meta.licenseUrl = parser.get(sec, "licenseurl", fallback="")
self.themeMeta = meta self._meta = meta
# Icons # Base
sec = "Icons" sec = "Base"
if parser.has_section(sec): if parser.has_section(sec):
self.iconCache.setIconColor("default", self._parseColor(parser, sec, "default")) self._setBaseColor("default", self._readColor(parser, sec, "default"))
self.iconCache.setIconColor("faded", self._parseColor(parser, sec, "faded")) self._setBaseColor("faded", self._readColor(parser, sec, "faded"))
self.iconCache.setIconColor("red", self._parseColor(parser, sec, "red")) self._setBaseColor("red", self._readColor(parser, sec, "red"))
self.iconCache.setIconColor("orange", self._parseColor(parser, sec, "orange")) self._setBaseColor("orange", self._readColor(parser, sec, "orange"))
self.iconCache.setIconColor("yellow", self._parseColor(parser, sec, "yellow")) self._setBaseColor("yellow", self._readColor(parser, sec, "yellow"))
self.iconCache.setIconColor("green", self._parseColor(parser, sec, "green")) self._setBaseColor("green", self._readColor(parser, sec, "green"))
self.iconCache.setIconColor("aqua", self._parseColor(parser, sec, "aqua")) self._setBaseColor("cyan", self._readColor(parser, sec, "cyan"))
self.iconCache.setIconColor("blue", self._parseColor(parser, sec, "blue")) self._setBaseColor("blue", self._readColor(parser, sec, "blue"))
self.iconCache.setIconColor("purple", self._parseColor(parser, sec, "purple")) self._setBaseColor("purple", self._readColor(parser, sec, "purple"))
# Project # Project
sec = "Project" sec = "Project"
if parser.has_section(sec): if parser.has_section(sec):
self.iconCache.setIconColor("root", self._parseColor(parser, sec, "root")) self._setBaseColor("root", self._readColor(parser, sec, "root"))
self.iconCache.setIconColor("folder", self._parseColor(parser, sec, "folder")) self._setBaseColor("folder", self._readColor(parser, sec, "folder"))
self.iconCache.setIconColor("file", self._parseColor(parser, sec, "file")) self._setBaseColor("file", self._readColor(parser, sec, "file"))
self.iconCache.setIconColor("title", self._parseColor(parser, sec, "title")) self._setBaseColor("title", self._readColor(parser, sec, "title"))
self.iconCache.setIconColor("chapter", self._parseColor(parser, sec, "chapter")) self._setBaseColor("chapter", self._readColor(parser, sec, "chapter"))
self.iconCache.setIconColor("scene", self._parseColor(parser, sec, "scene")) self._setBaseColor("scene", self._readColor(parser, sec, "scene"))
self.iconCache.setIconColor("note", self._parseColor(parser, sec, "note")) self._setBaseColor("note", self._readColor(parser, sec, "note"))
# Palette # Palette
sec = "Palette" sec = "Palette"
@@ -292,16 +374,40 @@ class GuiTheme:
# GUI # GUI
sec = "GUI" sec = "GUI"
if parser.has_section(sec): if parser.has_section(sec):
self.helpText = self._parseColor(parser, sec, "helptext") self.helpText = self._readColor(parser, sec, "helptext")
self.fadedText = self._parseColor(parser, sec, "fadedtext") self.fadedText = self._readColor(parser, sec, "fadedtext")
self.errorText = self._parseColor(parser, sec, "errortext") self.errorText = self._readColor(parser, sec, "errortext")
# Syntax
sec = "Syntax"
self.syntaxTheme = SyntaxColors()
if parser.has_section(sec):
self.syntaxTheme.back = self._readColor(parser, sec, "background")
self.syntaxTheme.text = self._readColor(parser, sec, "text")
self.syntaxTheme.link = self._readColor(parser, sec, "link")
self.syntaxTheme.head = self._readColor(parser, sec, "headertext")
self.syntaxTheme.headH = self._readColor(parser, sec, "headertag")
self.syntaxTheme.emph = self._readColor(parser, sec, "emphasis")
self.syntaxTheme.dialN = self._readColor(parser, sec, "dialog")
self.syntaxTheme.dialA = self._readColor(parser, sec, "altdialog")
self.syntaxTheme.hidden = self._readColor(parser, sec, "hidden")
self.syntaxTheme.note = self._readColor(parser, sec, "note")
self.syntaxTheme.code = self._readColor(parser, sec, "shortcode")
self.syntaxTheme.key = self._readColor(parser, sec, "keyword")
self.syntaxTheme.tag = self._readColor(parser, sec, "tag")
self.syntaxTheme.val = self._readColor(parser, sec, "value")
self.syntaxTheme.opt = self._readColor(parser, sec, "optional")
self.syntaxTheme.spell = self._readColor(parser, sec, "spellcheckline")
self.syntaxTheme.error = self._readColor(parser, sec, "errorline")
self.syntaxTheme.repTag = self._readColor(parser, sec, "replacetag")
self.syntaxTheme.mod = self._readColor(parser, sec, "modifier")
self.syntaxTheme.mark = self._readColor(parser, sec, "texthighlight")
# Update Dependant Colours # Update Dependant Colours
# Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp
text = self._guiPalette.text().color() text = self._guiPalette.text().color()
window = self._guiPalette.window().color() window = self._guiPalette.window().color()
highlight = self._guiPalette.highlight().color() highlight = self._guiPalette.highlight().color()
isDark = text.lightnessF() > window.lightnessF()
QtColActive = QPalette.ColorGroup.Active QtColActive = QPalette.ColorGroup.Active
QtColInactive = QPalette.ColorGroup.Inactive QtColInactive = QPalette.ColorGroup.Inactive
@@ -321,8 +427,8 @@ class GuiTheme:
darkOff = dark.darker(150) darkOff = dark.darker(150)
shadowOff = ref.darker(150) shadowOff = ref.darker(150)
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) grey = QColor(120, 120, 120) if darkMode else QColor(140, 140, 140)
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) dimmed = QColor(130, 130, 130) if darkMode else QColor(190, 190, 190)
placeholder = QColor(text) placeholder = QColor(text)
placeholder.setAlpha(128) placeholder.setAlpha(128)
@@ -351,117 +457,30 @@ class GuiTheme:
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight) self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey) self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
# Set project override colours
if (override := CONFIG.iconColTree) != "theme":
color = self._qColors.get(override, QtBlack)
self._setBaseColor("root", color)
self._setBaseColor("folder", color)
if not CONFIG.iconColDocs:
self._setBaseColor("file", color)
self._setBaseColor("title", color)
self._setBaseColor("chapter", color)
self._setBaseColor("scene", color)
self._setBaseColor("note", color)
self.isDarkTheme = darkMode
self._currentTheme = theme
# Load icons after the theme is parsed # Load icons after the theme is parsed
self.iconCache.loadTheme(CONFIG.iconTheme) self.iconCache.loadTheme(CONFIG.iconTheme)
# Finalise # Finalise
self.isDarkTheme = isDark
QApplication.setPalette(self._guiPalette) QApplication.setPalette(self._guiPalette)
self._buildStyleSheets(self._guiPalette) self._buildStyleSheets(self._guiPalette)
CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}")
return True return True
def loadSyntax(self) -> bool:
"""Load the currently specified syntax highlighter theme."""
theme = CONFIG.guiSyntax
if theme not in self._availSyntax:
logger.error("Could not find syntax theme '%s'", theme)
theme = DEF_SYNTAX
CONFIG.guiSyntax = theme
if not (file := self._availSyntax.get(theme)):
logger.error("Could not load syntax theme")
return False
CONFIG.splashMessage("Loading syntax theme ...")
logger.info("Loading syntax theme '%s'", theme)
parser = NWConfigParser()
try:
with open(file, mode="r", encoding="utf-8") as fo:
parser.read_file(fo)
except Exception:
logger.error("Could not read file: %s", file)
logException()
return False
# Main
sec = "Main"
meta = ThemeMeta()
if parser.has_section(sec):
meta.name = parser.rdStr(sec, "name", "")
meta.description = parser.rdStr(sec, "description", "N/A")
meta.author = parser.rdStr(sec, "author", "N/A")
meta.credit = parser.rdStr(sec, "credit", "N/A")
meta.url = parser.rdStr(sec, "url", "")
meta.license = parser.rdStr(sec, "license", "N/A")
meta.licenseUrl = parser.rdStr(sec, "licenseurl", "")
# Syntax
sec = "Syntax"
syntax = SyntaxColors()
if parser.has_section(sec):
syntax.back = self._parseColor(parser, sec, "background")
syntax.text = self._parseColor(parser, sec, "text")
syntax.link = self._parseColor(parser, sec, "link")
syntax.head = self._parseColor(parser, sec, "headertext")
syntax.headH = self._parseColor(parser, sec, "headertag")
syntax.emph = self._parseColor(parser, sec, "emphasis")
syntax.dialN = self._parseColor(parser, sec, "dialog")
syntax.dialA = self._parseColor(parser, sec, "altdialog")
syntax.hidden = self._parseColor(parser, sec, "hidden")
syntax.note = self._parseColor(parser, sec, "note")
syntax.code = self._parseColor(parser, sec, "shortcode")
syntax.key = self._parseColor(parser, sec, "keyword")
syntax.tag = self._parseColor(parser, sec, "tag")
syntax.val = self._parseColor(parser, sec, "value")
syntax.opt = self._parseColor(parser, sec, "optional")
syntax.spell = self._parseColor(parser, sec, "spellcheckline")
syntax.error = self._parseColor(parser, sec, "errorline")
syntax.repTag = self._parseColor(parser, sec, "replacetag")
syntax.mod = self._parseColor(parser, sec, "modifier")
syntax.mark = self._parseColor(parser, sec, "texthighlight")
CONFIG.splashMessage(f"Loaded syntax theme: {meta.name}")
self.syntaxMeta = meta
self.syntaxTheme = syntax
return True
def listThemes(self) -> list[tuple[str, str]]:
"""Scan the GUI themes folder and list all themes."""
if self._themeList:
return self._themeList
themes = []
parser = NWConfigParser()
for key, path in self._availThemes.items():
logger.debug("Checking theme config '%s'", key)
if name := _loadInternalName(parser, path):
themes.append((key, name))
self._themeList = sorted(themes, key=_sortTheme)
return self._themeList
def listSyntax(self) -> list[tuple[str, str]]:
"""Scan the syntax themes folder and list all themes."""
if self._syntaxList:
return self._syntaxList
themes = []
parser = NWConfigParser()
for key, path in self._availSyntax.items():
logger.debug("Checking theme syntax '%s'", key)
if name := _loadInternalName(parser, path):
themes.append((key, name))
self._syntaxList = sorted(themes, key=_sortTheme)
return self._syntaxList
def getStyleSheet(self, name: str) -> str: def getStyleSheet(self, name: str) -> str:
"""Load a standard style sheet.""" """Load a standard style sheet."""
return self._styleSheets.get(name, "") return self._styleSheets.get(name, "")
@@ -470,22 +489,26 @@ class GuiTheme:
# Internal Functions # Internal Functions
## ##
def _setBaseColor(self, key: str, color: QColor) -> None:
"""Set the colour for a named colour."""
self._qColors[key] = QColor(color)
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
return
def _resetTheme(self) -> None: def _resetTheme(self) -> None:
"""Reset GUI colours to default values.""" """Reset GUI colours to default values."""
palette = QPalette() palette = QPalette()
isDark = self.isDesktopDarkMode()
text = palette.color(QPalette.ColorRole.Text)
window = palette.color(QPalette.ColorRole.Window)
isDark = text.lightnessF() > window.lightnessF()
# Reset GUI Palette # Reset GUI Palette
default = palette.color(QPalette.ColorRole.Text)
faded = QColor(128, 128, 128) faded = QColor(128, 128, 128)
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41) red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41)
orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31) orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31)
yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0) yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0)
green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0) green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0)
aqua = QColor(102, 204, 204) if isDark else QColor(62, 153, 159) cyan = QColor(102, 204, 204) if isDark else QColor(62, 153, 159)
blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174) blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174)
purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168) purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168)
@@ -496,37 +519,38 @@ class GuiTheme:
self._guiPalette = palette self._guiPalette = palette
# Reset Icons # Reset Base Colours and Icons
icons = self.iconCache self.iconCache.clear()
icons.clear() self._svgColors = {}
icons.setIconColor("default", text) self._qColors = {}
icons.setIconColor("faded", faded) self._setBaseColor("default", default)
icons.setIconColor("red", red) self._setBaseColor("faded", faded)
icons.setIconColor("orange", orange) self._setBaseColor("red", red)
icons.setIconColor("yellow", yellow) self._setBaseColor("orange", orange)
icons.setIconColor("green", green) self._setBaseColor("yellow", yellow)
icons.setIconColor("aqua", aqua) self._setBaseColor("green", green)
icons.setIconColor("blue", blue) self._setBaseColor("cyan", cyan)
icons.setIconColor("purple", purple) self._setBaseColor("blue", blue)
icons.setIconColor("root", blue) self._setBaseColor("purple", purple)
icons.setIconColor("folder", yellow) self._setBaseColor("root", blue)
icons.setIconColor("file", text) self._setBaseColor("folder", yellow)
icons.setIconColor("title", green) self._setBaseColor("file", default)
icons.setIconColor("chapter", red) self._setBaseColor("title", green)
icons.setIconColor("scene", blue) self._setBaseColor("chapter", red)
icons.setIconColor("note", yellow) self._setBaseColor("scene", blue)
self._setBaseColor("note", yellow)
return return
def _parseColor(self, parser: NWConfigParser, section: str, name: str) -> QColor: def _readColor(self, parser: ConfigParser, section: str, name: str) -> QColor:
"""Parse a colour value from a config string.""" """Parse a colour value from a config string."""
return QColor(*parser.rdIntList(section, name, [0, 0, 0, 255])) return self.parseColor(parser.get(section, name, fallback="default"))
def _setPalette( def _setPalette(
self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole self, parser: ConfigParser, section: str, name: str, value: QPalette.ColorRole
) -> None: ) -> None:
"""Set a palette colour value from a config string.""" """Set a palette colour value from a config string."""
self._guiPalette.setBrush(value, self._parseColor(parser, section, name)) self._guiPalette.setBrush(value, self._readColor(parser, section, name))
return return
def _buildStyleSheets(self, palette: QPalette) -> None: def _buildStyleSheets(self, palette: QPalette) -> None:
@@ -561,6 +585,35 @@ class GuiTheme:
return return
def _scanThemes(self, files: list[Path]) -> None:
"""Scan the GUI themes folder and list all themes."""
parser = ConfigParser()
data: dict[str, tuple[str, str, bool, Path]] = {}
keys = []
for file in files:
try:
parser.clear()
parser.read(file, encoding="utf-8")
name = parser.get("Main", "name", fallback="")
dark = parser.get("Main", "mode", fallback="light").lower() == "dark"
if name:
key = file.stem
prefix = "*" if key.startswith("default") else ""
lookup = f"{prefix}{name} {key}"
keys.append(lookup)
data[lookup] = (file.stem, name, dark, file)
except Exception: # noqa: PERF203
logger.error("Could not read file: %s", file)
logException()
self._allThemes = {}
for lookup in sorted(keys):
key, name, dark, item = data[lookup]
logger.debug("Checking theme config '%s'", key)
self._allThemes[key] = ThemeEntry(name, dark, item)
return
class GuiIcons: class GuiIcons:
"""The icon class manages the content of the assets/icons folder, """The icon class manages the content of the assets/icons folder,
@@ -572,9 +625,8 @@ class GuiIcons:
""" """
__slots__ = ( __slots__ = (
"_availThemes", "_headerDec", "_headerDecNarrow", "_noIcon", "_allThemes", "_headerDec", "_headerDecNarrow", "_meta",
"_qColors", "_qIcons", "_svgColors", "_svgData", "_themeList", "_noIcon", "_qIcons", "_svgData", "_theme",
"mainTheme", "themeMeta",
) )
TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = { TOGGLE_ICON_KEYS: Final[dict[str, tuple[str, str]]] = {
@@ -588,63 +640,72 @@ class GuiIcons:
def __init__(self, mainTheme: GuiTheme) -> None: def __init__(self, mainTheme: GuiTheme) -> None:
self.mainTheme = mainTheme self._theme = mainTheme
self.themeMeta = ThemeMeta() self._meta = ThemeMeta()
# Storage # Storage
self._allThemes: dict[str, ThemeEntry] = {}
self._svgData: dict[str, bytes] = {} self._svgData: dict[str, bytes] = {}
self._svgColors: dict[str, bytes] = {}
self._qColors: dict[str, QColor] = {}
self._qIcons: dict[str, QIcon] = {} self._qIcons: dict[str, QIcon] = {}
self._headerDec: list[QPixmap] = [] self._headerDec: list[QPixmap] = []
self._headerDecNarrow: list[QPixmap] = [] self._headerDecNarrow: list[QPixmap] = []
# Icon Theme Path
self._availThemes: dict[str, Path] = {}
self._themeList: list[tuple[str, str]] = []
# None Icon # None Icon
self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg")) self._noIcon = QIcon(str(CONFIG.assetPath("icons") / "none.svg"))
_listConf(self._availThemes, CONFIG.assetPath("icons"), ".icons")
_listConf(self._availThemes, CONFIG.dataPath("icons"), ".icons")
return return
def clear(self) -> None: def clear(self) -> None:
"""Clear the icon cache.""" """Clear the icon cache."""
self._svgData = {} self._svgData = {}
self._svgColors = {}
self._qColors = {}
self._qIcons = {} self._qIcons = {}
self._headerDec = [] self._headerDec = []
self._headerDecNarrow = [] self._headerDecNarrow = []
self.themeMeta = ThemeMeta() self._meta = ThemeMeta()
return return
##
# Properties
##
@property
def iconThemes(self) -> dict[str, ThemeEntry]:
"""Return a dictionary of all icon themes."""
return self._allThemes
## ##
# Actions # Actions
## ##
def loadTheme(self, theme: str) -> bool: def initIcons(self) -> None:
"""Initialise icons."""
CONFIG.splashMessage("Scanning for icon themes ...")
icons: list[Path] = []
_listContent(icons, CONFIG.assetPath("icons"), ".icons")
_listContent(icons, CONFIG.dataPath("icons"), ".icons")
self._scanThemes(icons)
return
def loadTheme(self, theme: str) -> None:
"""Update the theme map. This is more of an init, since many of """Update the theme map. This is more of an init, since many of
the GUI icons cannot really be replaced without writing specific the GUI icons cannot really be replaced without writing specific
update functions for the classes where they're used. update functions for the classes where they're used.
""" """
if theme not in self._availThemes: if theme not in self._allThemes:
logger.error("Could not find icon theme '%s'", theme) logger.error("Could not find icon theme '%s'", theme)
theme = DEF_ICONS theme = DEF_ICONS
CONFIG.iconTheme = theme CONFIG.iconTheme = theme
if not (file := self._availThemes.get(theme)): entry = self._allThemes.get(theme)
if not entry:
logger.error("Could not load icon theme") logger.error("Could not load icon theme")
return False return
CONFIG.splashMessage("Loading icon theme ...") CONFIG.splashMessage(f"Loading icon theme: {entry.name}")
logger.info("Loading icon theme '%s'", theme) logger.info("Loading icon theme '%s'", theme)
try: try:
meta = ThemeMeta() meta = ThemeMeta()
with open(file, mode="r", encoding="utf-8") as icons: with open(entry.path, mode="r", encoding="utf-8") as icons:
for icon in icons: for icon in icons:
bits = icon.partition("=") bits = icon.partition("=")
key = bits[0].strip() key = bits[0].strip()
@@ -658,47 +719,23 @@ class GuiIcons:
meta.author = value meta.author = value
elif key == "meta:license": elif key == "meta:license":
meta.license = value meta.license = value
self.themeMeta = meta self._meta = meta
except Exception: except Exception:
logger.error("Could not read file: %s", file) logger.error("Could not read file: %s", entry.path)
logException() logException()
return False return
CONFIG.splashMessage(f"Loaded icon theme: {meta.name}")
CONFIG.splashMessage("Generating additional icons ...")
# Set colour overrides for project item icons
if (override := CONFIG.iconColTree) != "theme":
color = self._svgColors.get(override, b"#000000")
self._svgColors["root"] = color
self._svgColors["folder"] = color
if not CONFIG.iconColDocs:
self._svgColors["file"] = color
self._svgColors["title"] = color
self._svgColors["chapter"] = color
self._svgColors["scene"] = color
self._svgColors["note"] = color
# Populate generated icons cache # Populate generated icons cache
CONFIG.splashMessage("Generating additional icons ...")
self.getHeaderDecoration(0) self.getHeaderDecoration(0)
self.getHeaderDecorationNarrow(0) self.getHeaderDecorationNarrow(0)
return True
def setIconColor(self, key: str, color: QColor) -> None:
"""Set an icon colour for a named colour."""
self._qColors[key] = QColor(color)
self._svgColors[key] = color.name(QColor.NameFormat.HexRgb).encode("utf-8")
return return
## ##
# Access Functions # Access Functions
## ##
def getIconColor(self, name: str) -> QColor:
"""Return an icon color."""
return QColor(self._qColors.get(name) or QtBlack)
def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon: def getIcon(self, name: str, color: str | None = None, w: int = 24, h: int = 24) -> QIcon:
"""Return an icon from the icon buffer, or load it.""" """Return an icon from the icon buffer, or load it."""
variant = f"{name}-{color}" if color else name variant = f"{name}-{color}" if color else name
@@ -769,7 +806,7 @@ class GuiIcons:
map or the icon map. This function always returns a QPixmap. map or the icon map. This function always returns a QPixmap.
""" """
if name in self.IMAGE_MAP: if name in self.IMAGE_MAP:
idx = int(self.mainTheme.isDarkTheme) idx = int(self._theme.isDarkTheme)
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx] imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
else: else:
logger.error("Decoration with name '%s' does not exist", name) logger.error("Decoration with name '%s' does not exist", name)
@@ -793,7 +830,7 @@ class GuiIcons:
def getHeaderDecoration(self, hLevel: int) -> QPixmap: def getHeaderDecoration(self, hLevel: int) -> QPixmap:
"""Get the decoration for a specific heading level.""" """Get the decoration for a specific heading level."""
if not self._headerDec: if not self._headerDec:
iPx = self.mainTheme.baseIconHeight iPx = self._theme.baseIconHeight
self._headerDec = [ self._headerDec = [
self._generateDecoration("file", iPx, 0), self._generateDecoration("file", iPx, 0),
self._generateDecoration("title", iPx, 0), self._generateDecoration("title", iPx, 0),
@@ -806,7 +843,7 @@ class GuiIcons:
def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap: def getHeaderDecorationNarrow(self, hLevel: int) -> QPixmap:
"""Get the narrow decoration for a specific heading level.""" """Get the narrow decoration for a specific heading level."""
if not self._headerDecNarrow: if not self._headerDecNarrow:
iPx = self.mainTheme.baseIconHeight iPx = self._theme.baseIconHeight
self._headerDecNarrow = [ self._headerDecNarrow = [
self._generateDecoration("file", iPx, 0), self._generateDecoration("file", iPx, 0),
self._generateDecoration("title", iPx, 0), self._generateDecoration("title", iPx, 0),
@@ -817,21 +854,6 @@ class GuiIcons:
] ]
return self._headerDecNarrow[minmax(hLevel, 0, 5)] return self._headerDecNarrow[minmax(hLevel, 0, 5)]
def listThemes(self) -> list[tuple[str, str]]:
"""Scan the GUI icons folder and list all themes."""
if self._themeList:
return self._themeList
themes = []
for key, path in self._availThemes.items():
logger.debug("Checking icon theme '%s'", key)
if name := _loadIconName(path):
themes.append((key, name))
self._themeList = sorted(themes, key=_sortTheme)
return self._themeList
## ##
# Internal Functions # Internal Functions
## ##
@@ -847,7 +869,7 @@ class GuiIcons:
return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg")) return QIcon(str(CONFIG.assetPath("icons") / "x-novelwriter-project.svg"))
if svg := self._svgData.get(name, b""): if svg := self._svgData.get(name, b""):
if fill := self._svgColors.get(color or "default"): if fill := self._theme.getRawBaseColor(color or "default"):
svg = svg.replace(b"#000000", fill) svg = svg.replace(b"#000000", fill)
pixmap = QPixmap(w, h) pixmap = QPixmap(w, h)
pixmap.fill(QtTransparent) pixmap.fill(QtTransparent)
@@ -869,53 +891,46 @@ class GuiIcons:
painter = QPainter(pixmap) painter = QPainter(pixmap)
painter.setRenderHint(QtPaintAntiAlias) painter.setRenderHint(QtPaintAntiAlias)
if fill := self._svgColors.get(color or "default"): if fill := self._theme.getRawBaseColor(color or "default"):
painter.fillPath(path, QColor(fill.decode(encoding="utf-8"))) painter.fillPath(path, QColor(fill.decode(encoding="utf-8")))
painter.end() painter.end()
tMode = Qt.TransformationMode.SmoothTransformation tMode = Qt.TransformationMode.SmoothTransformation
return pixmap.scaledToHeight(height, tMode) return pixmap.scaledToHeight(height, tMode)
def _scanThemes(self, entries: list[Path]) -> None:
"""Scan the GUI themes folder and list all themes."""
data: dict[str, tuple[str, str, Path]] = {}
keys = []
for entry in entries:
try:
with open(entry, mode="r", encoding="utf-8") as fo:
for line in fo:
key, _, value = line.partition("=")
if key.strip() == "meta:name":
if name := value.strip():
lookup = entry.stem
keys.append(lookup)
data[lookup] = (lookup, name, entry)
break
except Exception:
logger.error("Could not read file: %s", entry)
logException()
self._allThemes = {}
for lookup in sorted(keys):
key, name, item = data[lookup]
logger.debug("Checking icon theme '%s'", key)
self._allThemes[key] = ThemeEntry(name, False, item)
return
# Module Functions # Module Functions
# ================ # ================
def _listConf(target: dict, path: Path, extension: str) -> None: def _listContent(data: list[Path], path: Path, extension: str) -> None:
"""Scan for theme files and populate the dictionary.""" """List files of a specific type and extend the list."""
if path.is_dir(): if path.is_dir():
for item in path.iterdir(): data.extend(n for n in path.iterdir() if n.is_file() and n.suffix == extension)
if item.is_file() and item.name.endswith(extension):
target[item.stem] = item
return return
def _sortTheme(data: tuple[str, str]) -> str:
"""Key function for theme sorting."""
key, name = data
return f"*{name}" if key.startswith("default_") else name
def _loadInternalName(parser: NWConfigParser, path: str | Path) -> str:
"""Open a conf file and read the 'name' setting."""
try:
with open(path, mode="r", encoding="utf-8") as inFile:
parser.read_file(inFile)
return parser.rdStr("Main", "name", "")
except Exception:
logger.error("Could not read file: %s", path)
logException()
return ""
def _loadIconName(path: Path) -> str:
"""Open an icons file and read the name setting."""
try:
with open(path, mode="r", encoding="utf-8") as icons:
for icon in icons:
key, _, value = icon.partition("=")
if key.strip() == "meta:name":
return value.strip()
except Exception:
logger.error("Could not read file: %s", path)
logException()
return ""
+37 -18
View File
@@ -30,7 +30,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from time import time from time import time
from PyQt6.QtCore import Qt, QTimer, pyqtSlot from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSlot
from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox,
@@ -902,10 +902,45 @@ class GuiMain(QMainWindow):
return not self.splitView.isVisible() return not self.splitView.isVisible()
def checkThemeUpdate(self) -> None:
"""Load theme if mode changed."""
if SHARED.theme.loadTheme():
self.refreshThemeColors(syntax=True)
self.docEditor.initEditor()
self.docViewer.initViewer()
return
def refreshThemeColors(self, syntax: bool = False, force: bool = False) -> None:
"""Refresh the GUI theme."""
SHARED.theme.loadTheme(force=force)
self.setPalette(QApplication.palette())
self.docEditor.updateTheme()
self.docViewer.updateTheme()
self.docViewerPanel.updateTheme()
self.sideBar.updateTheme()
self.projView.updateTheme()
self.novelView.updateTheme()
self.projSearch.updateTheme()
self.outlineView.updateTheme()
self.itemDetails.updateTheme()
self.mainStatus.updateTheme()
SHARED.project.tree.refreshAllItems()
if syntax:
self.docEditor.updateSyntaxColors()
return
## ##
# Events # Events
## ##
def changeEvent(self, event: QEvent) -> None:
"""Capture application change events."""
if int(event.type()) == 210: # ThemeChange
self.checkThemeUpdate()
return
def closeEvent(self, event: QCloseEvent) -> None: def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the closing event of the GUI and call the close """Capture the closing event of the GUI and call the close
function to handle all the close process steps. function to handle all the close process steps.
@@ -1054,23 +1089,7 @@ class GuiMain(QMainWindow):
self.novelView.refreshCurrentTree() self.novelView.refreshCurrentTree()
if theme: if theme:
SHARED.theme.loadTheme() self.refreshThemeColors(syntax=syntax, force=True)
self.setPalette(QApplication.palette())
self.docEditor.updateTheme()
self.docViewer.updateTheme()
self.docViewerPanel.updateTheme()
self.sideBar.updateTheme()
self.projView.updateTheme()
self.novelView.updateTheme()
self.projSearch.updateTheme()
self.outlineView.updateTheme()
self.itemDetails.updateTheme()
self.mainStatus.updateTheme()
SHARED.project.tree.refreshAllItems()
if syntax:
SHARED.theme.loadSyntax()
self.docEditor.updateSyntaxColors()
self.docEditor.initEditor() self.docEditor.initEditor()
self.docViewer.initViewer() self.docViewer.initViewer()
+1
View File
@@ -175,6 +175,7 @@ class SharedData(QObject):
is created. is created.
""" """
self._theme = theme self._theme = theme
self._theme.initThemes()
return return
def initSharedData(self, gui: GuiMain) -> None: def initSharedData(self, gui: GuiMain) -> None:
+3
View File
@@ -65,6 +65,9 @@ class MockTheme:
self.guiFontBU = QFont() self.guiFontBU = QFont()
return return
def initThemes(self) -> None:
return
def getPixmap(self, *a) -> QPixmap: def getPixmap(self, *a) -> QPixmap:
return QPixmap() return QPixmap()
+4 -3
View File
@@ -1,10 +1,11 @@
[Meta] [Meta]
timestamp = 2025-05-11 13:28:11 timestamp = 2025-06-02 16:21:38
[Main] [Main]
font = font =
theme = default lighttheme = default_light
syntax = default_light darktheme = default_dark
thememode = AUTO
icons = material_rounded_normal icons = material_rounded_normal
iconcoltree = theme iconcoltree = theme
iconcoldocs = False iconcoldocs = False
+6
View File
@@ -41,6 +41,7 @@ from novelwriter.common import (
readTextFile, simplified, transferCase, uniqueCompact, xmlElement, readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
xmlIndent, xmlSubElem, yesNo xmlIndent, xmlSubElem, yesNo
) )
from novelwriter.enum import nwItemClass
from tests.mocked import causeOSError from tests.mocked import causeOSError
from tests.tools import writeFile from tests.tools import writeFile
@@ -804,6 +805,7 @@ def testBaseCommon_NWConfigParser(fncPath):
"list1 = a, b, c\n" "list1 = a, b, c\n"
"list2 = 17, 18, 19\n" "list2 = 17, 18, 19\n"
"float1 = 4.2\n" "float1 = 4.2\n"
"enum1 = NOVEL\n"
f"path1 = {fncPath}\n" f"path1 = {fncPath}\n"
)) ))
@@ -875,3 +877,7 @@ def testBaseCommon_NWConfigParser(fncPath):
assert cfgParser.rdIntList("nope", "list2", [1]) == [1] assert cfgParser.rdIntList("nope", "list2", [1]) == [1]
assert cfgParser.rdIntList("main", "blabla", [1]) == [1] assert cfgParser.rdIntList("main", "blabla", [1]) == [1]
# Read Enum
assert cfgParser.rdEnum("main", "enum1", nwItemClass.NO_CLASS) == nwItemClass.NOVEL
assert cfgParser.rdEnum("main", "blabla", nwItemClass.NO_CLASS) == nwItemClass.NO_CLASS
+4 -4
View File
@@ -131,15 +131,15 @@ def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths):
assert tstConf.errorText().startswith("Could not load config file") assert tstConf.errorText().startswith("Could not load config file")
# Change a few settings, save, reset, and reload # Change a few settings, save, reset, and reload
tstConf.guiTheme = "foo" tstConf.lightTheme = "foo"
tstConf.guiSyntax = "bar" tstConf.darkTheme = "bar"
assert tstConf.saveConfig() is True assert tstConf.saveConfig() is True
newConf = Config() newConf = Config()
newConf.initConfig(confPath=fncPath, dataPath=fncPath) newConf.initConfig(confPath=fncPath, dataPath=fncPath)
newConf.loadConfig() newConf.loadConfig()
assert newConf.guiTheme == "foo" assert newConf.lightTheme == "foo"
assert newConf.guiSyntax == "bar" assert newConf.darkTheme == "bar"
# Test Correcting Quote Settings # Test Correcting Quote Settings
tstConf.fmtDQuoteOpen = '"' tstConf.fmtDQuoteOpen = '"'
+19 -16
View File
@@ -27,10 +27,11 @@ from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent
from PyQt6.QtWidgets import QFileDialog, QFontDialog from PyQt6.QtWidgets import QFileDialog, QFontDialog
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.config import DEF_GUI from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.gui.theme import ThemeEntry
from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone
KEY_DELAY = 1 KEY_DELAY = 1
@@ -55,15 +56,11 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
assert "en_GB" in languages assert "en_GB" in languages
# Check GUI Themes # Check GUI Themes
themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())] themes = [prefs.lightTheme.itemData(i) for i in range(prefs.lightTheme.count())]
assert len(themes) >= 5 assert DEF_GUI_LIGHT in themes
assert DEF_GUI in themes
# Check GUI Syntax themes = [prefs.darkTheme.itemData(i) for i in range(prefs.darkTheme.count())]
syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())] assert DEF_GUI_DARK in themes
assert len(syntax) >= 10
assert "default_dark" in syntax
assert "default_light" in syntax
# Check Spell Checking # Check Spell Checking
spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())] spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())]
@@ -124,7 +121,7 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
button = prefs.buttonBox.button(QtDialogSave) button = prefs.buttonBox.button(QtDialogSave)
assert button is not None assert button is not None
button.click() button.click()
assert signal.args == [False, False, False, False] assert len(signal.args) == 4
# Check Close Button # Check Close Button
prefs.show() prefs.show()
@@ -151,6 +148,12 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
(fncPath / "nw_en_US.qm").touch() (fncPath / "nw_en_US.qm").touch()
(fncPath / "project_en_US.json").touch() (fncPath / "project_en_US.json").touch()
CONFIG._nwLangPath = fncPath CONFIG._nwLangPath = fncPath
SHARED.theme._allThemes = {
"theme1": ThemeEntry(name="Theme 1", dark=False, path=fncPath),
"theme2": ThemeEntry(name="Theme 2", dark=False, path=fncPath),
"theme3": ThemeEntry(name="Theme 3", dark=True, path=fncPath),
"theme4": ThemeEntry(name="Theme 4", dark=True, path=fncPath),
}
prefs = GuiPreferences(nwGUI) prefs = GuiPreferences(nwGUI)
with qtbot.waitExposed(prefs): with qtbot.waitExposed(prefs):
@@ -158,7 +161,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
# Appearance # Appearance
prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US")) prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US"))
prefs.guiTheme.setCurrentIndex(prefs.guiTheme.findData("default_dark")) prefs.lightTheme.setCurrentIndex(prefs.lightTheme.findData("theme1"))
prefs.darkTheme.setCurrentIndex(prefs.darkTheme.findData("theme3"))
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True)) mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(True) # Use OS font dialog prefs.nativeFont.setChecked(True) # Use OS font dialog
@@ -169,14 +173,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.useCharCount.setChecked(True) prefs.useCharCount.setChecked(True)
assert CONFIG.guiLocale != "en_US" assert CONFIG.guiLocale != "en_US"
assert CONFIG.guiTheme != "default_dark" assert CONFIG.lightTheme == "default_light"
assert CONFIG.darkTheme == "default_dark"
assert CONFIG.guiFont.family() != "" assert CONFIG.guiFont.family() != ""
assert CONFIG.hideVScroll is False assert CONFIG.hideVScroll is False
assert CONFIG.hideHScroll is False assert CONFIG.hideHScroll is False
assert CONFIG.useCharCount is False assert CONFIG.useCharCount is False
# Document Style # Document Style
prefs.guiSyntax.setCurrentIndex(prefs.guiSyntax.findData("default_dark"))
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True)) mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(False) # Use Qt font dialog prefs.nativeFont.setChecked(False) # Use Qt font dialog
@@ -185,7 +189,6 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.showFullPath.setChecked(False) prefs.showFullPath.setChecked(False)
prefs.incNotesWCount.setChecked(False) prefs.incNotesWCount.setChecked(False)
assert CONFIG.guiSyntax != "default_dark"
assert CONFIG.textFont.family() != "" assert CONFIG.textFont.family() != ""
assert CONFIG.showFullPath is True assert CONFIG.showFullPath is True
assert CONFIG.incNotesWCount is True assert CONFIG.incNotesWCount is True
@@ -344,14 +347,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
# Appearance # Appearance
assert CONFIG.guiLocale == "en_US" assert CONFIG.guiLocale == "en_US"
assert CONFIG.guiTheme == "default_dark" assert CONFIG.lightTheme == "theme1"
assert CONFIG.darkTheme == "theme3"
assert CONFIG.guiFont == QFont() assert CONFIG.guiFont == QFont()
assert CONFIG.hideVScroll is True assert CONFIG.hideVScroll is True
assert CONFIG.hideHScroll is True assert CONFIG.hideHScroll is True
assert CONFIG.useCharCount is True assert CONFIG.useCharCount is True
# Document Style # Document Style
assert CONFIG.guiSyntax == "default_dark"
assert CONFIG.textFont == QFont() assert CONFIG.textFont == QFont()
assert CONFIG.showFullPath is False assert CONFIG.showFullPath is False
assert CONFIG.incNotesWCount is False assert CONFIG.incNotesWCount is False
+23 -7
View File
@@ -24,6 +24,7 @@ import shutil
from pathlib import Path from pathlib import Path
from shutil import copyfile from shutil import copyfile
from unittest.mock import Mock
import pytest import pytest
@@ -32,9 +33,10 @@ from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QInputDialog, QMessageBox from PyQt6.QtWidgets import QInputDialog, QMessageBox
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwView from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwTheme, nwView
from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.noveltree import GuiNovelView
from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.outline import GuiOutlineView
@@ -179,21 +181,35 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
@pytest.mark.gui @pytest.mark.gui
def testGuiMain_UpdateTheme(qtbot, nwGUI): def testGuiMain_UpdateTheme(qtbot, nwGUI):
"""Test updating the theme in the GUI.""" """Test updating the theme in the GUI."""
mainTheme = SHARED.theme theme = SHARED.theme
CONFIG.guiTheme = "default_dark" CONFIG.themeMode = nwTheme.DARK
CONFIG.guiSyntax = "default_dark" CONFIG.darkTheme = DEF_GUI_DARK
mainTheme.loadTheme() CONFIG.lightTheme = DEF_GUI_LIGHT
mainTheme.loadSyntax() theme.loadTheme()
assert theme.isDarkTheme is True
nwGUI._processConfigChanges(False, True, False, False) nwGUI._processConfigChanges(False, True, False, False)
nwGUI._processConfigChanges(True, True, True, True) nwGUI._processConfigChanges(True, True, True, True)
# Check editor syntax
syntax = SHARED.theme.syntaxTheme syntax = SHARED.theme.syntaxTheme
assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntax.back
assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back
assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntax.back
assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back
# Update by check
CONFIG.themeMode = nwTheme.LIGHT
nwGUI.checkThemeUpdate()
assert theme.isDarkTheme is False
# Through change event
event = Mock()
event.type.return_value = 210
CONFIG.themeMode = nwTheme.DARK
nwGUI.changeEvent(event)
assert theme.isDarkTheme is True
# qtbot.stop() # qtbot.stop()
+53
View File
@@ -0,0 +1,53 @@
"""
novelWriter Side Bar Class Tester
===================================
This file is a part of novelWriter
Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from novelwriter import CONFIG
from novelwriter.constants import nwLabels
from novelwriter.enum import nwTheme
@pytest.mark.gui
def testGuiSideBar_CycleColourTheme(nwGUI):
"""Test theme cycle feature on the side bar."""
CONFIG.themeMode = nwTheme.AUTO
sidebar = nwGUI.sideBar
sidebar.mainGui.checkThemeUpdate = lambda *a: None
# Run 3 Cycles
for _ in range(3):
# Cycle Light
sidebar._cycleColurTheme()
assert CONFIG.themeMode == nwTheme.LIGHT
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.LIGHT]
# Cycle Dark
sidebar._cycleColurTheme()
assert CONFIG.themeMode == nwTheme.DARK
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.DARK]
# Cycle Auto
sidebar._cycleColurTheme()
assert CONFIG.themeMode == nwTheme.AUTO
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.AUTO]
+1 -1
View File
@@ -31,7 +31,7 @@ from tests.tools import C, buildTestProject
@pytest.mark.gui @pytest.mark.gui
def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the the various features of the status bar.""" """Test the various features of the status bar."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
cHandle = SHARED.project.newFile("A Note", C.hCharRoot) cHandle = SHARED.project.newFile("A Note", C.hCharRoot)
newDoc = SHARED.project.storage.getDocument(cHandle) newDoc = SHARED.project.storage.getDocument(cHandle)
+340 -298
View File
@@ -20,344 +20,389 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
import sys from configparser import ConfigParser
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, Mock
import pytest import pytest
from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor, QFont, QFontDatabase, QIcon, QPalette, QPixmap, QStyleHints
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG
from novelwriter.common import NWConfigParser from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS
from novelwriter.config import DEF_GUI
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
from novelwriter.gui.theme import _listConf from novelwriter.gui.theme import (
STYLES_BIG_TOOLBUTTON, STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON, GuiTheme,
ThemeMeta, _listContent
)
from tests.mocked import causeOSError from tests.mocked import causeOSError
from tests.tools import writeFile
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Main(qtbot, nwGUI, tstPaths): def testGuiTheme_ParseColor():
"""Test the theme class init.""" """Test the colour parsing."""
mainTheme = SHARED.theme theme = GuiTheme()
# Methods # Pre-Populate
# ======= theme._qColors["red"] = QColor(255, 0, 0)
theme._qColors["green"] = QColor(0, 255, 0)
theme._qColors["blue"] = QColor(0, 0, 255)
mSize = mainTheme.getTextWidth("m") # By Name
assert mSize > 0 assert theme.parseColor("red").getRgb() == (255, 0, 0, 255)
assert mainTheme.getTextWidth("m", mainTheme.guiFont) == mSize assert theme.parseColor("green").getRgb() == (0, 255, 0, 255)
assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255)
assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255)
# Scan for Themes # CSS Format
# =============== assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255)
assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127)
assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored
assert theme.parseColor("#ff00007f15").getRgb() == (0, 0, 0, 255) # Too long -> ignored
result = {} # Name + Alpha
_listConf({}, Path("not_a_path"), ".conf") assert theme.parseColor("red:255").getRgb() == (255, 0, 0, 255)
assert result == {} assert theme.parseColor("red:127").getRgb() == (255, 0, 0, 127)
assert theme.parseColor("red:512").getRgb() == (255, 0, 0, 255) # Value truncated
themeOne = tstPaths.cnfDir / "themes" / "themeone.conf" # Values
themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf" assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255)
writeFile(themeOne, "# Stuff") assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255)
writeFile(themeTwo, "# Stuff") assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127)
assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated
_listConf(result, tstPaths.cnfDir / "themes", ".conf")
assert result["themeone"] == themeOne
assert result["themetwo"] == themeTwo
# Parse Colours
# =============
parser = NWConfigParser()
parser["Palette"] = {
"colour1": "100, 150, 200", # Valid
"colour2": "100, 150, 200, 250", # With alpha
"colour3": "100, 150, 200, 250, 300", # Too many values
"colour4": "250, 250", # Missing blue
"colour5": "-10, 127, 300", # Invalid red and blue
"colour6": "bob, 127, 255", # Invalid red
}
# Test the parser for several valid and invalid values
assert mainTheme._parseColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
assert mainTheme._parseColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
assert mainTheme._parseColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
assert mainTheme._parseColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
assert mainTheme._parseColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
assert mainTheme._parseColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)
# The palette should load with the parsed values
mainTheme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255)
mainTheme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255)
mainTheme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0)
mainTheme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255)
# Non-existing value should return default colour
mainTheme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255)
# qtbot.stop()
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): def testGuiTheme_ScanThemes(monkeypatch):
"""Test the theme part of the class.""" """Test the themes scanning."""
mainTheme = SHARED.theme theme = GuiTheme()
# List Themes # Load built-in themes
files = []
_listContent(files, CONFIG.assetPath("themes"), ".conf")
assert len(files) > 0
# Block reading theme files
with monkeypatch.context() as mp:
mp.setattr(ConfigParser, "read", causeOSError)
theme._scanThemes(files)
assert theme.colourThemes == {}
# Read all themes correctly
theme._scanThemes(files)
assert len(theme.colourThemes) > 0
dark = theme.colourThemes[DEF_GUI_DARK]
light = theme.colourThemes[DEF_GUI_LIGHT]
assert dark.name == "Default Dark Theme"
assert dark.dark is True
assert light.name == "Default Light Theme"
assert light.dark is False
@pytest.mark.gui
def testGuiTheme_LoadThemes(monkeypatch):
"""Test loading themes."""
theme = GuiTheme()
theme.iconCache = MagicMock()
CONFIG.lightTheme = DEF_GUI_LIGHT
CONFIG.darkTheme = DEF_GUI_DARK
# Load built-in themes
files = []
_listContent(files, CONFIG.assetPath("themes"), ".conf")
theme._scanThemes(files)
assert DEF_GUI_LIGHT in theme._allThemes
assert DEF_GUI_DARK in theme._allThemes
assert theme._currentTheme == ""
# Load light theme
CONFIG.themeMode = nwTheme.LIGHT
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_LIGHT
# Load dark theme
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_DARK
# Let auto switch back to light, then dark
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060500)
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light)
CONFIG.themeMode = nwTheme.AUTO
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_LIGHT
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060500)
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark)
CONFIG.themeMode = nwTheme.AUTO
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_DARK
# Error Cases
# =========== # ===========
# Block the reading of the files # Invalid light theme
CONFIG.lightTheme = "not_a_theme"
CONFIG.themeMode = nwTheme.LIGHT
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_LIGHT
# Invalid dark theme
CONFIG.darkTheme = "not_a_theme"
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_DARK
# Clear meta and check exit early cases
theme._meta = ThemeMeta()
assert theme._meta.name == ""
# Reload dark should not load anything
CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme()
assert theme._meta.name == ""
# Force reload, but fail parsing
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr(ConfigParser, "read", causeOSError)
assert mainTheme.listThemes() == [] CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme(force=True)
assert theme._meta.name == ""
# Load the theme info, default themes first # Invalid theme, and defaults are missing
themesList = mainTheme.listThemes() del theme._allThemes[DEF_GUI_DARK]
assert themesList[0] == ("default_dark", "Default Dark Theme") del theme._allThemes[DEF_GUI_LIGHT]
assert themesList[1] == ("default_light", "Default Light Theme")
assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
assert themesList[3] == ("dracula", "Dracula")
# A second call should returned the cached list CONFIG.lightTheme = "not_a_theme"
assert mainTheme.listThemes() == mainTheme._themeList CONFIG.themeMode = nwTheme.LIGHT
theme.loadTheme(force=True)
assert theme._meta.name == ""
# Check handling of broken theme settings CONFIG.darkTheme = "not_a_theme"
CONFIG.guiTheme = "not_a_theme" CONFIG.themeMode = nwTheme.DARK
availThemes = mainTheme._availThemes theme.loadTheme(force=True)
mainTheme._availThemes = {} assert theme._meta.name == ""
assert mainTheme.loadTheme() is False
mainTheme._availThemes = availThemes
# Check handling of unreadable file
CONFIG.guiTheme = DEF_GUI
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert mainTheme.loadTheme() is False
# Load Default Theme @pytest.mark.gui
# ================== def testGuiTheme_SpecialColors(tstPaths):
"""Test handling special cases for colours."""
theme = GuiTheme()
theme.iconCache = MagicMock()
if sys.platform != "win32": testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
# Set a mock colour for the window background testTheme.write_text(
mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
# Load the default theme
CONFIG.guiTheme = DEF_GUI
assert mainTheme.loadTheme() is True
# This should load a standard palette
wCol = QPalette().color(QPalette.ColorRole.Window).getRgb()
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol
# Mock Dark Theme
# ===============
mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
mockTheme.write_text(
"[Main]\n" "[Main]\n"
"name = Test\n" "name = Test\n"
"mode = light\n"
"\n"
"[Base]\n"
"default = #cccccc\n"
"faded = #949494\n"
"red = #ff0000\n"
"orange = #ff7f00\n"
"yellow = #ffff00\n"
"green = #00ff00\n"
"cyan = #00ffff\n"
"blue = #0000ff\n"
"purple = #ff00ff\n"
"\n"
"[Project]\n"
"root = blue\n"
"folder = yellow\n"
"file = default\n"
"title = green\n"
"chapter = red\n"
"scene = blue\n"
"note = yellow\n"
"\n" "\n"
"[Palette]\n" "[Palette]\n"
"window = 0, 0, 0\n" "window = #000000\n"
"text = 255, 255, 255\n" "text = #ffffff\n"
) )
mainTheme._availThemes["test"] = mockTheme theme._scanThemes([testTheme])
assert len(theme.colourThemes) == 1
CONFIG.themeMode = nwTheme.LIGHT
CONFIG.lightTheme = "test"
CONFIG.guiTheme = "test" # Load theme
assert mainTheme.loadTheme() is True theme.loadTheme()
assert mainTheme._guiPalette.window().color().getRgb() == (0, 0, 0, 255)
assert mainTheme._guiPalette.text().color().getRgb() == (255, 255, 255, 255)
assert mainTheme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
assert mainTheme.isDarkTheme is True
# Load Default Light Theme # Since window is black, a lighter version should be generated
# ======================== assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
CONFIG.guiTheme = "default_light" # Reload with project override to red
assert mainTheme.loadTheme() is True CONFIG.iconColTree = "red"
CONFIG.iconColDocs = True
theme.loadTheme(force=True)
# Check a few values assert theme.getBaseColor("root").getRgb() == (255, 0, 0, 255)
assert mainTheme._guiPalette.color( assert theme.getBaseColor("folder").getRgb() == (255, 0, 0, 255)
QPalette.ColorRole.Window assert theme.getBaseColor("file").getRgb() == (204, 204, 204, 255)
).getRgb() == (239, 239, 239, 255) assert theme.getBaseColor("title").getRgb() == (0, 255, 0, 255)
assert mainTheme._guiPalette.color( assert theme.getBaseColor("chapter").getRgb() == (255, 0, 0, 255)
QPalette.ColorRole.WindowText assert theme.getBaseColor("scene").getRgb() == (0, 0, 255, 255)
).getRgb() == (0, 0, 0, 255) assert theme.getBaseColor("note").getRgb() == (255, 255, 0, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.Base
).getRgb() == (255, 255, 255, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.AlternateBase
).getRgb() == (224, 224, 224, 255)
# Load Default Dark Theme assert theme.getRawBaseColor("root") == b"#ff0000"
# ======================= assert theme.getRawBaseColor("folder") == b"#ff0000"
assert theme.getRawBaseColor("file") == b"#cccccc"
CONFIG.guiTheme = "default_dark" assert theme.getRawBaseColor("title") == b"#00ff00"
assert mainTheme.loadTheme() is True assert theme.getRawBaseColor("chapter") == b"#ff0000"
assert theme.getRawBaseColor("scene") == b"#0000ff"
# Check a few values assert theme.getRawBaseColor("note") == b"#ffff00"
assert mainTheme._guiPalette.color(
QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255)
# qtbot.stop()
@pytest.mark.gui
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
"""Test the syntax part of the class."""
mainTheme = SHARED.theme
# List Themes
# ===========
# Block the reading of the files
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert mainTheme.listThemes() == []
# Load the syntax info
syntaxList = mainTheme.listSyntax()
assert syntaxList[0] == ("default_dark", "Default Dark")
assert syntaxList[1] == ("default_light", "Default Light")
# A second call should returned the cached list
assert mainTheme.listSyntax() == mainTheme._syntaxList
# Check handling of broken theme settings
availSyntax = mainTheme._availSyntax
mainTheme._availSyntax = {}
CONFIG.guiSyntax = "not_a_syntax"
assert mainTheme.loadSyntax() is False
mainTheme._availSyntax = availSyntax
# Check handling of unreadable file
CONFIG.guiSyntax = "default_light"
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert mainTheme.loadSyntax() is False
# Load Default Light Syntax
# =========================
# Load the default syntax
CONFIG.guiSyntax = "default_light"
assert mainTheme.loadSyntax() is True
# Check some values
assert mainTheme.syntaxMeta.name == "Default Light"
assert mainTheme.syntaxTheme.back == QColor(255, 255, 255)
assert mainTheme.syntaxTheme.text == QColor(0, 0, 0)
assert mainTheme.syntaxTheme.link == QColor(0, 0, 200)
# Load Default Dark Theme
# =======================
# Load the default syntax
CONFIG.guiSyntax = "default_dark"
assert mainTheme.loadSyntax() is True
# Check some values
assert mainTheme.syntaxMeta.name == "Default Dark"
assert mainTheme.syntaxTheme.back == QColor(42, 42, 42)
assert mainTheme.syntaxTheme.text == QColor(204, 204, 204)
assert mainTheme.syntaxTheme.link == QColor(102, 153, 204)
# qtbot.stop()
@pytest.mark.gui
def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
"""Test the icon cache class."""
iconCache = SHARED.theme.iconCache
# Load Theme
# ==========
# Invalid theme name
availThemes = iconCache._availThemes
iconCache._availThemes = {}
assert iconCache.loadTheme("not_a_theme") is False
iconCache._availThemes = availThemes
# Check handling of unreadable file
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert iconCache.loadTheme("material_rounded_normal") is False
# Load working theme file
assert iconCache.loadTheme("material_rounded_normal") is True
assert iconCache.themeMeta.name == "Material Symbols - Rounded Medium"
# Load with project colour override
purple = iconCache._svgColors["purple"]
assert iconCache._svgColors["root"] != purple
assert iconCache._svgColors["folder"] != purple
assert iconCache._svgColors["file"] != purple
assert iconCache._svgColors["title"] != purple
assert iconCache._svgColors["chapter"] != purple
assert iconCache._svgColors["scene"] != purple
assert iconCache._svgColors["note"] != purple
# Reload with project override to purple, also for docs
CONFIG.iconColTree = "purple" CONFIG.iconColTree = "purple"
assert iconCache.loadTheme("material_rounded_normal") is True CONFIG.iconColDocs = False
assert iconCache._svgColors["root"] == purple theme.loadTheme(force=True)
assert iconCache._svgColors["folder"] == purple
assert iconCache._svgColors["file"] == purple
assert iconCache._svgColors["title"] == purple
assert iconCache._svgColors["chapter"] == purple
assert iconCache._svgColors["scene"] == purple
assert iconCache._svgColors["note"] == purple
# Change some colours assert theme.getBaseColor("root").getRgb() == (255, 0, 255, 255)
iconCache.setIconColor("root", QColor(255, 255, 255)) assert theme.getBaseColor("folder").getRgb() == (255, 0, 255, 255)
assert iconCache._svgColors["root"] != purple assert theme.getBaseColor("file").getRgb() == (255, 0, 255, 255)
assert iconCache._svgColors["root"] == b"#ffffff" assert theme.getBaseColor("title").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("chapter").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("scene").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("note").getRgb() == (255, 0, 255, 255)
# List Themes assert theme.getRawBaseColor("root") == b"#ff00ff"
# =========== assert theme.getRawBaseColor("folder") == b"#ff00ff"
assert theme.getRawBaseColor("file") == b"#ff00ff"
# Load error returns empty list assert theme.getRawBaseColor("title") == b"#ff00ff"
with monkeypatch.context() as mp: assert theme.getRawBaseColor("chapter") == b"#ff00ff"
mp.setattr("builtins.open", causeOSError) assert theme.getRawBaseColor("scene") == b"#ff00ff"
themes = iconCache.listThemes() assert theme.getRawBaseColor("note") == b"#ff00ff"
assert themes == []
# Successful read
themes = iconCache.listThemes()
assert len(themes) > 1
assert "material_rounded_normal" in dict(themes)
# Load error doesn't matter on second read since list is cached
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert iconCache.listThemes() == themes
# qtbot.stop()
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_LoadIcons(qtbot, nwGUI): def testGuiTheme_Methods(monkeypatch):
"""Test other themes methods."""
theme = GuiTheme()
theme.iconCache = MagicMock()
CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.themeMode = nwTheme.DARK
# Init theme
assert theme.colourThemes == {}
theme.initThemes()
assert theme._meta.name == "Default Dark Theme"
# Text width
theme.guiFont = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont)
assert theme.getTextWidth("MMMMM") > theme.getTextWidth("MMM")
font = QFont(theme.guiFont)
font.setPointSizeF(0.5*font.pointSizeF())
assert theme.getTextWidth("MMMMM", font) < theme.getTextWidth("MMMMM")
# Detect desktop mode Qt 6.5+
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060500)
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light)
assert theme.isDesktopDarkMode() is False
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark)
assert theme.isDesktopDarkMode() is True
# Detect desktop mode Qt 6.4
mockWhite = Mock()
mockWhite.color.return_value = QColor(255, 255, 255)
mockBlack = Mock()
mockBlack.color.return_value = QColor(0, 0, 0)
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060400)
mp.setattr(QPalette, "window", lambda *a: mockWhite)
mp.setattr(QPalette, "windowText", lambda *a: mockBlack)
assert theme.isDesktopDarkMode() is False
mp.setattr(QPalette, "window", lambda *a: mockBlack)
mp.setattr(QPalette, "windowText", lambda *a: mockWhite)
assert theme.isDesktopDarkMode() is True
# Stylesheets
assert theme.getStyleSheet(STYLES_FLAT_TABS) != ""
assert theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) != ""
assert theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) != ""
assert theme.getStyleSheet("stuff") == ""
@pytest.mark.gui
def testGuiTheme_ScanIcons(monkeypatch):
"""Test the icon theme scanning."""
theme = GuiTheme()
# Load built-in themes
files = []
_listContent(files, CONFIG.assetPath("icons"), ".icons")
assert len(files) > 0
# Block reading theme files
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
theme.iconCache._scanThemes(files)
assert theme.iconCache.iconThemes == {}
# Read all themes correctly
theme.iconCache._scanThemes(files)
assert len(theme.iconCache.iconThemes) > 0
@pytest.mark.gui
def testGuiTheme_IconThemes(monkeypatch):
"""Test loading icon theme."""
CONFIG.lightTheme = DEF_GUI_LIGHT
CONFIG.themeMode = nwTheme.LIGHT
CONFIG.iconTheme = DEF_ICONS
theme = GuiTheme()
# Init should load default theme
theme.initThemes()
assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium"
assert DEF_ICONS in theme.iconCache.iconThemes
# Load default theme directly
theme.iconCache._meta = ThemeMeta()
theme.iconCache.loadTheme("DEF_ICONS")
assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium"
# Failed loading should load nothing
theme.iconCache._meta = ThemeMeta()
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
theme.iconCache.loadTheme("DEF_ICONS")
assert theme.iconCache._meta.name == ""
# Reload with non-existent theme should reload default
theme.iconCache._meta = ThemeMeta()
theme.iconCache.loadTheme("not_a_theme")
assert theme.iconCache._meta.name == "Material Symbols - Rounded Medium"
# If default theme is missing, load nothing
del theme.iconCache._allThemes[DEF_ICONS]
theme.iconCache._meta = ThemeMeta()
theme.iconCache.loadTheme("not_a_theme")
assert theme.iconCache._meta.name == ""
@pytest.mark.gui
def testGuiTheme_LoadIcons():
"""Test the icon cache class.""" """Test the icon cache class."""
iconCache = SHARED.theme.iconCache theme = GuiTheme()
assert iconCache.loadTheme("material_rounded_normal") is True theme.initThemes()
iconCache = theme.iconCache
# Load Icons # Load Icons
# ========== # ==========
@@ -455,14 +500,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI):
nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0" nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0"
) == iconCache._noIcon ) == iconCache._noIcon
# qtbot.stop()
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI): def testGuiTheme_LoadDecorations(monkeypatch):
"""Test the icon cache class.""" """Test the icon cache class."""
iconCache = SHARED.theme.iconCache theme = GuiTheme()
assert iconCache.loadTheme("material_rounded_normal") is True theme.initThemes()
iconCache = theme.iconCache
# Load Decorations # Load Decorations
# ================ # ================
@@ -519,5 +563,3 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
assert iconCache.getHeaderDecorationNarrow(4) == iconCache._headerDecNarrow[4] assert iconCache.getHeaderDecorationNarrow(4) == iconCache._headerDecNarrow[4]
assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5] assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5]
assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5] assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5]
# qtbot.stop()
+4
View File
@@ -90,6 +90,10 @@ ICONS = [
"sb_search", "sb_search",
"sb_stats", "sb_stats",
"theme_light",
"theme_dark",
"theme_auto",
"add", "add",
"bookmarks", "bookmarks",
"browse", "browse",
+4
View File
@@ -56,6 +56,10 @@
"sb_search": "magnifying-glass", "sb_search": "magnifying-glass",
"sb_stats": "chart-simple", "sb_stats": "chart-simple",
"theme_light": "sun",
"theme_dark": "moon",
"theme_auto": "circle-half-stroke",
"add": "plus", "add": "plus",
"bookmarks": "bookmark", "bookmarks": "bookmark",
"browse": "folder-open", "browse": "folder-open",
+4
View File
@@ -56,6 +56,10 @@
"sb_search": "search", "sb_search": "search",
"sb_stats": "bar_chart", "sb_stats": "bar_chart",
"theme_light": "light_mode",
"theme_dark": "dark_mode",
"theme_auto": "contrast",
"add": "add", "add": "add",
"bookmarks": "bookmarks", "bookmarks": "bookmarks",
"browse": "folder_open", "browse": "folder_open",
+4
View File
@@ -56,6 +56,10 @@
"sb_search": "file-search", "sb_search": "file-search",
"sb_stats": "bar-chart-fill", "sb_stats": "bar-chart-fill",
"theme_light": "sun",
"theme_dark": "moon",
"theme_auto": "contrast",
"add": "add", "add": "add",
"bookmarks": "bookmark", "bookmarks": "bookmark",
"browse": "folder-2", "browse": "folder-2",