dark mode v1

This commit is contained in:
2026-01-12 21:24:09 -07:00
parent c1cbe14871
commit e265ad4454
3 changed files with 170 additions and 72 deletions

View File

@@ -18,6 +18,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
@@ -32,6 +33,7 @@
#include "palette.h"
#include "../common/zoitechat.h"
#include "../common/zoitechatc.h" /* prefs */
#include "../common/util.h"
#include "../common/cfgfiles.h"
#include "../common/typedef.h"
@@ -87,6 +89,76 @@ GdkColor colors[] = {
{0, 0xa4a4, 0x0000, 0x0000}, /* 41 spell checker color (red) */
};
/* User palette snapshot (what we write to colors.conf) */
static GdkColor user_colors[MAX_COL + 1];
static gboolean user_colors_valid = FALSE;
/* ZoiteChat's curated dark palette (applies when prefs.hex_gui_dark_mode is enabled). */
static const GdkColor dark_colors[MAX_COL + 1] = {
/* mIRC colors 0-15 */
{0, 0xe5e5, 0xe5e5, 0xe5e5}, /* 0 white */
{0, 0x3c3c, 0x3c3c, 0x3c3c}, /* 1 black (dark gray for contrast) */
{0, 0x5656, 0x9c9c, 0xd6d6}, /* 2 blue */
{0, 0x0d0d, 0xbcbc, 0x7979}, /* 3 green */
{0, 0xf4f4, 0x4747, 0x4747}, /* 4 red */
{0, 0xcece, 0x9191, 0x7878}, /* 5 light red / brown */
{0, 0xc5c5, 0x8686, 0xc0c0}, /* 6 purple */
{0, 0xd7d7, 0xbaba, 0x7d7d}, /* 7 orange */
{0, 0xdcdc, 0xdcdc, 0xaaaa}, /* 8 yellow */
{0, 0xb5b5, 0xcece, 0xa8a8}, /* 9 light green */
{0, 0x4e4e, 0xc9c9, 0xb0b0}, /* 10 aqua */
{0, 0x9c9c, 0xdcdc, 0xfefe}, /* 11 light aqua */
{0, 0x3737, 0x9494, 0xffff}, /* 12 light blue */
{0, 0xd6d6, 0x7070, 0xd6d6}, /* 13 pink */
{0, 0x8080, 0x8080, 0x8080}, /* 14 gray */
{0, 0xc0c0, 0xc0c0, 0xc0c0}, /* 15 light gray */
/* mIRC colors 16-31 (repeat) */
{0, 0xe5e5, 0xe5e5, 0xe5e5}, {0, 0x3c3c, 0x3c3c, 0x3c3c},
{0, 0x5656, 0x9c9c, 0xd6d6}, {0, 0x0d0d, 0xbcbc, 0x7979},
{0, 0xf4f4, 0x4747, 0x4747}, {0, 0xcece, 0x9191, 0x7878},
{0, 0xc5c5, 0x8686, 0xc0c0}, {0, 0xd7d7, 0xbaba, 0x7d7d},
{0, 0xdcdc, 0xdcdc, 0xaaaa}, {0, 0xb5b5, 0xcece, 0xa8a8},
{0, 0x4e4e, 0xc9c9, 0xb0b0}, {0, 0x9c9c, 0xdcdc, 0xfefe},
{0, 0x3737, 0x9494, 0xffff}, {0, 0xd6d6, 0x7070, 0xd6d6},
{0, 0x8080, 0x8080, 0x8080}, {0, 0xc0c0, 0xc0c0, 0xc0c0},
/* selection colors */
{0, 0xffff, 0xffff, 0xffff}, /* 32 COL_MARK_FG */
{0, 0x2626, 0x4f4f, 0x7878}, /* 33 COL_MARK_BG */
/* foreground/background */
{0, 0xd4d4, 0xd4d4, 0xd4d4}, /* 34 COL_FG */
{0, 0x1e1e, 0x1e1e, 0x1e1e}, /* 35 COL_BG */
/* interface colors */
{0, 0x4040, 0x4040, 0x4040}, /* 36 COL_MARKER (marker line) */
{0, 0x3737, 0x9494, 0xffff}, /* 37 COL_NEW_DATA (tab: new data) */
{0, 0xd7d7, 0xbaba, 0x7d7d}, /* 38 COL_HILIGHT (tab: nick mentioned) */
{0, 0xf4f4, 0x4747, 0x4747}, /* 39 COL_NEW_MSG (tab: new message) */
{0, 0x8080, 0x8080, 0x8080}, /* 40 COL_AWAY (tab: away) */
{0, 0xf4f4, 0x4747, 0x4747}, /* 41 COL_SPELL (spellcheck underline) */
};
void
palette_user_set_color (int idx, const GdkColor *col)
{
if (!col)
return;
if (idx < 0 || idx > MAX_COL)
return;
if (!user_colors_valid)
{
memcpy (user_colors, colors, sizeof (user_colors));
user_colors_valid = TRUE;
}
user_colors[idx].red = col->red;
user_colors[idx].green = col->green;
user_colors[idx].blue = col->blue;
user_colors[idx].pixel = 0;
}
void
palette_alloc (GtkWidget * widget)
{
@@ -141,6 +213,10 @@ palette_load (void)
g_free (cfg);
close (fh);
}
/* Snapshot the user's palette for dark mode toggling. */
memcpy (user_colors, colors, sizeof (user_colors));
user_colors_valid = TRUE;
}
void
@@ -148,6 +224,11 @@ palette_save (void)
{
int i, j, fh;
char prefname[256];
const GdkColor *outpal = colors;
/* Don't clobber the user's colors.conf with the dark palette. */
if (prefs.hex_gui_dark_mode && user_colors_valid)
outpal = user_colors;
fh = zoitechat_open_file ("colors.conf", O_TRUNC | O_WRONLY | O_CREAT, 0600, XOF_DOMODE);
if (fh != -1)
@@ -156,14 +237,14 @@ palette_save (void)
for (i = 0; i < 32; i++)
{
g_snprintf (prefname, sizeof prefname, "color_%d", i);
cfg_put_color (fh, colors[i].red, colors[i].green, colors[i].blue, prefname);
cfg_put_color (fh, outpal[i].red, outpal[i].green, outpal[i].blue, prefname);
}
/* our special colors are mapped at 256+ */
for (i = 256, j = 32; j < MAX_COL+1; i++, j++)
{
g_snprintf (prefname, sizeof prefname, "color_%d", i);
cfg_put_color (fh, colors[j].red, colors[j].green, colors[j].blue, prefname);
cfg_put_color (fh, outpal[j].red, outpal[j].green, outpal[j].blue, prefname);
}
close (fh);
@@ -179,76 +260,41 @@ palette_color_eq (const GdkColor *a, const GdkColor *b)
gboolean
palette_apply_dark_mode (gboolean enable)
{
/*
* Stock ZoiteChat defaults from this file (keep them in sync with the
* colors[] initializer above):
* - Foreground: 0x2512/0x29e8/0x2b85
* - Background: 0xfae0/0xfae0/0xf8c4
*/
static const GdkColor light_fg = {0, 0x2512, 0x29e8, 0x2b85};
static const GdkColor light_bg = {0, 0xfae0, 0xfae0, 0xf8c4};
/* Common legacy "defaults" seen in the wild (pure black/white). */
static const GdkColor legacy_light_fg = {0, 0x0000, 0x0000, 0x0000};
static const GdkColor legacy_light_bg = {0, 0xffff, 0xffff, 0xffff};
/* A readable "dark" preset (roughly #D4D4D4 on #1E1E1E). */
static const GdkColor dark_fg = {0, 0xd4d4, 0xd4d4, 0xd4d4};
static const GdkColor dark_bg = {0, 0x1e1e, 0x1e1e, 0x1e1e};
GdkColor old_colors[MAX_COL + 1];
GdkColormap *cmap;
int i;
gboolean changed = FALSE;
if (enable)
memcpy (old_colors, colors, sizeof (old_colors));
/* Ensure we have a snapshot of the user's palette before overriding anything. */
if (!user_colors_valid)
{
if (palette_color_eq (&colors[COL_FG], &light_fg) ||
palette_color_eq (&colors[COL_FG], &legacy_light_fg))
{
colors[COL_FG].red = dark_fg.red;
colors[COL_FG].green = dark_fg.green;
colors[COL_FG].blue = dark_fg.blue;
changed = TRUE;
}
if (palette_color_eq (&colors[COL_BG], &light_bg) ||
palette_color_eq (&colors[COL_BG], &legacy_light_bg))
{
colors[COL_BG].red = dark_bg.red;
colors[COL_BG].green = dark_bg.green;
colors[COL_BG].blue = dark_bg.blue;
changed = TRUE;
}
}
else
{
if (palette_color_eq (&colors[COL_FG], &dark_fg))
{
/*
* Revert to a predictable light default. Most users who hit this toggle
* are coming from black-on-white palettes.
*/
colors[COL_FG].red = legacy_light_fg.red;
colors[COL_FG].green = legacy_light_fg.green;
colors[COL_FG].blue = legacy_light_fg.blue;
changed = TRUE;
}
if (palette_color_eq (&colors[COL_BG], &dark_bg))
{
colors[COL_BG].red = legacy_light_bg.red;
colors[COL_BG].green = legacy_light_bg.green;
colors[COL_BG].blue = legacy_light_bg.blue;
changed = TRUE;
}
memcpy (user_colors, colors, sizeof (user_colors));
user_colors_valid = TRUE;
}
/* Ensure the new colors have pixels allocated in the current colormap. */
if (changed)
if (enable)
memcpy (colors, dark_colors, sizeof (colors));
else
memcpy (colors, user_colors, sizeof (colors));
/* Allocate the new colors for GTK's colormap. */
cmap = gdk_colormap_get_system ();
for (i = 0; i <= MAX_COL; i++)
gdk_colormap_alloc_color (cmap, &colors[i], FALSE, TRUE);
for (i = 0; i <= MAX_COL; i++)
{
GdkColormap *cmap = gdk_colormap_get_system ();
if (cmap)
if (old_colors[i].red != colors[i].red ||
old_colors[i].green != colors[i].green ||
old_colors[i].blue != colors[i].blue)
{
gdk_colormap_alloc_color (cmap, &colors[COL_FG], TRUE, TRUE);
gdk_colormap_alloc_color (cmap, &colors[COL_BG], TRUE, TRUE);
changed = TRUE;
break;
}
}
return changed;
}

View File

@@ -38,12 +38,21 @@ void palette_alloc (GtkWidget * widget);
void palette_load (void);
void palette_save (void);
/* Keep a copy of the user's palette so dark mode can be toggled without losing it. */
void palette_user_set_color (int idx, const GdkColor *col);
/*
* Apply ZoiteChat's built-in "dark mode" background/foreground overrides.
* Apply ZoiteChat's built-in "dark mode" palette.
*
* This is intentionally conservative: it only adjusts the palette if the
* colors are still at ZoiteChat's stock defaults, so user-customized palettes
* continue to take precedence.
* When enabled, ZoiteChat switches to a curated dark-friendly palette for:
* - message colors (mIRC palette)
* - selection colors
* - tab highlight colors
* - chat/user/channel list background + foreground
*
* The user's palette is preserved in-memory and written to colors.conf even
* while dark mode is enabled, so disabling dark mode restores the previous
* colors without surprises.
*
* Returns TRUE if any palette entries were changed.
*/

View File

@@ -54,6 +54,9 @@ static int last_selected_page = 0;
static int last_selected_row = 0; /* sound row */
static gboolean color_change;
static struct zoitechatprefs setup_prefs;
/* Color picker buttons on the Colors page (disabled when dark mode is enabled). */
static GSList *color_selector_widgets;
static GtkWidget *dark_mode_toggle_widget;
static GtkWidget *cancel_button;
static GtkWidget *font_dialog = NULL;
@@ -347,8 +350,9 @@ static const setting dark_mode_setting =
ST_TOGGLE,
N_("Enable dark mode for chat views"),
P_OFFINTNL(hex_gui_dark_mode),
N_("Makes the chat buffer, channel list, and user list use a dark background/foreground.\n"
"This only changes the stock background/foreground colors. If you've customized them, your palette wins."),
N_("Applies ZoiteChat's built-in dark palette to the chat buffer, channel list, and user list.\n"
"This includes message colors, selection colors, and interface highlights.\n"
"While dark mode is enabled, the color selectors in this page are disabled."),
0,
0
};
@@ -1413,6 +1417,26 @@ setup_create_page (const setting *set)
return tab;
}
static void
setup_color_selectors_set_sensitive (gboolean sensitive)
{
GSList *l = color_selector_widgets;
while (l)
{
GtkWidget *w = (GtkWidget *) l->data;
if (GTK_IS_WIDGET (w))
gtk_widget_set_sensitive (w, sensitive);
l = l->next;
}
}
static void
setup_dark_mode_ui_toggle_cb (GtkToggleButton *but, gpointer userdata)
{
(void) userdata;
setup_color_selectors_set_sensitive (!gtk_toggle_button_get_active (but));
}
static void
setup_color_ok_cb (GtkWidget *button, GtkWidget *dialog)
{
@@ -1446,6 +1470,10 @@ setup_color_ok_cb (GtkWidget *button, GtkWidget *dialog)
/* is this line correct?? */
gdk_colormap_free_colors (gtk_widget_get_colormap (button), &old_color, 1);
/* Keep a copy of the user's palette so we can restore it after dark mode. */
if (!prefs.hex_gui_dark_mode)
palette_user_set_color ((int)(col - colors), col);
gtk_widget_destroy (dialog);
}
@@ -1507,6 +1535,9 @@ setup_create_color_button (GtkWidget *table, int num, int row, int col)
style->bg[GTK_STATE_NORMAL] = colors[num];
gtk_widget_set_style (but, style);
g_object_unref (style);
/* Track all color selector widgets so we can disable them when dark mode is enabled. */
color_selector_widgets = g_slist_prepend (color_selector_widgets, but);
}
static void
@@ -1536,6 +1567,9 @@ setup_create_other_color (char *text, int num, int row, GtkWidget *tab)
static GtkWidget *
setup_create_color_page (void)
{
color_selector_widgets = NULL;
dark_mode_toggle_widget = NULL;
GtkWidget *tab, *box, *label;
int i;
@@ -1582,7 +1616,10 @@ setup_create_color_page (void)
setup_create_other_colorR (_("Away user:"), COL_AWAY, 10, tab);
setup_create_other_color (_("Highlight:"), COL_HILIGHT, 11, tab);
setup_create_other_colorR (_("Spell checker:"), COL_SPELL, 11, tab);
setup_create_toggleL (tab, 13, &dark_mode_setting);
dark_mode_toggle_widget = setup_create_toggleL (tab, 13, &dark_mode_setting);
g_signal_connect (G_OBJECT (dark_mode_toggle_widget), "toggled",
G_CALLBACK (setup_dark_mode_ui_toggle_cb), NULL);
setup_color_selectors_set_sensitive (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dark_mode_toggle_widget)));
setup_create_header (tab, 15, N_("Color Stripping"));
@@ -2268,8 +2305,7 @@ setup_apply (struct zoitechatprefs *pr)
memcpy (&prefs, pr, sizeof (prefs));
/*
* "Dark mode" is mostly about the chat views. Be conservative: only adjust
* the stock Foreground/Background colors so user palettes keep winning.
* "Dark mode" applies ZoiteChat's built-in dark palette to the chat views.
*
* IMPORTANT: don't short-circuit this call.
* We MUST run palette_apply_dark_mode() when the toggle changes, otherwise
@@ -2376,6 +2412,13 @@ setup_close_cb (GtkWidget *win, GtkWidget **swin)
{
*swin = NULL;
if (color_selector_widgets)
{
g_slist_free (color_selector_widgets);
color_selector_widgets = NULL;
dark_mode_toggle_widget = NULL;
}
if (font_dialog)
{
gtk_widget_destroy (font_dialog);