Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Support custom TOML themes #286

Merged
merged 37 commits into from
Nov 13, 2023

Conversation

joshuamegnauth54
Copy link
Contributor

@joshuamegnauth54 joshuamegnauth54 commented Jun 26, 2023

Feature: Support custom GUI themes defined in TOML.

themed

Rationale

Many programs support custom themes that are defined in simple file formats, such as TOML. This patch adds a similar feature to Sniffnet by working within its preexisting style and palette infrastructure. Theme designers, such as the individuals who contribute to Catppuccin or Dracula.

Usage

I added a text box to the theme picker to load a custom theme from a TOML file. Iced doesn't have a file dialog yet. The text input box is an imperfect solution but works for now.

unthemed_style_picker
themed_style_picker

A custom theme should be defined in TOML with the following schema:

name = "Catppuccin (Mocha)"

[description]
EN = "Catppuccin is a colorful, medium contrast pastel theme.\nhttps://github.com/catppuccin/catppuccin"
# Contributed by Emilia
HU = "Catpuccin egy színes, közepes kontrasztú, pasztell téma.\nhttps://github.com/catppuccin/catppuccin"
# Contributed by Bartosz
PL = "Catppuccin to kolorowy i pastelowy motyw o średnim kontraście.\nhttps://github.com/catppuccin/catppuccin"

# Color palettes are in RGBA hexadecimal where the alpha is optional.
[palette]
primary = "#1e1e2e"          # Base
secondary = "#89b4fa"        # Blue
buttons = "#313244"          # Surface0
incoming = "#89b4fa"         # Blue
outgoing = "#f5c2e7"         # Pink
text_headers = "#11111b"     # Crust
text_body = "#cdd6f4"        # Text
round_borders = "#74c7ec"    # Sapphire
round_containers = "#585b70" # Surface 2
starred = "#f9e2af"          # Yellow

# Alpha channels are floats within [0.0, 1.0]
badge_alpha = 0.75
color_mix_chart = 0.3

All colors are hexadecimal RGB. Descriptions are multilingual but currently unused in the UI at the moment. The format is really simple and similar to other programs.

Examples of theme files in other open source programs (non-exhaustive)

The example above is located in resources\themes\catppuccin_mocha.toml and uses colors from Catppuccin.

Implementation details

CustomStyle, CustomPalette, and PaletteExtension

I added a new variant to StyleType that holds a CustomStyle struct.

I implemented three new types for custom styles. CustomStyle contains metadata on the theme such as its name and description(s) as well as the palette. CustomPalette contains the colors themselves. I used Sniffnet's pre-existing Palette type as well as an extension type, PaletteExtension, to hold extra colors such as the favorites star.

Palette and PaletteExtension are flattened upon deserialization for ergonomics. A future patch can simplify Sniffnet's styles by unifying both types without affecting any existing themes at all.

Split deserialization

CustomStyle is serialized to and deserialized from a file path. In other words, Sniffnet's config file saves the path to a custom theme to reload it on start rather than serializing the colors.

I implemented a deserializer for iced::Color that constructs it from a hex string.

Two new dependencies (sort of)

I added the toml crate as a dependency and serde-test as a dev-dependency.

The toml crate is already used by confy by default. confy is already used by Sniffnet which means that toml is a transitive dependency.

