What changed, and why it matters
This commit adjusts color values in the Krux device's user interface themes to improve text and icon contrast. It is purely a visual accessibility/usability fix and does not change any security-sensitive logic, cryptography, input handling, or network behavior.
No security action needed. Treat as a normal UI/accessibility improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch modifies theme color definitions in src/krux/themes.py (adding darker variants of green, red, pink, and orange), updates src/krux/pages/utils.py to use darker network indicator colors only in the Light theme, and adds unit tests verifying WCAG-style contrast ratios. The changes are cosmetic and aimed at readability on small e-ink/LCD screens.
Changed components
src/krux/themes.pysrc/krux/pages/utils.pytests/test_display.pytests/test_themes.pyInspect captured patch +198 / −22
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67224a7..5141c97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ Switch from the pure-Python urtypes and foundation-ur-py packages to the new uUR
- Added `flash_success` method to standardize green success flashes across confirmation screens
- Update Embit to latest - 3ae0ef2
- Replace custom quirc (library to decode QR codes from images) with k_quirc
+- Fix default theme contrast failures for Light, CypherPink, network
+ indicators, and Amigo info panels
# Changelog 26.04.0 - April 2025
diff --git a/src/krux/pages/utils.py b/src/krux/pages/utils.py
index 73447e8..50ebf9c 100644
--- a/src/krux/pages/utils.py
+++ b/src/krux/pages/utils.py
@@ -189,6 +189,14 @@ class Utils(Page):
@staticmethod
def get_network_color(network_name: str):
"""Returns the correct theme color to write network"""
- from ..themes import TEST_TXT_COLOR, MAIN_TXT_COLOR
+ from ..krux_settings import Settings, ThemeSettings
+ from ..themes import (
+ DARKERGREEN,
+ DARKERORANGE,
+ MAIN_TXT_COLOR,
+ TEST_TXT_COLOR,
+ )
+ if Settings().appearance.theme == ThemeSettings.LIGHT_THEME_NAME:
+ return DARKERORANGE if network_name == "Mainnet" else DARKERGREEN
return MAIN_TXT_COLOR if network_name == "Mainnet" else TEST_TXT_COLOR
diff --git a/src/krux/themes.py b/src/krux/themes.py
index cc10867..1964b09 100644
--- a/src/krux/themes.py
+++ b/src/krux/themes.py
@@ -34,12 +34,16 @@ DARKWHITE = 0x1CE7
WHITE = 0xFFFF
GREEN = 0xE007
DARKGREEN = 0x8005
+DARKERGREEN = 0x4004
RED = 0x00F8
+DARKERRED = 0x00C0
LIGHT_PINK = 0xDFFC
PINK = 0x1FF8
+DARKPINK = 0x1AD0
PURPLE = 0x0F78
ORANGE = 0x20FD
DARKORANGE = 0xA0CA
+DARKERORANGE = 0xE0B2
YELLOW = 0x85F6
BLUE = 0xF800
LIGHTBLUE = 0xBD0E
@@ -67,13 +71,13 @@ THEMES = {
"background": WHITE,
"info_background": DARKWHITE,
"foreground": BLACK,
- "frame": LIGHTGREY,
+ "frame": DARKGREY,
"disabled": DARKWHITE,
- "go": DARKGREEN,
+ "go": DARKERGREEN,
"esc_no": RED,
"del": DARKORANGE,
"toggle": BLUE,
- "error": RED,
+ "error": DARKERRED,
"highlight": BLUE,
},
ThemeSettings.ORANGE_THEME_NAME: {
@@ -93,7 +97,7 @@ THEMES = {
"background": BLACK,
"info_background": LIGHTBLACK,
"foreground": LIGHT_PINK,
- "frame": PURPLE,
+ "frame": DARKPINK,
"disabled": DARKGREY,
"go": PINK,
"esc_no": RED,
diff --git a/tests/test_display.py b/tests/test_display.py
index a814c52..8d1fae2 100644
--- a/tests/test_display.py
+++ b/tests/test_display.py
@@ -843,7 +843,8 @@ def test_draw_hcentered_text_on_inverted_display(mocker, amigo):
def test_draw_infobox(mocker, amigo):
from krux.display import Display, DEFAULT_PADDING, FONT_HEIGHT, FONT_WIDTH
- from krux.themes import WHITE, BLACK, DARKGREY
+ from krux.krux_settings import Settings
+ from krux.themes import WHITE, BLACK, THEMES, theme
mocker.patch("krux.display.lcd", new=mocker.MagicMock())
mocker.patch("krux.display.lcd.string_width_px", side_effect=string_width_px)
@@ -853,23 +854,39 @@ def test_draw_infobox(mocker, amigo):
mocker.spy(d, "fill_rectangle")
mocker.spy(d, "draw_string")
- d.draw_hcentered_text("Hello world", DEFAULT_PADDING, WHITE, BLACK, info_box=True)
-
- d.fill_rectangle.assert_called_with(
- DEFAULT_PADDING - 3,
- DEFAULT_PADDING - 1,
- d.width() - 2 * DEFAULT_PADDING + 6,
- FONT_HEIGHT + 2,
- DARKGREY,
- FONT_WIDTH,
- )
- d.draw_string.assert_called_with(
- (d.width() - len("Hello world") * FONT_WIDTH) // 2,
- DEFAULT_PADDING,
- "Hello world",
- WHITE,
- DARKGREY,
+ cases = (
+ "Dark",
+ "Light",
+ "Orange",
+ "CypherPink",
+ "CypherPunk",
)
+ for theme_name in cases:
+ Settings().appearance.theme = theme_name
+ theme.update()
+ info_bg_color = THEMES[theme_name]["disabled"]
+
+ d.fill_rectangle.reset_mock()
+ d.draw_string.reset_mock()
+ d.draw_hcentered_text(
+ "Hello world", DEFAULT_PADDING, WHITE, BLACK, info_box=True
+ )
+
+ d.fill_rectangle.assert_called_with(
+ DEFAULT_PADDING - 3,
+ DEFAULT_PADDING - 1,
+ d.width() - 2 * DEFAULT_PADDING + 6,
+ FONT_HEIGHT + 2,
+ info_bg_color,
+ FONT_WIDTH,
+ )
+ d.draw_string.assert_called_with(
+ (d.width() - len("Hello world") * FONT_WIDTH) // 2,
+ DEFAULT_PADDING,
+ "Hello world",
+ WHITE,
+ info_bg_color,
+ )
def test_draw_centered_text(mocker, m5stickv):
diff --git a/tests/test_themes.py b/tests/test_themes.py
new file mode 100644
index 0000000..16c382e
--- /dev/null
+++ b/tests/test_themes.py
@@ -0,0 +1,145 @@
+import pytest
+
+
+@pytest.fixture
+def rgb565_to_rgb():
+ def _calc(color):
+ color = ((color & 0xFF) << 8) | ((color >> 8) & 0xFF)
+ return (
+ ((color >> 11) & 0x1F) * 255 / 31,
+ ((color >> 5) & 0x3F) * 255 / 63,
+ (color & 0x1F) * 255 / 31,
+ )
+
+ return _calc
+
+
+@pytest.fixture
+def relative_luminance_component():
+ def _calc(value):
+ value = value / 255
+ if value <= 0.03928:
+ return value / 12.92
+ return ((value + 0.055) / 1.055) ** 2.4
+
+ return _calc
+
+
+@pytest.fixture
+def relative_luminance(rgb565_to_rgb, relative_luminance_component):
+ def _calc(color):
+ red, green, blue = rgb565_to_rgb(color)
+ return (
+ 0.2126 * relative_luminance_component(red)
+ + 0.7152 * relative_luminance_component(green)
+ + 0.0722 * relative_luminance_component(blue)
+ )
+
+ return _calc
+
+
+@pytest.fixture
+def contrast_ratio(relative_luminance):
+ def _calc(color_a, color_b):
+ luminance_a = relative_luminance(color_a)
+ luminance_b = relative_luminance(color_b)
+ lighter = max(luminance_a, luminance_b)
+ darker = min(luminance_a, luminance_b)
+ return (lighter + 0.05) / (darker + 0.05)
+
+ return _calc
+
+
+def test_text_and_status_colors_meet_normal_text_contrast(amigo, contrast_ratio):
+ from krux.themes import THEMES
+
+ cases = (
+ ("Dark", "foreground", "background"),
+ ("Dark", "foreground", "info_background"),
+ ("Dark", "go", "background"),
+ ("Dark", "error", "background"),
+ ("Light", "foreground", "background"),
+ ("Light", "foreground", "info_background"),
+ ("Light", "go", "background"),
+ ("Light", "error", "background"),
+ ("Orange", "foreground", "background"),
+ ("Orange", "foreground", "info_background"),
+ ("Orange", "go", "background"),
+ ("Orange", "error", "background"),
+ ("CypherPink", "foreground", "background"),
+ ("CypherPink", "foreground", "info_background"),
+ ("CypherPink", "go", "background"),
+ ("CypherPink", "error", "background"),
+ ("CypherPunk", "foreground", "background"),
+ ("CypherPunk", "foreground", "info_background"),
+ ("CypherPunk", "go", "background"),
+ ("CypherPunk", "error", "background"),
+ )
+ for theme_name, foreground, background in cases:
+ palette = THEMES[theme_name]
+ assert contrast_ratio(palette[foreground], palette[background]) >= 4.5
+
+
+def test_frames_meet_non_text_contrast(amigo, contrast_ratio):
+ from krux.themes import THEMES
+
+ cases = (
+ "Dark",
+ "Light",
+ "Orange",
+ "CypherPink",
+ "CypherPunk",
+ )
+ for theme_name in cases:
+ palette = THEMES[theme_name]
+ assert contrast_ratio(palette["frame"], palette["background"]) >= 3
+
+
+def test_cypherpink_frame_keeps_theme_identity(amigo, contrast_ratio):
+ from krux.themes import THEMES
+
+ palette = THEMES["CypherPink"]
+
+ assert palette["frame"] != palette["disabled"]
+ assert contrast_ratio(palette["frame"], palette["background"]) >= 4.5
+
+
+def test_network_colors_use_light_theme_variants_only_on_light_theme(
+ amigo, contrast_ratio
+):
+ from krux.krux_settings import Settings
+ from krux.pages.utils import Utils
+ from krux.themes import DARKERGREEN, DARKERORANGE, GREEN, ORANGE, THEMES
+
+ cases = (
+ ("Dark", ORANGE, GREEN),
+ ("Light", DARKERORANGE, DARKERGREEN),
+ ("Orange", ORANGE, GREEN),
+ ("CypherPink", ORANGE, GREEN),
+ ("CypherPunk", ORANGE, GREEN),
+ )
+ for theme_name, main_color, test_color in cases:
+ Settings().appearance.theme = theme_name
+ background = THEMES[theme_name]["background"]
+
+ assert Utils.get_network_color("Mainnet") == main_color
+ assert Utils.get_network_color("Testnet") == test_color
+ assert contrast_ratio(main_color, background) >= 4.5
+ assert contrast_ratio(test_color, background) >= 4.5
+
+
+def test_amigo_uses_disabled_color_for_info_background(amigo):
+ from krux.krux_settings import Settings
+ from krux.themes import THEMES, Theme
+
+ cases = (
+ "Dark",
+ "Light",
+ "Orange",
+ "CypherPink",
+ "CypherPunk",
+ )
+ for theme_name in cases:
+ Settings().appearance.theme = theme_name
+
+ assert Theme().info_bg_color == THEMES[theme_name]["disabled"]
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.