Compare commits

...
Author SHA1 Message Date
deepend-tildeclub 61ec9af5e3 Merge pull request #420 from ZoiteChat/docs-theme-update
update theme to match new site
2026-10-12 17:11:27 -07:00
deepend 412dc92b10 update theme to match new site 2026-10-12 18:04:49 -06:00
deepend-tildeclub 04dab3dcd6 Merge pull request #412 from TehPeGaSuS/wrap-align-no-indent
Add option to align wrapped lines under the nick or message
2026-10-12 15:28:33 -07:00
ThePeGaSuS c51f885253 add option to align wrapped lines under the nick or message
When nick indentation is off, lines after the first wrap back to the
left edge, under the timestamp. Add a text_wrap_align preference
(start of line, prefix, message) so they can line up under the nick or
the message text instead. It is applied at wrap time, so existing
scrollback is re-wrapped when the setting changes, and the menu is
greyed out while nick indentation is on.
2026-10-12 17:43:09 +02:00
15 changed files with 970 additions and 2294 deletions
+46 -5
View File
@@ -33,10 +33,51 @@ Example:
that is not visually indented
```
## Read the Docs theme skin
## HTML theme
The HTML output keeps the existing `basicstrap` Sphinx theme, then applies the
ZoiteChat main-site skin from `_static/css/zoitechat-docs.css`.
The `zoitechat` theme inherits Sphinx's built-in `basic` theme. It uses the
same compact layout, blue navigation, and system fonts as zoitechat.org.
Appearance starts in System mode on each page. Light and Dark are available
for the current page; no cookies or browser storage are used.
Brand navigation links are set in `conf.py` through `html_context`, and the
header logo uses `_static/zoitechat-logo.svg`.
The theme's templates are in `_theme/zoitechat/`. Its readable CSS and
JavaScript are in `_static/css/zoitechat-docs.css` and
`_static/js/zoitechat-docs.js`. No minification, JavaScript package manager,
external fonts, or CDN assets are required.
Navigation URLs, including `zoite_atlas_url`, are configured through
`html_context` in `conf.py`. The logo
is `_static/zoitechat-logo.svg`. Standard Sphinx search, indexes, source
links, anchors, translations, and previous/next links remain available.
The HTML output also works as locally installed documentation; project-site
links only access the network when clicked.
Build from the repository root:
```sh
sphinx-build -b html docs docs/_build/html
```
Use a clean output directory when switching themes, so stale Basicstrap
assets from an earlier build are not included in a package.
## Debian packaging
The active theme does not use the bundled Basicstrap, Bootstrap, jQuery,
jQuery Cookie, Font Awesome, or icon fonts. Its font stack includes DejaVu
Sans and DejaVu Sans Mono; no font files are bundled by the theme.
Packagers may exclude `docs/_theme/basicstrap/` from the source package.
The default theme continues to build without that directory. No pre-minified
third-party assets need to be added or rebuilt for this theme.
Build with the distribution's `python3-sphinx`. After installing the HTML
into the package staging directory, use `dh_sphinxdoc` (or `dh --with
sphinxdoc`) to replace Sphinx's standard JavaScript files with system
symlinks. Include `${sphinxdoc:Depends}` in the package dependencies and
`${sphinxdoc:Built-Using}` where required for generated assets. The custom
`zoitechat-docs.js` remains a small, readable project source file.
A built-in theme is still selectable with `-D html_theme=classic` if a
packager needs a different layout. The old RST content has existing build
diagnostics; changing the theme does not suppress or alter them.
+655 -2281
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
(() => {
'use strict';
const initialise = () => {
const picker = document.getElementById('theme-picker');
const preference = window.matchMedia('(prefers-color-scheme: dark)');
if (picker instanceof HTMLSelectElement) {
const applyTheme = () => {
const dark = picker.value === 'dark'
|| (picker.value === 'system' && preference.matches);
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
document.querySelectorAll('meta[name="theme-color"]').forEach((meta) => {
meta.setAttribute('content', dark ? '#111416' : '#ffffff');
});
};
// Follow the OS on every visit, including browser back/forward navigation.
const resetTheme = () => {
picker.value = 'system';
applyTheme();
};
picker.addEventListener('change', applyTheme);
preference.addEventListener('change', applyTheme);
window.addEventListener('pageshow', resetTheme);
resetTheme();
}
const navigation = document.getElementById('docs-navigation');
const compact = window.matchMedia('(max-width: 760px)');
if (navigation instanceof HTMLDetailsElement) {
const fitNavigation = () => { navigation.open = !compact.matches; };
compact.addEventListener('change', fitNavigation);
fitNavigation();
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialise, { once: true });
} else {
initialise();
}
})();
+5
View File
@@ -0,0 +1,5 @@
{# SPDX-License-Identifier: GPL-2.0-or-later #}
<nav class="global-toc" aria-label="{{ _('Documentation contents') }}">
<h2>{{ _('Contents') }}</h2>
{{ toctree(maxdepth=2, collapse=true, includehidden=true, titles_only=true) }}
</nav>
+102
View File
@@ -0,0 +1,102 @@
{# SPDX-License-Identifier: GPL-2.0-or-later #}
{% extends "basic/layout.html" %}
{% set docs_root = root_doc|default(master_doc) %}
{% block extrahead %}
{{ super() }}
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#111416" media="(prefers-color-scheme: dark)">
{% endblock %}
{% block body_tag %}<body id="top">{% endblock %}
{% block header %}
<a class="skip-link" href="#main-content">{{ _('Skip to content') }}</a>
<header class="site-header">
<div class="topbar">
<a class="brand" href="{{ pathto(docs_root)|e }}">
<img src="{{ logo_url|default(pathto('_static/zoitechat-logo.svg', 1), true)|e }}" alt="" width="38" height="42">
<strong>{{ project|e }}</strong>
</a>
<label class="appearance" for="theme-picker">
<span>{{ _('Appearance') }}</span>
<select id="theme-picker" aria-label="{{ _('Documentation appearance') }}">
<option value="system" selected>{{ _('System') }}</option>
<option value="light">{{ _('Light') }}</option>
<option value="dark">{{ _('Dark') }}</option>
</select>
</label>
</div>
<nav class="site-nav" aria-label="{{ _('Primary navigation') }}">
<a href="{{ zoite_site_url|e }}">{{ _('Home') }}</a>
<a href="{{ zoite_download_url|e }}">{{ _('Downloads') }}</a>
<a href="{{ zoite_addons_url|e }}">{{ _('Addons') }}</a>
<a class="selected" href="{{ pathto(docs_root)|e }}" aria-current="location">{{ _('Documentation') }}</a>
<a href="{{ zoite_atlas_url|e }}">{{ _('Atlas') }}</a>
{% if hasdoc('genindex') %}<a href="{{ pathto('genindex')|e }}">{{ _('Index') }}</a>{% endif %}
<details class="support-menu">
<summary>{{ _('IRC support') }}</summary>
<div class="support-links">
<a href="{{ zoite_irc_url|e }}">{{ _('Zoite IRC') }}<small>#zoitechat · {{ _('Official support') }}</small></a>
<a href="{{ libera_irc_url|e }}">{{ _('Libera.Chat') }}<small>#zoitechat</small></a>
</div>
</details>
</nav>
<div class="docs-location">
<nav aria-label="{{ _('Breadcrumbs') }}">
<a href="{{ pathto(docs_root)|e }}">{{ _('Documentation') }}</a>
{% for parent in parents %}
<span aria-hidden="true">/</span> <a href="{{ parent.link|e }}">{{ parent.title }}</a>
{% endfor %}
{% if pagename != docs_root %}<span aria-hidden="true">/</span> <span>{{ title|striptags|e }}</span>{% endif %}
</nav>
<span class="docs-version">{{ release|e }}</span>
</div>
</header>
{% endblock %}
{% block relbar1 %}{% endblock %}
{% block relbar2 %}{% endblock %}
{% block content %}
<div class="docs-layout{% if not render_sidebar %} no-sidebar{% endif %}">
{% if render_sidebar %}
<aside class="docs-sidebar" aria-label="{{ _('Documentation navigation') }}">
<details class="docs-navigation" id="docs-navigation" open>
<summary>{{ _('Documentation') }}</summary>
<div class="sidebar-content">
{% for sidebartemplate in sidebars %}
{% include sidebartemplate %}
{% endfor %}
</div>
</details>
</aside>
{% endif %}
<main class="docs-content" id="main-content" tabindex="-1">
{% block body %}{% endblock %}
{% if prev or next %}
<nav class="page-relations" aria-label="{{ _('Related pages') }}">
<div>
{% if prev %}<small>{{ _('Previous') }}</small><a href="{{ prev.link|e }}" rel="prev">{{ prev.title }}</a>{% endif %}
</div>
<div>
{% if next %}<small>{{ _('Next') }}</small><a href="{{ next.link|e }}" rel="next">{{ next.title }}</a>{% endif %}
</div>
</nav>
{% endif %}
</main>
</div>
{% endblock %}
{% block footer %}
<footer class="site-footer">
<span>{{ project|e }} {{ _('documentation') }} · {{ release|e }}</span>
<nav aria-label="{{ _('Footer navigation') }}">
<a href="{{ pathto(docs_root)|e }}">{{ _('Contents') }}</a>
<a href="{{ zoite_atlas_url|e }}">{{ _('Atlas') }}</a>
{% if hasdoc('search') %}<a href="{{ pathto('search')|e }}">{{ _('Search') }}</a>{% endif %}
{% if hasdoc('genindex') %}<a href="{{ pathto('genindex')|e }}">{{ _('Index') }}</a>{% endif %}
<a href="#top">{{ _('Back to top') }}</a>
</nav>
</footer>
{% endblock %}
+7
View File
@@ -0,0 +1,7 @@
{# SPDX-License-Identifier: GPL-2.0-or-later #}
{% if display_toc %}
<nav class="local-toc" aria-label="{{ _('On this page') }}">
<h2>{{ _('On this page') }}</h2>
{{ toc }}
</nav>
{% endif %}
+10
View File
@@ -0,0 +1,10 @@
{# SPDX-License-Identifier: GPL-2.0-or-later #}
{% if pagename != "search" %}
<form class="docs-search" action="{{ pathto('search')|e }}" method="get" role="search">
<label for="docs-search-input">{{ _('Search documentation') }}</label>
<div class="search-inputs">
<input id="docs-search-input" type="search" name="q" placeholder="{{ _('Search documentation') }}" autocomplete="off" spellcheck="false">
<button type="submit">{{ _('Search') }}</button>
</div>
</form>
{% endif %}
+7
View File
@@ -0,0 +1,7 @@
[theme]
inherit = basic
stylesheet = basic.css
pygments_style = sphinx
sidebars = searchbox.html, globaltoc.html, localtoc.html, sourcelink.html
[options]
+5 -3
View File
@@ -99,7 +99,7 @@ trim_footnote_reference_space = True
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'basicstrap'
html_theme = 'zoitechat'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -127,13 +127,14 @@ html_context = {
'zoite_site_url': 'https://zoitechat.org/',
'zoite_download_url': 'https://zoitechat.org/download.php',
'zoite_addons_url': 'https://zoitechat.org/addons.php',
'zoite_atlas_url': 'https://atlas.zoitechat.org/',
'zoite_irc_url': 'https://zoite.net/webchat/?join=%23zoitechat',
'libera_irc_url': 'https://web.libera.chat/#zoitechat',
}
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
html_logo = 'zoitechat-logo.svg'
html_logo = '_static/zoitechat-logo.svg'
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@@ -145,8 +146,9 @@ html_favicon = '_static/favicon.ico'
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Main-site visual skin for Read the Docs/Sphinx HTML output.
# Local, readable assets shared by hosted and offline HTML documentation.
html_css_files = ['css/zoitechat-docs.css']
html_js_files = ['js/zoitechat-docs.js']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
+2
View File
@@ -590,6 +590,7 @@ const struct prefs vars[] =
{"gui_gtk3_theme", P_OFFSET (hex_gui_gtk3_theme), TYPE_STR},
{"text_indent", P_OFFINT (hex_text_indent), TYPE_BOOL},
{"text_max_indent", P_OFFINT (hex_text_max_indent), TYPE_INT},
{"text_wrap_align", P_OFFINT (hex_text_wrap_align), TYPE_INT},
{"text_max_lines", P_OFFINT (hex_text_max_lines), TYPE_INT},
{"text_replay", P_OFFINT (hex_text_replay), TYPE_BOOL},
{"text_search_case_match", P_OFFINT (hex_text_search_case_match), TYPE_BOOL},
@@ -881,6 +882,7 @@ load_default_config(void)
prefs.hex_net_reconnect_delay = 10;
prefs.hex_notify_timeout = 15;
prefs.hex_text_max_indent = 256;
prefs.hex_text_wrap_align = 0;
prefs.hex_text_max_lines = 5000;
prefs.hex_url_grabber_limit = 100; /* 0 means unlimited */
+1
View File
@@ -306,6 +306,7 @@ struct zoitechatprefs
int hex_net_reconnect_delay;
int hex_notify_timeout;
int hex_text_max_indent;
int hex_text_wrap_align;
int hex_text_max_lines;
int hex_url_grabber_limit;
+38
View File
@@ -59,6 +59,8 @@ static struct zoitechatprefs setup_prefs;
static GtkWidget *cancel_button;
static GtkWidget *font_dialog = NULL;
static GtkWidget *setup_topicbar_multiline_toggle = NULL;
static GtkWidget *setup_wrap_align_label = NULL;
static GtkWidget *setup_wrap_align_combo = NULL;
enum
{
@@ -157,6 +159,14 @@ static const char *const langsmenu[] =
};
#endif
static const char *const wrapalignmenu[] =
{
N_("Start of line (default)"),
N_("Prefix (nick name)"),
N_("Message"),
NULL
};
static const setting appearance_settings[] =
{
{ST_HEADER, N_("General"),0,0,0},
@@ -172,6 +182,7 @@ static const setting appearance_settings[] =
{ST_TOGGLR, N_("Indent nick names"), P_OFFINTNL(hex_text_indent), N_("Make nick names right-justified"),0,0},
{ST_TOGGLE, N_ ("Show marker line"), P_OFFINTNL (hex_text_show_marker), N_ ("Insert a red line after the last read text."), 0, 0},
{ST_TOGGLE, N_("Keep search position when closing"), P_OFFINTNL(hex_text_search_keep_position), 0, 0, 0},
{ST_MENU, N_("Align wrapped lines under:"), P_OFFINTNL(hex_text_wrap_align), N_("Where wrapped lines start when nick names are not indented"), wrapalignmenu, 0},
{ST_HEADER, N_("Timestamps"),0,0,0},
{ST_TOGGLE, N_("Enable timestamps"), P_OFFINTNL(hex_stamp_text),0,0,1},
@@ -857,6 +868,20 @@ setup_topicbar_inline_toggled_cb (GtkToggleButton *but, gpointer userdata)
!gtk_toggle_button_get_active (but));
}
static void
setup_text_indent_toggled_cb (GtkToggleButton *but, gpointer userdata)
{
gboolean sens = !gtk_toggle_button_get_active (but);
(void) userdata;
/* wrapped line alignment only applies without nick indentation */
if (setup_wrap_align_label)
gtk_widget_set_sensitive (setup_wrap_align_label, sens);
if (setup_wrap_align_combo)
gtk_widget_set_sensitive (setup_wrap_align_combo, sens);
}
static void
setup_create_toggleR (GtkWidget *tab, int row, const setting *set)
{
@@ -867,6 +892,9 @@ setup_create_toggleR (GtkWidget *tab, int row, const setting *set)
setup_get_int (&setup_prefs, set));
g_signal_connect (G_OBJECT (wid), "toggled",
G_CALLBACK (setup_toggle_cb), (gpointer)set);
if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_text_indent))
g_signal_connect (G_OBJECT (wid), "toggled",
G_CALLBACK (setup_text_indent_toggled_cb), NULL);
if (set->tooltip)
gtk_widget_set_tooltip_text (wid, _(set->tooltip));
setup_table_attach (tab, wid, 4, 5, row, row + 1, TRUE, FALSE,
@@ -1279,6 +1307,16 @@ setup_create_menu (GtkWidget *table, int row, const setting *set)
setup_table_attach (table, cbox, 3, 6, row, row + 1, TRUE, FALSE,
SETUP_ALIGN_FILL, SETUP_ALIGN_FILL, 0, 0);
if (set->offset == STRUCT_OFFSET_INT (struct zoitechatprefs, hex_text_wrap_align))
{
setup_wrap_align_label = wid;
setup_wrap_align_combo = cbox;
g_object_add_weak_pointer (G_OBJECT (wid), (gpointer *)&setup_wrap_align_label);
g_object_add_weak_pointer (G_OBJECT (cbox), (gpointer *)&setup_wrap_align_combo);
gtk_widget_set_sensitive (wid, !setup_prefs.hex_text_indent);
gtk_widget_set_sensitive (cbox, !setup_prefs.hex_text_indent);
}
}
static void
+1 -1
View File
@@ -92,7 +92,7 @@ PrintTextLine (xtext_buffer *xtbuf, unsigned char *text, int len, int indent, ti
memcpy (new_text, stamp, stamp_size);
g_free (stamp);
memcpy (new_text + stamp_size, text, len);
gtk_xtext_append (xtbuf, new_text, len + stamp_size, timet);
gtk_xtext_append_wrap (xtbuf, new_text, len + stamp_size, timet, stamp_size);
g_free (new_text);
} else
gtk_xtext_append (xtbuf, text, len, timet);
+46 -4
View File
@@ -81,6 +81,7 @@ struct textentry
gint16 mark_end;
int indent;
gint16 left_len;
gint16 stamp_len; /* bytes of str taken by the timestamp (no-indent mode) */
GSList *slp;
GArray *sublines; /* byte offsets at the end of each screen row */
int subline_count;
@@ -296,6 +297,7 @@ static void gtk_xtext_get_property (GObject *object, guint prop_id,
static void gtk_xtext_render_ents (GtkXText * xtext, textentry *, textentry *);
static void gtk_xtext_recalc_widths (xtext_buffer *buf, int);
static void gtk_xtext_fix_indent (xtext_buffer *buf);
static int gtk_xtext_text_width (GtkXText *xtext, unsigned char *text, int len);
static int gtk_xtext_find_subline (GtkXText *xtext, textentry *ent, int line);
/* For use by gtk_xtext_strip_color() and its callers -- */
struct offlen_s {
@@ -1322,6 +1324,36 @@ gtk_xtext_selection_clear (xtext_buffer *buf)
return ret;
}
/* x position that the lines after the first of 'ent' start at */
static int
gtk_xtext_cont_indent (xtext_buffer *buf, textentry *ent)
{
int w, off;
if (ent->left_len != -1 || prefs.hex_text_wrap_align < 1)
return buf->indent;
/* 1 = under the prefix (nick), 2 = under the message */
off = ent->stamp_len;
if (prefs.hex_text_wrap_align >= 2)
{
/* the nick (or "*") is the first word after the timestamp */
while (off < ent->str_len && ent->str[off] != ' ')
off++;
while (off < ent->str_len && ent->str[off] == ' ')
off++;
}
if (off <= 0 || off >= ent->str_len)
return buf->indent;
w = ent->indent + gtk_xtext_text_width (buf->xtext, ent->str, off);
/* leave room for the text, or just wrap normally */
if (w > (buf->window_width - MARGIN) / 2)
return buf->indent;
return w;
}
static int
find_x (GtkXText *xtext, textentry *ent, int x, int subline, int indent)
{
@@ -1414,7 +1446,7 @@ gtk_xtext_find_x (GtkXText * xtext, int x, textentry * ent, int subline,
if (subline < 1)
indent = ent->indent;
else
indent = xtext->buffer->indent;
indent = gtk_xtext_cont_indent (xtext->buffer, ent);
if (line > gtk_adjustment_get_page_size (xtext->adj) || line < 0)
{
@@ -4035,7 +4067,7 @@ gtk_xtext_render_line (GtkXText * xtext, textentry * ent, int line,
taken--;
}
indent = xtext->buffer->indent;
indent = gtk_xtext_cont_indent (xtext->buffer, ent);
line++;
taken++;
str += len;
@@ -4296,7 +4328,7 @@ gtk_xtext_lines_taken (xtext_buffer *buf, textentry *ent)
str += len;
end = str - ent->str;
g_array_append_val (ent->sublines, end);
indent = buf->indent;
indent = gtk_xtext_cont_indent (buf, ent);
}
while (str < ent->str + ent->str_len);
}
@@ -5376,6 +5408,7 @@ gtk_xtext_append_indent (xtext_buffer *buf,
prefix_width = gtk_xtext_text_width (buf->xtext, str, left_len + 1);
ent->left_len = left_len;
ent->stamp_len = 0;
ent->str = str;
ent->str_len = left_len + 1 + right_len;
ent->indent = buf->indent - prefix_width;
@@ -5421,6 +5454,14 @@ gtk_xtext_append_indent (xtext_buffer *buf,
void
gtk_xtext_append (xtext_buffer *buf, unsigned char *text, int len, time_t stamp)
{
gtk_xtext_append_wrap (buf, text, len, stamp, 0);
}
/* stamp_len: how many bytes at the start of text are the timestamp */
void
gtk_xtext_append_wrap (xtext_buffer *buf, unsigned char *text, int len, time_t stamp, int stamp_len)
{
textentry *ent;
@@ -5438,6 +5479,7 @@ gtk_xtext_append (xtext_buffer *buf, unsigned char *text, int len, time_t stamp)
memcpy (ent->str, text, len);
ent->indent = 0;
ent->left_len = -1;
ent->stamp_len = (stamp_len > 0 && stamp_len < ent->str_len) ? stamp_len : 0;
gtk_xtext_append_entry (buf, ent, stamp);
}
@@ -5474,7 +5516,7 @@ gtk_xtext_lastlog (xtext_buffer *out, xtext_buffer *search_area)
}
else
{
gtk_xtext_append (out, ent->str, ent->str_len, 0);
gtk_xtext_append_wrap (out, ent->str, ent->str_len, 0, ent->stamp_len);
}
if (out->text_last)
+1
View File
@@ -281,6 +281,7 @@ struct _GtkXTextClass
GtkWidget *gtk_xtext_new (const XTextColor *palette, int separator);
void gtk_xtext_append (xtext_buffer *buf, unsigned char *text, int len, time_t stamp);
void gtk_xtext_append_wrap (xtext_buffer *buf, unsigned char *text, int len, time_t stamp, int stamp_len);
void gtk_xtext_append_indent (xtext_buffer *buf,
unsigned char *left_text, int left_len,
unsigned char *right_text, int right_len,