serde-test is only used in unit tests as a dev-dependency (i.e. it's only compiled and used in tests).

StyleType borrows

StyleType is no longer copyable because CustomStyle isn't Copy. StyleType is wrapped into an Arc which is passed around or cloned as necessary. This is a fairly simple change that doesn't affect Sniffnet itself.

Future work

  • Support file dialogs
  • Add the localized description somewhere in the UI

Let me know what you think for now!

`iced::Color` doesn't implement `serde::Deserialize` nor
`serde::Serialize`. I need `Deserialize` so that custom color schemes
can be deserialized into the palette type used in Sniffnet.

I implemented ser/de to deserialize from a hex string into the float
colors used by `iced`. My reasoning is that hexadecimal colors are
easier for color scheme artists. Anecdotally, I've also noticed
hex colors are the most prevalent for color schemes. Some schemes also
provide RGB, but I haven't encountered float colors yet.

For example:
* [Catppuccin](https://github.com/catppuccin/catppuccin)
* [Dracula](https://draculatheme.com/contribute)
* [gruvbox](https://github.com/morhetz/gruvbox)
* [Nord](https://github.com/nordtheme/nord)
* [Tokyo Night](https://github.com/folke/tokyonight.nvim)
Merge branch 'main' of github.com:joshuamegnauth54/sniffnet into add_themes
Previously, I implemented a remote type to serialize and deserialize
`iced::Color`. That implementation caused some problems when writing the
unit tests because `serde_test` expected the `iced::Color` struct. I
didn't want to overhaul the tests from their concise and more direct
implementations - especially considering I don't really understand the
problem. Anyway, using `deserialize_with` and `serialize_with` is
cleaner than remote derive so this is overall a win.

I implemented `PartialEq` for the custom style's palette's to account
for floating point's imprecision. I also fixed an incorrectly copied
number that caused the unit tests to fail.

With that, deserializing custom styles from TOML seems to work and all
unit tests pass.
- Add support for descriptions in other languages.
- Use the latest `toml` crate because `confy`'s version had a bug that
  my code is hitting. Deserializing the `Language` enum in a TOML table
  failed with an error that claimed that the enum isn't a key. Upgrading
  the `toml` crate solved that problem.
- Clean up TOML theme spec.
Primarily to keep parity with the types that derived Hash before these
patches.
`StyleType` is no longer `Copy` because the enum holds `CustomStyle`.
Iced doesn't provide a file dialog to use because that should be handled
natively or something. See [iced-rs/iced#347].
(Also, remove a debug line).
@GyulyVGC GyulyVGC added the enhancement New feature, request, or improvement label Jun 26, 2023
@GyulyVGC GyulyVGC added this to the v1.3.0 milestone Jun 26, 2023
@GyulyVGC
Copy link
Owner

GyulyVGC commented Jun 26, 2023

Hey, thanks for all the work!

The problems

While I appreciate your effort, I feel that specifying colors through an external file adds an unneeded complexity which goes against Sniffnet's aim to be comfortable to use.
I also wouldn't like to introduce other dependencies only for an aesthetic matter, if possible.
For your information, a color picker for iced exists: https://github.com/iced-rs/iced_aw#color-picker
Even in this case tough, the colors to specify are a lot and it's not so easy for a user to find a combination that actually looks good in every detail, so I'd discard this idea as well.

The solution

What we could do, instead, is providing a more extended set of palettes personally curated by us.
The advantages are many:

  • user doesn't have to waste time choosing colors one by one
  • user will find a combination of colors that looks good in every detail without worrying about particulars such as the badge_alpha or color_mix_chart
  • having many palettes (20+) will satisfy most of the users even if their exact favourite palette is not present

What I suggest is changing the least possible in the app logic, providing new palettes which could be divided by day/night/other criteria.

@joshuamegnauth54
Copy link
Contributor Author

joshuamegnauth54 commented Jun 28, 2023

Hello again and thank you for your time! I should clarify my intent with this patch. I apologize for the confusion.

I don't intend for users to pick colors one by one for their color schemes. Instead, the purpose of this patch is to allow end users to use common themes that are created by groups such as:

Those groups release themes for programs with easily modifiable styles.

For a few examples, I'll refer to alacritty (terminal emulator), bat (a cat type program), and zellij (terminal workspace manager). All three of these support custom themes via simple formats like TOML or kdl.

So, if an alacritty user wants to use the Dracula theme, they can head on over to the Dracula repository and download a small YAML with the theme.

The process is similar for bat. A user who wants to use the Catppuccin theme only has to download the theme file from the Catppuccin repository just like alacritty's.

And of course it's the same for zellij as well as many other programs. Dracula releases its theme for many programs such as kitty, fish, zsh, neovim/vim, Alacritty, VSCode, Steam, zellij, exa, Sway, et cetera. Catppuccin and other themes support a similar number of software.

Thus, my intention is for this patch to provide a commensurate experience with Sniffnet - groups that create themes can easily publish a theme, like Catppuccin, for Sniffnet so that end users can use it easily if they wish.

Dependencies

I completely agree with you on the dependencies issue! However, I should clarify the two dependencies because the situation is not what it seems.

  • The toml crate is used by confy which is used directly by Sniffnet. Thus, toml is already a transitive dependency of Sniffnet; I only exposed it directly.
  • serde-test is an official serde crate for tests. I added this as a dev-dependency for unit tests I wrote, so it's not used in Sniffnet directly.

The crux of this patch is adding an extra variant to the StyleType for custom styles. My PR message over exaggerated its complexity for completion, but it's fairly straightforward.

I hope that clarifies some of your concerns! Thank you for your time whether or not you want this feature.😄 I'm simplifying the code even more in the meantime.

I previously wrote PartialEq implementations for `Color` and structs
that used it, but deriving it seems to work fine now.
@GyulyVGC
Copy link
Owner

GyulyVGC commented Jun 28, 2023

Thanks for the clarifications and for your time as well.
I'm just wondering what the process of obtaining a Catpuccin, Dracula, etc, theme for Sniffnet would look like.
I imagine we should ping them and they hopefully take care of the palette definitions and in-app previews pictures.
Even in that case though, if we had the palettes ready-made by them, it'd be still convenient to directly display them in app.

  • all users (including those not aware of Catpuccin, etc...) would have the possibility of setting them
  • users aware of Catpuccin, etc, will set them without the need of leaving Sniffnet app
  • this without considering the complication introduced by iced missing file dialog
  • the apps/IDE/terminals supporting those themes seems to be lower-level applications wrt higher-level interfaces as Sniffnet, for which this approach is not so usual

You pointed out a really interesting topic and at this point I'm interested in having Catpuccin and other well-established themes for Sniffnet.
Once we have them, I'm still of the idea that having them directly in-app would be the best solution.
Instead of dividing themes by day/night we could divide them by provider (i.e., having Sniffnet original themes, themes provided by Dracula, by Catpuccin and so on).

Next steps

Since I'm planning to include this kind of feature in v1.3 (which will land not before some months) I think we should have time to obtain enough themes from those groups.
If you want to help in the process, feel free to do it.
These are roughly the steps I'd like to take:

  • in the meantime, we could ask those groups to support Sniffnet
  • once we have some palettes, if you want you can create a new branch to introduce them in a style similar to the current ones (that page should become a scrollable to have enough space and the palettes from each group could be hidden inside a kind of popup menu as I already do in the page of notification settings)
  • you can keep your current branch, which will be possibly merged once Iced has a proper file selection dialog (your current solution could be useful in the future for users who want a theme that's not supported)
  • now, palettes previews are realised with SVG, but in order to not generate a new SVG for every theme, we should create them with iced Canvas widget (the color in order are: primary, secondary, outgoing, and buttons)

@joshuamegnauth54
Copy link
Contributor Author

I'll have to look into the process of obtaining themes from those groups. As for your other concern, you can definitely distribute those themes with Sniffnet to make it easier for end users.

For example, Pygments includes more than 20 built in styles including Dracula and Monokai.

Zellij has a page that notes that the maintainers accept theme contributions. The built in themes are located in the zellij repository.

Bat also distributes built in themes in a similar manner.

I think your ideas are awesome and very useful. Sniffnet will have (as you said) both the original built in themes as well as a set of common themes. It will also have the ability to load themes that aren't distributed with Sniffnet. We can still split the additional schemes by Day and Night too.

As for the next steps, I'll definitely stick around to maintain and improve this feature. I'll start working on the improvements listed soon.

@GyulyVGC
Copy link
Owner

Thanks so much for your ideas and help!

`StyleType` should serialize as a TOML table to account for
`StyleType::Custom` taking a value.

I added two attributes to `StyleType` to hint that the `toml` crate
should serialize it as a table instead of a simple key = value.
@GyulyVGC
Copy link
Owner

GyulyVGC commented Jul 3, 2023

  • now, palettes previews are realised with SVG, but in order to not generate a new SVG for every theme, we should create them with iced Canvas widget (the color in order are: primary, secondary, outgoing, and buttons)

This is just to inform you that I already solved the problem of generating palettes manually instead of using SVGs.

Oh, I also removed the incoming color from the palette, since it should be always equal to secondary.

@joshuamegnauth54
Copy link
Contributor Author

Thank you! I'll merge this in and get back to working on it within a day or two.

@joshuamegnauth54
Copy link
Contributor Author

joshuamegnauth54 commented Jul 28, 2023

Hey! Sorry for disappearing for a few weeks. I had to repair my aging computer, but I'm back now to work on this.

sniffnet

I added a scrollbar to the theme picker. Custom themes are loaded from the configdir/themes. You can bundle a few common and maintained themes as well.

I wrote a few example themes using Catppuccin and Dracula's specs as examples to show that it works.

Should I remove the text box I originally added to load themes?

Edit: To clarify, the original themes are at the top of the scrollbar. Extra themes are added below those. I made sure that it works with an odd number of extra themes too.

@GyulyVGC
Copy link
Owner

Hey! Don't worry at all about delays!

I think it'd be better to open a separate PR from a different branch if we want to add some of the new themes in a reasonable amount of time.

As I've anticipated, I'd prefer avoiding the management of external files until a proper file dialog is supported.

Consequently, the idea of a new branch and a separate PR to not waste anyway the job you did so far.

Ideally, we should include in the new PR only the following parts:

  • the new palettes of colour, simply defined as Palette structs, as the original ones
  • the graphical modifications to the settings styling page (scrollable and the new items representing the palettes)

Again, thanks a lot for all the work!

use super::palette::{Palette, PaletteExtension};
use crate::Language;

/// Custom color scheme data including the palette, name, and location of the toml.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I have a question before I start. I implemented a CustomStyle struct that holds the name of the style, the translated descriptions, and the Palette plus the extra yellow color.

This struct made it easier to define themes without having to extend functions like get_colors for each theme.

Original:

pub fn get_colors(style: StyleType) -> Palette {
    match style {
        StyleType::Night => NIGHT_STYLE,
        StyleType::Day => DAY_STYLE,
        StyleType::DeepSea => DEEP_SEA_STYLE,
        StyleType::MonAmour => MON_AMOUR_STYLE,
    }
}

How it looks in my PR:

pub fn get_colors(style: &StyleType) -> &Palette {
    match style {
        StyleType::Night => &NIGHT_STYLE,
        StyleType::Day => &DAY_STYLE,
        StyleType::DeepSea => &DEEP_SEA_STYLE,
        StyleType::MonAmour => &MON_AMOUR_STYLE,
        StyleType::Custom(style) => &style.palette.base,
    }
}

Another example.
Original:

pub fn get_starred_color(style: StyleType) -> Color {
    match style {
        StyleType::Night | StyleType::DeepSea => Color {
            r: 245.0 / 255.0,
            g: 193.0 / 255.0,
            b: 39.0 / 255.0,
            a: 0.5,
        },
        StyleType::Day | StyleType::MonAmour => Color {
            r: 245.0 / 255.0,
            g: 193.0 / 255.0,
            b: 39.0 / 255.0,
            a: 0.8,
        },
    }
}

How it looks in my PR:

pub fn get_starred_color(style: &StyleType) -> Color {
    match style {
        StyleType::Night | StyleType::DeepSea => Color {
            r: 245.0 / 255.0,
            g: 193.0 / 255.0,
            b: 39.0 / 255.0,
            a: 0.5,
        },
        StyleType::Day | StyleType::MonAmour => Color {
            r: 245.0 / 255.0,
            g: 193.0 / 255.0,
            b: 39.0 / 255.0,
            a: 0.8,
        },
        StyleType::Custom(style) => style.palette.extension.starred,
    }
}

In other words, the struct encapsulates the palette plus the metadata for the theme. Functions take &Arc<StyleType> or &StyleType instead of StyleType.

Should I port that over to the new PR or just extend the matches with new themes as in the original code?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Arc should not be needed for the moment.

You can also forget about the palettes' descriptions since I'd like to include them just for the original themes, to not make complex getting all the translations from native speaker and because I'll display the additional palettes in a more condensed form.
The palette name is enough.

And we don't even need the path at this point.

I think the best thing to do is not keeping CustomStyle, but only keeping CustomPalette.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I know that's a lot limited with respect to all the work you did, but I think it's a good compromise given the simplicity of this solution and the fact that without a file dialog is not comfortable to include external palettes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought I responded to this. 😅

It's no problem! A minimal, working custom styles implementation is best for now. Plus, I learned a lot anyway. I mostly finished the patch so I'll open a PR now.

@joshuamegnauth54 joshuamegnauth54 marked this pull request as draft August 2, 2023 04:56
@GyulyVGC GyulyVGC mentioned this pull request Sep 2, 2023
The newer implementation is much simpler, so only some of the code needs
to be ported over. This is a WIP. The old tests don't work yet.
The least invasive way of supporting custom TOML themes is to add a new
variant to `ExtraStyles`. I implemented `Hash` for `CustomPalette`
because it's not derivable for `Palette`. Finally, indicating whether a
style is nightly must be stored in the palette extensions struct
because I can't hard code it (the old solution for the built in extra styles).
(Message not implemented yet)
- The custom theme text box is highlighted in red if the path doesn't
  exist or if it doesn't have a `.toml` extension.
- The last set theme takes prescedence instead of custom themes always
  clobbering the set theme.
- Fix all old TOML themes.
@joshuamegnauth54
Copy link
Contributor Author

I'm mostly finished porting over the old code from this PR. Custom themes work, so you can test it if you want. 😺

I have to remove some unused functions and fix my old unit tests, but you can let me know what else I should do with the code committed so far.

@GyulyVGC GyulyVGC changed the base branch from main to advanced-settings October 1, 2023 09:43
@GyulyVGC
Copy link
Owner

GyulyVGC commented Oct 1, 2023

I'm mostly finished porting over the old code from this PR. Custom themes work, so you can test it if you want. 😺

I have to remove some unused functions and fix my old unit tests, but you can let me know what else I should do with the code committed so far.

Tyvm! I'll let you know as soon as I have time to check your PR 👍

@joshuamegnauth54 joshuamegnauth54 marked this pull request as ready for review October 2, 2023 07:22
@GyulyVGC GyulyVGC added the design Styling and appearance label Nov 10, 2023
README.md Outdated Show resolved Hide resolved
resources/themes/dracula.toml Outdated Show resolved Hide resolved
@GyulyVGC
Copy link
Owner

I've fixed some aspects and I think we are finally ready to merge this.

Most notably, I didn't like that the input box for the custom style was emptied when selecting one of the default themes.
To avoid this while correctly remembering the last theme selection, I decided to serialise the custom palette into the style field of ConfigSettings.
In this way, if a user chooses one of the default themes after having specified a file path, the default theme will be remembered when reopening the app and the file path will be still there.
Moreover, the custom theme will just be read from the config instead of being loaded again from the file.

Other than that I just changed minor aspects.

I really liked your implementation.
Thank you so much for the amazing work @joshuamegnauth54 🙏

@GyulyVGC GyulyVGC merged commit 06ae195 into GyulyVGC:advanced-settings Nov 13, 2023
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design Styling and appearance enhancement New feature, request, or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants