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

Use color scheme based on OS system dark/light mode preferences #1233

Merged
merged 8 commits into from
Oct 10, 2023
2 changes: 1 addition & 1 deletion .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ IncludeCategories:
Priority: 52
- Regex: '^<harfbuzz/'
Priority: 53
- Regex: '^<(QtCore|QtGui|QtWidgets|QtQml|QtQuick|QtNetwork|QtMultimedia)/'
- Regex: '^<(QtCore|QtGui|QtDBus|QtWidgets|QtQml|QtQuick|QtNetwork|QtMultimedia)/'
Priority: 64
- Regex: '^<catch2/'
Priority: 70
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ _deps/**
out
out/**
CMakeSettings.json
vcpkg_installed/**

# VIM: code completion / IntelliSense
.clangd
Expand Down
16 changes: 15 additions & 1 deletion docs/configuration/profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,13 +363,27 @@ profiles:


### `colors`
section in the configuration file allows you to specify the colorscheme to use for the terminal. Alternatively, you can inline the color definitions within this section.
section in the configuration file allows you to specify the colorscheme to use for the terminal.
``` yaml
profiles:
profile_name:
colors: "default"
```

To make the terminal's color scheme dependant on OS appearance (dark and light mode) settings,
you need to specify two color schemes:

```yaml
profiles:
profile_name:
colors:
dark: "some_dark_scheme_name"
light: "some_light_scheme_name"
```

With this, the terminal will use the color scheme as specified in `dark` when OS dark mode is on,
and `light`'s color scheme otherwise.

### `hyperlink_decoration:`
section in the configuration file allows you to configure the styling and colorization of hyperlinks when they are displayed in the terminal and when they are hovered over by the cursor.
``` yaml
Expand Down
40 changes: 40 additions & 0 deletions docs/vt-extensions/color-palette-update-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Dark and Light Mode detection

Most modern operating systems and desktop environments do support Dark and Light themes,
this includes at least MacOS, Windows, KDE Plasma, Gnome, and probably others.

Some even support switching from dark to light and light to dark mode based on sun rise / sun set.

In order to not make the terminal emulator look bad after such switch, we must
enable the applications inside the terminal to detect when the terminal has
updated the color palette. This may happen either due to the operaging system having
changed the current theme or simply because the user has explicitly requested to
reconfigure the currently used theme.

Ideally we are getting CLI tools like [delta]() to query the theme mode before sending out RGB values
to the terminal to make the output look more in line with the rest of the desktop.

But also TUIs like vim should be able to reflect dark/light mode changes as soon as the
desktop has charnged from light to dark and vice versa, e.g. to make it more pleasing to the eyes.

## Query the current theme mode?

Send `CSI ? 996 n` to the terminal to explicitly request the current
color preference (dark mode or light mode) by the operating system.

The terminal will reply back in either of the two ways:

VT sequence | description
------------------|---------------------------------
`CSI ? 997 ; 1 n` | DSR reply to indicate dark mode
`CSI ? 997 ; 2 n` | DSR reply to indicate light mode

## Request unsolicited DSR on color palette updates

Send `CSI ? 2031 h` to the terminal to enable unsolicited DSR (device status report) messages
for color palette updates and `CSI ? 2031 l` respectively to disable it again.

The sent out DSR looks equivalent to the already above mentioned.
This notification is not just sent when dark/light mode has been changed
by the operating system / desktop, but also if the user explicitly changed color scheme,
e.g. by configuration.
4 changes: 4 additions & 0 deletions metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@
<li>Do not clear search term when entering search editor again.</li>
<li>Clear search term when switch to insert vi mode (#1135)</li>
<li>Delete dpi_scale entry in configuration (#1137)</li>
<li>Removes the ability to inline colorschemes within a configuration profile. Colorschemes must now always be referenced by their name.</li>
<li>Adds the ability to chose a color scheme based on the operating systems's dark/light mode setting. This will change live whenever the OS's dark/light mode setting changes as well (#604).</li>
<li>Adds VT sequence DECSSCLS (change scroll speed) and properly handle DECSCLM (enable slow scrolling mode) (#1204)</li>
<li>Adds VT sequence parameter ?996 to DSR to request a report of current color scheme dark/light mode hint.</li>
<li>Adds VT sequence `SM ?2031` and `RM ?2031` to enable/disable unsolicited DSR for color scheme updates by the user or OS.</li>
<li>Adds percentage value to Indicator Statusline to indicate scroll offset in scrollback buffer.</li>
<li>Adds inheritance of profiles in configuration file based on default profile (#1063).</li>
<li>Adds config option `profile.*.bell` to adjust BEL behavior and fixes (#1162) and (#1163).</li>
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ nav:
- vt-extensions/index.md
- vt-extensions/clickable-links.md
- vt-extensions/vertical-line-marks.md
- vt-extensions/color-palette-update-notifications.md
- vt-extensions/synchronized-output.md
- vt-extensions/buffer-capture.md
- vt-extensions/font-settings.md
Expand Down
1 change: 0 additions & 1 deletion src/contour/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ target_compile_definitions(contour PRIVATE
)

set_target_properties(contour PROPERTIES AUTOMOC ON)

set_target_properties(contour PROPERTIES AUTORCC ON)

# Disable all deprecated Qt functions prior to Qt 6.0
Expand Down
93 changes: 55 additions & 38 deletions src/contour/Config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,6 @@ namespace
{
if (!node)
return;
;

usedKeys.emplace(basePath);
using vtbackend::RGBColor;
Expand Down Expand Up @@ -1159,6 +1158,37 @@ namespace
return colors;
}

vtbackend::ColorPalette loadColorSchemeByName(
UsedKeys& usedKeys,
string const& path,
YAML::Node const& nameNode,
unordered_map<string, vtbackend::ColorPalette> const& colorschemes,
logstore::message_builder& logger)
{
auto const name = nameNode.as<string>();
if (auto i = colorschemes.find(name); i != colorschemes.end())
{
usedKeys.emplace(path);
return i->second;
}

for (fs::path const& prefix: configHomes("contour"))
{
auto const filePath = prefix / "colorschemes" / (name + ".yml");
auto fileContents = readFile(filePath);
if (!fileContents)
continue;
YAML::Node subDocument = YAML::Load(fileContents.value());
UsedKeys usedColorKeys;
auto colors = loadColorScheme(usedColorKeys, "", subDocument);
// TODO: Check usedColorKeys for validity.
configLog()("Loaded colors from {}.", filePath.string());
return colors;
}
logger("Could not open colorscheme file for \"{}\".", name);
return vtbackend::ColorPalette {};
}

void softLoadFont(UsedKeys& usedKeys,
string_view basePath,
YAML::Node const& node,
Expand Down Expand Up @@ -1301,40 +1331,29 @@ namespace
unordered_map<string, vtbackend::ColorPalette> const& colorschemes,
logstore::message_builder logger)
{

if (auto colors = profile["colors"]; colors) // {{{
{
usedKeys.emplace(fmt::format("{}.{}.colors", parentPath, profileName));
auto const path = fmt::format("{}.{}.{}", parentPath, profileName, "colors");
if (colors.IsMap())
terminalProfile.colors = loadColorScheme(usedKeys, path, colors);
else if (auto i = colorschemes.find(colors.as<string>()); i != colorschemes.end())
{
usedKeys.emplace(path);
terminalProfile.colors = i->second;
terminalProfile.colors =
DualColorConfig { .darkMode = loadColorSchemeByName(
usedKeys, path + ".dark", colors["dark"], colorschemes, logger),
.lightMode = loadColorSchemeByName(
usedKeys, path + ".light", colors["light"], colorschemes, logger) };
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
errorLog()("Dual color scheme is not supported by your local Qt version. "
"Falling back to single color scheme.");
#endif
}
else if (colors.IsScalar())
{
bool found = false;
for (fs::path const& prefix: configHomes("contour"))
{
auto const filePath = prefix / "colorschemes" / (colors.as<string>() + ".yml");
auto fileContents = readFile(filePath);
if (!fileContents)
continue;
YAML::Node subDocument = YAML::Load(fileContents.value());
UsedKeys usedColorKeys;
terminalProfile.colors = loadColorScheme(usedColorKeys, "", subDocument);
// TODO: Check usedColorKeys for validity.
configLog()("Loaded colors from {}.", filePath.string());
found = true;
break;
}
if (!found)
logger("Could not open colorscheme file for \"{}\".", colors.as<string>());
terminalProfile.colors =
SimpleColorConfig { loadColorSchemeByName(usedKeys, path, colors, colorschemes, logger) };
}
else
logger("scheme '{}' not found.", colors.as<string>());
logger("Invalid colors value.");
}
else
logger("No colors section in profile {} found.", profileName);
Expand Down Expand Up @@ -1372,12 +1391,18 @@ namespace
"size_indicator_on_resize",
terminalProfile.sizeIndicatorOnResize,
logger);
tryLoadChildRelative(usedKeys,
profile,
basePath,
"draw_bold_text_with_bright_colors",
terminalProfile.colors.useBrightColors,
logger);
bool useBrightColors = false;
tryLoadChildRelative(
usedKeys, profile, basePath, "draw_bold_text_with_bright_colors", useBrightColors, logger);

if (auto* simple = get_if<SimpleColorConfig>(&terminalProfile.colors))
simple->colors.useBrightColors = useBrightColors;
else if (auto* dual = get_if<DualColorConfig>(&terminalProfile.colors))
{
dual->darkMode.useBrightColors = useBrightColors;
dual->lightMode.useBrightColors = useBrightColors;
}

tryLoadChildRelative(usedKeys, profile, basePath, "wm_class", terminalProfile.wmClass, logger);

if (auto args = profile["arguments"]; args && args.IsSequence())
Expand Down Expand Up @@ -2026,19 +2051,11 @@ void loadConfigFromFile(Config& config, fs::path const& fileName)
if (auto colorschemes = doc["color_schemes"]; colorschemes)
{
usedKeys.emplace("color_schemes");
// load default colorschemes
const std::string nameDefault = "default";
auto const pathDefault = "color_schemes." + nameDefault;
config.colorschemes[nameDefault] =
loadColorScheme(usedKeys, pathDefault, colorschemes.begin()->second);

for (auto i = colorschemes.begin(); i != colorschemes.end(); ++i)
{
auto const name = i->first.as<string>();
if (name == nameDefault)
continue;
auto const path = "color_schemes." + name;
config.colorschemes[name] = config.colorschemes[nameDefault];
updateColorScheme(config.colorschemes[name], usedKeys, path, i->second);
}
}
Expand Down
15 changes: 14 additions & 1 deletion src/contour/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@ struct InputModeConfig
CursorConfig cursor;
};

struct DualColorConfig
{
vtbackend::ColorPalette darkMode {};
vtbackend::ColorPalette lightMode {};
};

struct SimpleColorConfig
{
vtbackend::ColorPalette colors {};
};

using ColorConfig = std::variant<SimpleColorConfig, DualColorConfig>;

struct TerminalProfile
{
vtpty::Process::ExecInfo shell;
Expand Down Expand Up @@ -168,7 +181,7 @@ struct TerminalProfile
} permissions;

bool drawBoldTextWithBrightColors = false;
vtbackend::ColorPalette colors {};
ColorConfig colors = SimpleColorConfig {};

vtbackend::LineCount modalCursorScrollOff { 8 };

Expand Down
24 changes: 24 additions & 0 deletions src/contour/ContourGuiApp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@
#include <crispy/utils.h>

#include <QtCore/QProcess>
#if !defined(__APPLE__) && !defined(_WIN32)
#include <QtDBus/QDBusConnection>
#endif
#include <QtDBus/QtDBus>
#include <QtGui/QGuiApplication>
#include <QtGui/QStyleHints>
#include <QtGui/QSurfaceFormat>
#include <QtQml/QQmlApplicationEngine>
#include <QtQml/QQmlContext>
Expand Down Expand Up @@ -347,6 +352,25 @@ int ContourGuiApp::terminalGuiAction()
// NB: We use QApplication over QGuiApplication because we want to use SystemTrayIcon.
QApplication app(qtArgsCount, (char**) qtArgsPtr.data());

#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
_colorPreference = QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark
? vtbackend::ColorPreference::Dark
: vtbackend::ColorPreference::Light;

displayLog()("Color theme mode at startup: {}", _colorPreference);

connect(QGuiApplication::styleHints(), &QStyleHints::colorSchemeChanged, [&](Qt::ColorScheme newScheme) {
auto const newValue = newScheme == Qt::ColorScheme::Dark ? vtbackend::ColorPreference::Dark
: vtbackend::ColorPreference::Light;
if (_colorPreference == newValue)
return;

_colorPreference = newValue;
displayLog()("Color preference changed to {} mode\n", _colorPreference);
sessionsManager().updateColorPreference(_colorPreference);
});
#endif

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Enforce OpenGL over any other. As much as I'd love to provide other backends, too.
// We currently only support OpenGL.
Expand Down
10 changes: 7 additions & 3 deletions src/contour/ContourGuiApp.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
#include <contour/Config.h>
#include <contour/ContourApp.h>
#include <contour/TerminalSessionManager.h>
#include <contour/helper.h>

#include <vtpty/Process.h>

#include <QtDBus/QDBusVariant>
#include <QtQml/QQmlApplicationEngine>

#include <filesystem>
#include <list>
#include <memory>
#include <optional>
#include <string_view>

namespace contour
{
Expand Down Expand Up @@ -55,7 +55,7 @@ class ContourGuiApp: public QObject, public ContourApp
{
if (const auto* const profile = config().profile(profileName()))
return *profile;
fmt::print("Failed to access config profile.\n");
displayLog()("Failed to access config profile.");
Require(false);
}

Expand All @@ -69,6 +69,8 @@ class ContourGuiApp: public QObject, public ContourApp

[[nodiscard]] static QUrl resolveResource(std::string_view path);

vtbackend::ColorPreference colorPreference() const noexcept { return _colorPreference; }

private:
static void ensureTermInfoFile();
bool loadConfig(std::string const& target);
Expand All @@ -82,6 +84,8 @@ class ContourGuiApp: public QObject, public ContourApp
char const** _argv = nullptr;
std::optional<vtpty::Process::ExitStatus> _exitStatus;

vtbackend::ColorPreference _colorPreference = vtbackend::ColorPreference::Dark;

std::unique_ptr<QQmlApplicationEngine> _qmlEngine;
};

Expand Down
Loading
Loading