zram-aware memory, configurable uptime format (1:1.3.2-2ubuntu3+zram2)

- zram.cc/.h: read zram usage; memory bar can subtract RAM held by zram
- memswap: zram and disk swap shown separately in the tooltip
- uptime: format string with %d %h %m %s, %H %M %S, %t, \n and {...}
- settings dialog fields for both, Russian translation
- tests/test_format_uptime.cc with tests/run.sh
This is what the installed 1:1.3.2-2ubuntu3+zram2 package was built from.
This commit is contained in:
4aiman 2026-11-03 22:24:11 +03:00
commit 0a59ec699f
14 changed files with 892 additions and 26 deletions

1
.gitignore vendored
View file

@ -13,6 +13,7 @@ stamp-h1
autom4te.cache/
*.gmo
po/.intltool-merge-cache
po/.intltool-merge-cache.lock
po/POTFILES
po/stamp-it
po/*.pot

32
debian/changelog vendored
View file

@ -1,3 +1,35 @@
xfce4-systemload-plugin (1:1.3.2-2ubuntu3+zram2) noble; urgency=medium
* Configurable uptime format (uptime/format in xfconf, "Format" field in
the properties dialog). Placeholders %d %h %m %s, two-digit %H %M %S,
total hours %t; \n splits the label into several centred lines; a {...}
block disappears when all its numbers are zero. Empty keeps the old
built-in text.
* Fix g_snprintf size argument in the uptime label: it passed the size of
the whole [2][32] array (64) for a 32-byte buffer.
* Not for upload. Keep on hold.
-- chaiman <chaiman@lpc25> Tue, 03 Nov 2026 12:00:00 +0300
xfce4-systemload-plugin (1:1.3.2-2ubuntu3+zram1) noble; urgency=medium
* Local build: zram-aware memory accounting.
- New zram monitor: physical RAM occupied by the compressed swap,
with stored size and compression ratio in the tooltip.
- New "Do not count the RAM held by zram" option in the memory monitor,
off by default. Off, the memory bar keeps the literal meaning of
"used" - RAM that is occupied and unavailable - and the zram bar is a
breakdown of a part of it, so the two overlap. On, the two bars add up
to the occupied total without counting the same pages twice.
Either way the tooltip spells out the zram share, so the number on
screen is never ambiguous.
- Swap monitor now reads /proc/swaps and counts disk-backed areas
only. SwapTotal/SwapFree lump zram in and report its uncompressed
size, which corresponds to nothing physical.
- Not for upload. Keep on hold, rebuild after any upstream change.
-- chaiman <chaiman@lpc25> Thu, 27 Aug 2026 20:54:13 +0300
xfce4-systemload-plugin (1:1.3.2-2ubuntu3) noble; urgency=medium
* No-change rebuild for CVE-2024-3094

View file

@ -39,7 +39,9 @@ libsystemload_la_SOURCES = \
settings.h \
systemload.cc \
uptime.cc \
uptime.h
uptime.h \
zram.cc \
zram.h
libsystemload_la_LDFLAGS = \
-avoid-version \

View file

@ -34,6 +34,7 @@
#include <glib.h>
#include "memswap.h"
#include "zram.h"
#if defined(__linux__) || defined(__FreeBSD_kernel__)
@ -120,7 +121,35 @@ gint read_memswap(gulong *mem, gulong *swap, gulong *MT, gulong *MU, gulong *ST,
MFree += MCached + MBuffers;
MUsed = MTotal - MFree;
SUsed = STotal - SFree;
/*
* MUsed deliberately still includes the RAM held by zram: that memory is
* in fact occupied and unavailable to anyone. Whether to subtract it is a
* presentation choice, made by the caller through the "exclude zram from
* memory usage" setting - see update_monitors() in systemload.cc.
*/
/*
* SwapTotal/SwapFree lump zram together with disk swap, and for zram they
* report the UNCOMPRESSED size - a figure that corresponds to nothing
* physical. Ask /proc/swaps instead and count only the disk-backed areas:
* anything landing there is a genuine spill out of RAM.
*
* If /proc/swaps is unreadable, fall back to the meminfo numbers.
*/
{
gulong DTotal = 0, DUsed = 0;
if (read_disk_swap(&DTotal, &DUsed) == 0)
{
STotal = DTotal;
SUsed = DUsed;
}
else
{
SUsed = STotal - SFree;
}
}
*mem = MUsed * 100 / MTotal;
if(STotal)

View file

@ -55,6 +55,7 @@ static const gchar *const DEFAULT_LABEL[] = {
"mem",
"net",
"swap",
"zram",
};
static const gchar *const DEFAULT_COLOR[] = {
@ -62,6 +63,7 @@ static const gchar *const DEFAULT_COLOR[] = {
"#2ec27e", /* MEM */
"#e66100", /* NET */
"#f5c211", /* SWAP */
"#c061cb", /* ZRAM */
};
@ -92,13 +94,15 @@ struct _SystemloadConfig {
guint timeout_seconds;
gchar *system_monitor_command;
bool uptime;
gchar *uptime_format;
bool memory_exclude_zram;
struct {
bool enabled;
bool use_label;
gchar *label;
GdkRGBA color;
} monitor[4];
} monitor[N_MONITORS];
};
enum SystemloadProperty {
@ -123,6 +127,12 @@ enum SystemloadProperty {
PROP_SWAP_USE_LABEL,
PROP_SWAP_LABEL,
PROP_SWAP_COLOR,
PROP_ZRAM_ENABLED,
PROP_ZRAM_USE_LABEL,
PROP_ZRAM_LABEL,
PROP_ZRAM_COLOR,
PROP_MEMORY_EXCLUDE_ZRAM,
PROP_UPTIME_FORMAT,
N_PROPERTIES,
};
@ -158,6 +168,11 @@ prop2monitor (SystemloadProperty p)
case PROP_SWAP_LABEL:
case PROP_SWAP_COLOR:
return SWAP_MONITOR;
case PROP_ZRAM_ENABLED:
case PROP_ZRAM_USE_LABEL:
case PROP_ZRAM_LABEL:
case PROP_ZRAM_COLOR:
return ZRAM_MONITOR;
default:
/* Ideally, this codepath is never reached */
return CPU_MONITOR;
@ -339,6 +354,46 @@ systemload_config_class_init (SystemloadConfigClass *klass)
GDK_TYPE_RGBA,
GParamFlags (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
g_object_class_install_property (gobject_class,
PROP_ZRAM_ENABLED,
g_param_spec_boolean ("zram-enabled", NULL, NULL,
TRUE,
GParamFlags (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
g_object_class_install_property (gobject_class,
PROP_ZRAM_USE_LABEL,
g_param_spec_boolean ("zram-use-label", NULL, NULL,
TRUE,
GParamFlags (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
g_object_class_install_property (gobject_class,
PROP_ZRAM_LABEL,
g_param_spec_string ("zram-label", NULL, NULL,
DEFAULT_LABEL[ZRAM_MONITOR],
GParamFlags (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
g_object_class_install_property (gobject_class,
PROP_ZRAM_COLOR,
g_param_spec_boxed ("zram-color",
NULL, NULL,
GDK_TYPE_RGBA,
GParamFlags (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
/* Default FALSE: "used memory" keeps its literal meaning - RAM that is
* occupied and unavailable, zram's own footprint included. */
g_object_class_install_property (gobject_class,
PROP_MEMORY_EXCLUDE_ZRAM,
g_param_spec_boolean ("memory-exclude-zram", NULL, NULL,
FALSE,
GParamFlags (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
/* Empty = the built-in localized "1d 2h 3m" text; see format_uptime() */
g_object_class_install_property (gobject_class,
PROP_UPTIME_FORMAT,
g_param_spec_string ("uptime-format", NULL, NULL,
"",
GParamFlags (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
systemload_config_signals[CONFIGURATION_CHANGED] =
g_signal_new (g_intern_static_string ("configuration-changed"),
G_TYPE_FROM_CLASS (gobject_class),
@ -357,6 +412,7 @@ systemload_config_init (SystemloadConfig *config)
config->timeout_seconds = DEFAULT_TIMEOUT_SECONDS;
config->system_monitor_command = g_strdup (DEFAULT_SYSTEM_MONITOR_COMMAND);
config->uptime = true;
config->uptime_format = g_strdup ("");
for (gsize i = 0; i < G_N_ELEMENTS (config->monitor); i++)
{
config->monitor[i].enabled = true;
@ -376,6 +432,7 @@ systemload_config_finalize (GObject *object)
xfconf_shutdown();
g_free (config->property_base);
g_free (config->system_monitor_command);
g_free (config->uptime_format);
for (gsize i = 0; i < G_N_ELEMENTS (config->monitor); i++)
g_free (config->monitor[i].label);
@ -411,10 +468,19 @@ systemload_config_get_property (GObject *object,
g_value_set_boolean (value, config->uptime);
break;
case PROP_UPTIME_FORMAT:
g_value_set_string (value, config->uptime_format);
break;
case PROP_MEMORY_EXCLUDE_ZRAM:
g_value_set_boolean (value, config->memory_exclude_zram);
break;
case PROP_CPU_ENABLED:
case PROP_MEMORY_ENABLED:
case PROP_NETWORK_ENABLED:
case PROP_SWAP_ENABLED:
case PROP_ZRAM_ENABLED:
g_value_set_boolean (value, config->monitor[prop2monitor(prop_id)].enabled);
break;
@ -422,6 +488,7 @@ systemload_config_get_property (GObject *object,
case PROP_MEMORY_USE_LABEL:
case PROP_NETWORK_USE_LABEL:
case PROP_SWAP_USE_LABEL:
case PROP_ZRAM_USE_LABEL:
g_value_set_boolean (value, config->monitor[prop2monitor(prop_id)].use_label);
break;
@ -429,6 +496,7 @@ systemload_config_get_property (GObject *object,
case PROP_MEMORY_LABEL:
case PROP_NETWORK_LABEL:
case PROP_SWAP_LABEL:
case PROP_ZRAM_LABEL:
g_value_set_string (value, config->monitor[prop2monitor(prop_id)].label);
break;
@ -436,6 +504,7 @@ systemload_config_get_property (GObject *object,
case PROP_MEMORY_COLOR:
case PROP_NETWORK_COLOR:
case PROP_SWAP_COLOR:
case PROP_ZRAM_COLOR:
g_value_set_boxed (value, &config->monitor[prop2monitor(prop_id)].color);
break;
@ -502,6 +571,28 @@ systemload_config_set_property (GObject *object,
}
break;
case PROP_UPTIME_FORMAT:
val_string = g_value_get_string (value);
if (g_strcmp0 (config->uptime_format, val_string) != 0)
{
g_free (config->uptime_format);
/* xfconf may hand over NULL for an unset key; keep "" for "default" */
config->uptime_format = g_strdup (val_string ? val_string : "");
g_object_notify (G_OBJECT (config), "uptime-format");
g_signal_emit (G_OBJECT (config), systemload_config_signals [CONFIGURATION_CHANGED], 0);
}
break;
case PROP_MEMORY_EXCLUDE_ZRAM:
val_bool = g_value_get_boolean (value);
if (config->memory_exclude_zram != val_bool)
{
config->memory_exclude_zram = val_bool;
g_object_notify (G_OBJECT (config), "memory-exclude-zram");
g_signal_emit (G_OBJECT (config), systemload_config_signals [CONFIGURATION_CHANGED], 0);
}
break;
case PROP_CPU_ENABLED:
val_bool = g_value_get_boolean (value);
if (config->monitor[CPU_MONITOR].enabled != val_bool)
@ -694,6 +785,54 @@ systemload_config_set_property (GObject *object,
g_boxed_free (GDK_TYPE_RGBA, val_rgba);
break;
case PROP_ZRAM_ENABLED:
val_bool = g_value_get_boolean (value);
if (config->monitor[ZRAM_MONITOR].enabled != val_bool)
{
config->monitor[ZRAM_MONITOR].enabled = val_bool;
g_object_notify (G_OBJECT (config), "zram-enabled");
g_signal_emit (G_OBJECT (config), systemload_config_signals [CONFIGURATION_CHANGED], 0);
}
break;
case PROP_ZRAM_USE_LABEL:
val_bool = g_value_get_boolean (value);
if (config->monitor[ZRAM_MONITOR].use_label != val_bool)
{
config->monitor[ZRAM_MONITOR].use_label = val_bool;
g_object_notify (G_OBJECT (config), "zram-use-label");
g_signal_emit (G_OBJECT (config), systemload_config_signals [CONFIGURATION_CHANGED], 0);
}
break;
case PROP_ZRAM_LABEL:
val_string = g_value_get_string (value);
if (g_strcmp0 (config->monitor[ZRAM_MONITOR].label, val_string) != 0)
{
g_free (config->monitor[ZRAM_MONITOR].label);
config->monitor[ZRAM_MONITOR].label = g_value_dup_string (value);
g_object_notify (G_OBJECT (config), "zram-label");
g_signal_emit (G_OBJECT (config), systemload_config_signals [CONFIGURATION_CHANGED], 0);
}
break;
case PROP_ZRAM_COLOR:
val_rgba = (GdkRGBA*) g_value_dup_boxed (value);
if (!rgba_equal (config->monitor[ZRAM_MONITOR].color, *val_rgba))
{
config->monitor[ZRAM_MONITOR].color = *val_rgba;
g_object_notify (G_OBJECT (config), "zram-color");
g_signal_emit (G_OBJECT (config), systemload_config_signals [CONFIGURATION_CHANGED], 0);
}
if (is_default_color (ZRAM_MONITOR, val_rgba))
{
char *property = g_strconcat (config->property_base, "/zram/color", NULL);
xfconf_channel_reset_property (config->channel, property, TRUE);
g_free (property);
}
g_boxed_free (GDK_TYPE_RGBA, val_rgba);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -734,6 +873,22 @@ systemload_config_get_uptime_enabled (const SystemloadConfig *config)
return config->uptime;
}
const gchar*
systemload_config_get_uptime_format (const SystemloadConfig *config)
{
g_return_val_if_fail (IS_SYSTEMLOAD_CONFIG (config), "");
return config->uptime_format;
}
bool
systemload_config_get_memory_exclude_zram (const SystemloadConfig *config)
{
g_return_val_if_fail (IS_SYSTEMLOAD_CONFIG (config), false);
return config->memory_exclude_zram;
}
bool
systemload_config_get_enabled (const SystemloadConfig *config, SystemloadMonitor monitor)
{
@ -809,6 +964,10 @@ systemload_config_new (const gchar *property_base)
xfconf_g_property_bind (channel, property, G_TYPE_BOOLEAN, config, "uptime-enabled");
g_free (property);
property = g_strconcat (property_base, "/uptime/format", NULL);
xfconf_g_property_bind (channel, property, G_TYPE_STRING, config, "uptime-format");
g_free (property);
property = g_strconcat (property_base, "/cpu/enabled", NULL);
xfconf_g_property_bind (channel, property, G_TYPE_BOOLEAN, config, "cpu-enabled");
g_free (property);
@ -872,6 +1031,26 @@ systemload_config_new (const gchar *property_base)
property = g_strconcat (property_base, "/swap/color", NULL);
xfconf_g_property_bind_gdkrgba (channel, property, config, "swap-color");
g_free (property);
property = g_strconcat (property_base, "/zram/enabled", NULL);
xfconf_g_property_bind (channel, property, G_TYPE_BOOLEAN, config, "zram-enabled");
g_free (property);
property = g_strconcat (property_base, "/zram/use-label", NULL);
xfconf_g_property_bind (channel, property, G_TYPE_BOOLEAN, config, "zram-use-label");
g_free (property);
property = g_strconcat (property_base, "/zram/label", NULL);
xfconf_g_property_bind (channel, property, G_TYPE_STRING, config, "zram-label");
g_free (property);
property = g_strconcat (property_base, "/zram/color", NULL);
xfconf_g_property_bind_gdkrgba (channel, property, config, "zram-color");
g_free (property);
property = g_strconcat (property_base, "/memory/exclude-zram", NULL);
xfconf_g_property_bind (channel, property, G_TYPE_BOOLEAN, config, "memory-exclude-zram");
g_free (property);
}
return config;

View file

@ -26,11 +26,19 @@
#define MIN_TIMEOUT 500
#define MAX_TIMEOUT 10000
/*
* ZRAM_MONITOR is appended at the end on purpose: the numeric values of the
* existing entries index both the config array and the settings already
* stored in xfconf. Inserting in the middle would silently reassign the
* user's colours and labels to the wrong monitors.
*/
enum SystemloadMonitor {
CPU_MONITOR,
MEM_MONITOR,
NET_MONITOR,
SWAP_MONITOR,
ZRAM_MONITOR,
N_MONITORS,
};
typedef struct _SystemloadConfigClass SystemloadConfigClass;
@ -55,6 +63,20 @@ guint systemload_config_get_timeout (const Syste
guint systemload_config_get_timeout_seconds (const SystemloadConfig *config);
const gchar *systemload_config_get_system_monitor_command (const SystemloadConfig *config);
bool systemload_config_get_uptime_enabled (const SystemloadConfig *config);
/* "" means the built-in format; syntax is documented at format_uptime() */
const gchar *systemload_config_get_uptime_format (const SystemloadConfig *config);
/*
* When true, the RAM occupied by zram is subtracted from the memory monitor.
*
* Both readings are truthful, they answer different questions. Included (the
* default) means "how much RAM is unavailable" - the literal sense of "used",
* and what every other monitor reports; the zram bar is then a breakdown of a
* part of it, so the two bars overlap. Excluded means "how much RAM the system
* spends on its actual work" - the memory and zram bars then add up to the
* occupied total without counting anything twice.
*/
bool systemload_config_get_memory_exclude_zram (const SystemloadConfig *config);
bool systemload_config_get_enabled (const SystemloadConfig *config, SystemloadMonitor monitor);
bool systemload_config_get_use_label (const SystemloadConfig *config, SystemloadMonitor monitor);

View file

@ -55,6 +55,7 @@
#include "plugin.h"
#include "settings.h"
#include "uptime.h"
#include "zram.h"
@ -88,7 +89,7 @@ struct t_global_monitor {
bool use_timeout_seconds;
guint timeout_id;
t_command command;
t_monitor *monitor[4];
t_monitor *monitor[N_MONITORS];
t_uptime_monitor uptime;
#ifdef HAVE_UPOWER_GLIB
UpClient *upower;
@ -97,9 +98,12 @@ struct t_global_monitor {
/* zram sits between memory and swap: that is where it sits physically too -
* data already pushed out of RAM, but not yet spilled onto the disk. */
static const SystemloadMonitor VISUAL_ORDER[] = {
CPU_MONITOR,
MEM_MONITOR,
ZRAM_MONITOR,
SWAP_MONITOR,
NET_MONITOR,
};
@ -168,7 +172,10 @@ static void
update_monitors(t_global_monitor *global)
{
const SystemloadConfig *config = global->config;
gulong MTotal = 0, MUsed = 0, NTotal = 0, STotal = 0, SUsed = 0;
gulong MTotal = 0, MUsed = 0, NTotal = 0, STotal = 0, SUsed = 0, MZram = 0;
gulong ZPhys = 0, ZStored = 0;
gdouble ZRatio = 0.0;
bool ZValid = false;
for(gsize i = 0; i < G_N_ELEMENTS (global->monitor); i++)
global->monitor[i]->value_read = 0;
@ -181,10 +188,32 @@ update_monitors(t_global_monitor *global)
gulong mem, swap;
if (read_memswap(&mem, &swap, &MTotal, &MUsed, &STotal, &SUsed) == 0)
{
/*
* MUsed as returned includes the RAM held by zram - that memory is
* genuinely occupied. Whether the memory bar should show it is a
* question of what the bar is meant to answer, so it is left to
* the user; see systemload_config_get_memory_exclude_zram().
*/
MZram = read_zram_phys();
if (systemload_config_get_memory_exclude_zram (config))
{
MUsed = (MUsed > MZram) ? (MUsed - MZram) : 0;
mem = (MTotal > 0) ? (MUsed * 100 / MTotal) : 0;
}
global->monitor[MEM_MONITOR]->value_read = mem;
global->monitor[SWAP_MONITOR]->value_read = swap;
}
}
if (systemload_config_get_enabled (config, ZRAM_MONITOR))
{
gulong zram;
if (read_zram (&zram, &ZPhys, &ZStored, &ZRatio) == 0)
{
global->monitor[ZRAM_MONITOR]->value_read = zram;
ZValid = true;
}
}
if (systemload_config_get_enabled (config, NET_MONITOR))
{
gulong net;
@ -215,8 +244,26 @@ update_monitors(t_global_monitor *global)
if (systemload_config_get_enabled (config, MEM_MONITOR))
{
gchar tooltip[128];
g_snprintf(tooltip, sizeof(tooltip), _("Memory: %ldMB of %ldMB used"), MUsed >> 10 , MTotal >> 10);
gchar tooltip[192];
/* Always spell out the zram share, whichever way the bar is counted:
* that way the number on screen is never ambiguous. */
if (MZram > 0)
{
if (systemload_config_get_memory_exclude_zram (config))
g_snprintf(tooltip, sizeof(tooltip),
_("Memory: %ldMB of %ldMB used, excluding %ldMB held by zram"),
MUsed >> 10, MTotal >> 10, MZram >> 10);
else
g_snprintf(tooltip, sizeof(tooltip),
_("Memory: %ldMB of %ldMB used, including %ldMB held by zram"),
MUsed >> 10, MTotal >> 10, MZram >> 10);
}
else
{
g_snprintf(tooltip, sizeof(tooltip), _("Memory: %ldMB of %ldMB used"), MUsed >> 10 , MTotal >> 10);
}
set_tooltip(global->monitor[MEM_MONITOR]->ebox, tooltip);
}
@ -227,14 +274,31 @@ update_monitors(t_global_monitor *global)
set_tooltip(global->monitor[NET_MONITOR]->ebox, tooltip);
}
if (systemload_config_get_enabled (config, ZRAM_MONITOR))
{
gchar tooltip[192];
if (ZValid && ZStored > 0)
g_snprintf(tooltip, sizeof(tooltip),
_("ZRAM: %ldMB of RAM holds %ldMB of data (%.2f:1)"),
ZPhys >> 10, ZStored >> 10, ZRatio);
else if (ZValid)
g_snprintf(tooltip, sizeof(tooltip), _("ZRAM: empty"));
else
g_snprintf(tooltip, sizeof(tooltip), _("No zram device"));
set_tooltip(global->monitor[ZRAM_MONITOR]->ebox, tooltip);
}
if (systemload_config_get_enabled (config, SWAP_MONITOR))
{
gchar tooltip[128];
/* Disk-backed swap only; zram is reported by its own monitor */
if (STotal)
g_snprintf(tooltip, sizeof(tooltip), _("Swap: %ldMB of %ldMB used"), SUsed >> 10, STotal >> 10);
g_snprintf(tooltip, sizeof(tooltip), _("Disk swap: %ldMB of %ldMB used"), SUsed >> 10, STotal >> 10);
else
g_snprintf(tooltip, sizeof(tooltip), _("No swap"));
g_snprintf(tooltip, sizeof(tooltip), _("No disk swap"));
set_tooltip(global->monitor[SWAP_MONITOR]->ebox, tooltip);
}
@ -248,22 +312,33 @@ update_monitors(t_global_monitor *global)
gint hours = (global->uptime.value_read / 3600) % 24;
gint mins = (global->uptime.value_read / 60) % 60;
g_snprintf(days_str[0], sizeof(days_str), _("%dd"), days);
g_snprintf(hours_str[0], sizeof(hours_str), _("%dh"), hours);
g_snprintf(mins_str[0], sizeof(mins_str), _("%dm"), mins);
/* sizeof of one element: the whole [2][32] array is 64 bytes and
* would let g_snprintf write past the 32-byte buffer */
g_snprintf(days_str[0], sizeof(days_str[0]), _("%dd"), days);
g_snprintf(hours_str[0], sizeof(hours_str[0]), _("%dh"), hours);
g_snprintf(mins_str[0], sizeof(mins_str[0]), _("%dm"), mins);
g_snprintf(days_str[1], sizeof(days_str), ngettext("%d day", "%d days", days), days);
g_snprintf(hours_str[1], sizeof(hours_str), ngettext("%d hour", "%d hours", hours), hours);
g_snprintf(mins_str[1], sizeof(mins_str), ngettext("%d minute", "%d minutes", mins), mins);
if (days > 0)
g_snprintf(text, sizeof(text), "%s %s %s", days_str[0], hours_str[0], mins_str[0]);
else
g_snprintf(text, sizeof(text), "%s %s", hours_str[0], mins_str[0]);
g_snprintf(days_str[1], sizeof(days_str[1]), ngettext("%d day", "%d days", days), days);
g_snprintf(hours_str[1], sizeof(hours_str[1]), ngettext("%d hour", "%d hours", hours), hours);
g_snprintf(mins_str[1], sizeof(mins_str[1]), ngettext("%d minute", "%d minutes", mins), mins);
g_snprintf(tooltip, sizeof(tooltip), _("Uptime: %s, %s, %s"), days_str[1], hours_str[1], mins_str[1]);
set_label_text(GTK_LABEL(global->uptime.label), text);
const gchar *format = systemload_config_get_uptime_format (config);
if (format != NULL && format[0] != '\0')
{
gchar *custom = format_uptime (format, global->uptime.value_read);
set_label_text(GTK_LABEL(global->uptime.label), custom);
g_free (custom);
}
else
{
if (days > 0)
g_snprintf(text, sizeof(text), "%s %s %s", days_str[0], hours_str[0], mins_str[0]);
else
g_snprintf(text, sizeof(text), "%s %s", hours_str[0], mins_str[0]);
set_label_text(GTK_LABEL(global->uptime.label), text);
}
set_tooltip(global->uptime.ebox, tooltip);
}
}
@ -344,6 +419,8 @@ create_monitor (t_global_monitor *global)
gtk_event_box_set_visible_window(GTK_EVENT_BOX(global->uptime.ebox), FALSE);
global->uptime.label = gtk_label_new("");
/* A format with \n gives several lines; keep them centred as a block */
gtk_label_set_justify(GTK_LABEL(global->uptime.label), GTK_JUSTIFY_CENTER);
gtk_widget_show(global->uptime.label);
gtk_container_add(GTK_CONTAINER(global->uptime.ebox),
@ -669,12 +746,18 @@ static GtkWidget *new_label (GtkGrid *grid, guint row, const gchar *labeltext, G
return label;
}
/* Create a new monitor setting with gtkswitch, and eventually a color button and a checkbox + entry */
/* Create a new monitor setting with gtkswitch, and eventually a color button and a checkbox + entry
*
* extra_toggle_setting: optional boolean property of the config object to
* expose as a checkbox under the label row; NULL for none. Used by the memory
* monitor for "exclude zram". */
static void
new_monitor_setting (t_global_monitor *global,
GtkGrid *grid, int position,
const gchar *title, bool color,
const gchar *setting)
const gchar *setting,
const gchar *extra_toggle_setting = NULL,
const gchar *extra_toggle_text = NULL)
{
GtkWidget *sw, *label;
gchar *markup, *setting_name;
@ -747,6 +830,50 @@ new_monitor_setting (t_global_monitor *global,
g_free (setting_name);
gtk_grid_attach(GTK_GRID(subgrid), button, 2, 0, 1, 1);
}
if (extra_toggle_setting != NULL)
{
GtkWidget *check = gtk_check_button_new_with_mnemonic (extra_toggle_text);
gtk_widget_set_margin_start (check, 12);
g_object_bind_property (G_OBJECT (global->config), extra_toggle_setting,
G_OBJECT (check), "active",
GBindingFlags (G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL));
gtk_grid_attach (GTK_GRID(subgrid), check, 0, 1, 3, 1);
}
}
else
{
GtkWidget *revealer = gtk_revealer_new ();
GtkWidget *subgrid = gtk_grid_new ();
gtk_container_add (GTK_CONTAINER (revealer), subgrid);
gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), TRUE);
g_object_set_data (G_OBJECT(sw), "sensitive_widget", revealer);
gtk_grid_attach(GTK_GRID(grid), revealer, 0, position + 1, 2, 1);
gtk_grid_set_column_spacing (GTK_GRID(subgrid), 12);
gtk_grid_set_row_spacing (GTK_GRID(subgrid), 6);
label = gtk_label_new_with_mnemonic (_("_Format:"));
gtk_widget_set_halign (label, GTK_ALIGN_START);
gtk_widget_set_valign (label, GTK_ALIGN_CENTER);
gtk_widget_set_margin_start (label, 12);
gtk_grid_attach (GTK_GRID(subgrid), label, 0, 0, 1, 1);
GtkWidget *entry = gtk_entry_new ();
gtk_widget_set_hexpand (entry, TRUE);
gtk_label_set_mnemonic_widget (GTK_LABEL (label), entry);
gtk_entry_set_placeholder_text (GTK_ENTRY (entry), _("built-in format"));
gtk_widget_set_tooltip_text (entry,
_("%d days, %h hours, %m minutes, %s seconds\n"
"%H %M %S - the same with two digits (07)\n"
"%t - total hours, %% - percent sign\n"
"\\n - new line\n"
"{...} - hidden when every number inside is 0\n"
"Example: {%dd }%H:%M\n"
"Leave empty for the built-in format"));
g_object_bind_property (G_OBJECT (global->config), "uptime-format",
G_OBJECT (entry), "text",
GBindingFlags (G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL));
gtk_grid_attach(GTK_GRID(subgrid), entry, 1, 0, 1, 1);
}
switch_cb (GTK_SWITCH (sw), enabled, global);
@ -758,18 +885,22 @@ monitor_create_options(XfcePanelPlugin *plugin, t_global_monitor *global)
SystemloadConfig *config = global->config;
GtkWidget *label, *entry, *button, *box;
/* Indices must match enum SystemloadMonitor; uptime has no enum entry and
* therefore sits at N_MONITORS, right after the last real monitor. */
static const gchar *FRAME_TEXT[] = {
N_ ("CPU monitor"),
N_ ("Memory monitor"),
N_ ("Network monitor"),
N_ ("Swap monitor"),
N_ ("ZRAM monitor"),
N_ ("Uptime monitor")
};
static const gchar *SETTING_TEXT[] = {
"cpu",
"memory",
"network",
"swap"
"swap",
"zram"
};
xfce_panel_plugin_block_menu (plugin);
@ -856,12 +987,16 @@ monitor_create_options(XfcePanelPlugin *plugin, t_global_monitor *global)
new_monitor_setting (global, GTK_GRID(grid), 4 + 2 * i,
_(FRAME_TEXT[monitor]),
true,
SETTING_TEXT[monitor]);
SETTING_TEXT[monitor],
(monitor == MEM_MONITOR) ? "memory-exclude-zram" : NULL,
(monitor == MEM_MONITOR)
? _("Do not count the RAM held by _zram")
: NULL);
}
/* Uptime monitor options */
new_monitor_setting (global, GTK_GRID(grid), 4 + 2*G_N_ELEMENTS (global->monitor),
_(FRAME_TEXT[4]), FALSE, "uptime");
_(FRAME_TEXT[N_MONITORS]), FALSE, "uptime");
gtk_widget_show_all (dlg);
}

View file

@ -138,3 +138,86 @@ gulong read_uptime()
#else
#error "Your platform is not yet supported"
#endif
/* Platform independent, hence outside the #if chain above. The scan is
* byte-wise: every special character is ASCII and UTF-8 continuation bytes
* never collide with ASCII, so Cyrillic text passes through untouched. */
gchar *format_uptime(const gchar *format, gulong uptime)
{
const gulong days = uptime / 86400;
const gulong hours = (uptime / 3600) % 24;
const gulong mins = (uptime / 60) % 60;
const gulong secs = uptime % 60;
const gulong total_hours = uptime / 3600;
GString *out = g_string_new(NULL);
GString *group = NULL; /* non-NULL while inside {...} */
gboolean group_nonzero = FALSE;
for (const gchar *p = format ? format : ""; *p; p++)
{
GString *dst = group ? group : out;
if (*p == '\\' && p[1] != '\0')
{
p++;
g_string_append_c(dst, (*p == 'n') ? '\n' : *p);
}
else if (*p == '{' && group == NULL)
{
group = g_string_new(NULL);
group_nonzero = FALSE;
}
else if (*p == '}' && group != NULL)
{
if (group_nonzero)
g_string_append_len(out, group->str, group->len);
g_string_free(group, TRUE);
group = NULL;
}
else if (*p == '%' && p[1] != '\0')
{
p++;
gulong value;
gboolean padded = FALSE;
switch (*p)
{
case 'd': value = days; break;
case 'h': value = hours; break;
case 'm': value = mins; break;
case 's': value = secs; break;
case 'H': value = hours; padded = TRUE; break;
case 'M': value = mins; padded = TRUE; break;
case 'S': value = secs; padded = TRUE; break;
case 't': value = total_hours; break;
case '%':
g_string_append_c(dst, '%');
continue;
default:
g_string_append_c(dst, '%');
g_string_append_c(dst, *p);
continue;
}
if (padded)
g_string_append_printf(dst, "%02lu", value);
else
g_string_append_printf(dst, "%lu", value);
if (value != 0)
group_nonzero = TRUE;
}
else
{
g_string_append_c(dst, *p);
}
}
if (group != NULL)
{
/* Unclosed block: show it as typed rather than hiding the rest */
g_string_append_c(out, '{');
g_string_append_len(out, group->str, group->len);
g_string_free(group, TRUE);
}
return g_string_free(out, FALSE);
}

View file

@ -33,4 +33,19 @@
gulong read_uptime();
/*
* Render an uptime (in seconds) through a user format string.
*
* %d days %h hours (0-23) %m minutes (0-59) %s seconds (0-59)
* %H %M %S the same, always two digits ("07")
* %t total hours %% a literal percent sign
* \n line break \\ \{ \} \% the character itself
* {...} the block is dropped when every number inside it is 0
*
* Unknown %x sequences and an unclosed '{' are kept as literal text, so a
* typo is visible on the panel instead of silently disappearing.
* Returns a newly allocated string; free with g_free().
*/
gchar *format_uptime(const gchar *format, gulong uptime);
#endif /* _XFCE_SYSTEMLOAD_UPTIME_H_ */

226
panel-plugin/zram.cc Normal file
View file

@ -0,0 +1,226 @@
/*
* This file is part of Xfce (https://gitlab.xfce.org).
*
* zram monitor: local addition, not part of upstream.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES
* ARE DISCLAIMED.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <glib.h>
#include <glib/gstdio.h>
#include "zram.h"
#ifdef __linux__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* /sys/block/zramN/mm_stat is a single line of whitespace-separated numbers:
*
* orig_data_size compr_data_size mem_used_total mem_limit mem_used_max
* same_pages pages_compacted huge_pages [huge_pages_since]
*
* All sizes are in bytes. Only the first three are needed here.
*
* The file exists only while the device is initialised; an allocated but
* unused zramN has no mm_stat, so a failed read is normal, not an error.
*/
static gboolean
read_one_device (const gchar *devname, guint64 *orig, guint64 *compr, guint64 *used)
{
gchar *path = g_strdup_printf ("/sys/block/%s/mm_stat", devname);
gchar *contents = NULL;
gboolean ok = FALSE;
if (g_file_get_contents (path, &contents, NULL, NULL))
{
guint64 o = 0, c = 0, u = 0;
if (sscanf (contents, "%" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT " %" G_GUINT64_FORMAT,
&o, &c, &u) == 3)
{
*orig = o;
*compr = c;
*used = u;
ok = TRUE;
}
}
g_free (contents);
g_free (path);
return ok;
}
/* Walks /sys/block and sums mm_stat over every zram device. */
static gboolean
sum_devices (guint64 *orig, guint64 *compr, guint64 *used)
{
GDir *dir = g_dir_open ("/sys/block", 0, NULL);
if (dir == NULL)
return FALSE;
guint64 t_orig = 0, t_compr = 0, t_used = 0;
gboolean found = FALSE;
const gchar *name;
while ((name = g_dir_read_name (dir)) != NULL)
{
if (!g_str_has_prefix (name, "zram"))
continue;
guint64 o = 0, c = 0, u = 0;
if (read_one_device (name, &o, &c, &u))
{
t_orig += o;
t_compr += c;
t_used += u;
found = TRUE;
}
}
g_dir_close (dir);
if (!found)
return FALSE;
*orig = t_orig;
*compr = t_compr;
*used = t_used;
return TRUE;
}
static gulong
mem_total_kb (void)
{
gchar *contents = NULL;
gulong total = 0;
if (g_file_get_contents ("/proc/meminfo", &contents, NULL, NULL))
{
const gchar *p = strstr (contents, "MemTotal");
if (p != NULL)
sscanf (p + strlen ("MemTotal"), ": %lu", &total);
}
g_free (contents);
return total;
}
gulong
read_zram_phys (void)
{
guint64 orig = 0, compr = 0, used = 0;
if (!sum_devices (&orig, &compr, &used))
return 0;
return (gulong) (used / 1024);
}
gint
read_zram (gulong *zram, gulong *phys_kb, gulong *stored_kb, gdouble *ratio)
{
guint64 orig = 0, compr = 0, used = 0;
if (!sum_devices (&orig, &compr, &used))
return -1;
*phys_kb = (gulong) (used / 1024);
*stored_kb = (gulong) (orig / 1024);
*ratio = (compr > 0) ? ((gdouble) orig / (gdouble) compr) : 0.0;
gulong total = mem_total_kb ();
*zram = (total > 0) ? (*phys_kb * 100 / total) : 0;
return 0;
}
gint
read_disk_swap (gulong *total_kb, gulong *used_kb)
{
gchar *contents = NULL;
if (!g_file_get_contents ("/proc/swaps", &contents, NULL, NULL))
return -1;
gulong total = 0, used = 0;
gchar **lines = g_strsplit (contents, "\n", -1);
/* Line 0 is the header: Filename Type Size Used Priority */
for (guint i = 1; lines[i] != NULL; i++)
{
if (lines[i][0] == '\0')
continue;
gchar **f = g_strsplit_set (lines[i], " \t", -1);
gchar **compact = g_new0 (gchar *, g_strv_length (f) + 1);
guint n = 0;
/* /proc/swaps is tab-padded, so collapse the empty fields */
for (guint j = 0; f[j] != NULL; j++)
if (f[j][0] != '\0')
compact[n++] = f[j];
/* Skip zram: its size is the uncompressed one and it costs no disk */
if (n >= 4 && !g_str_has_prefix (compact[0], "/dev/zram"))
{
total += strtoul (compact[2], NULL, 10);
used += strtoul (compact[3], NULL, 10);
}
g_free (compact);
g_strfreev (f);
}
g_strfreev (lines);
g_free (contents);
*total_kb = total;
*used_kb = used;
return 0;
}
#else /* !__linux__ */
gulong
read_zram_phys (void)
{
return 0;
}
gint
read_zram (gulong *zram, gulong *phys_kb, gulong *stored_kb, gdouble *ratio)
{
*zram = 0;
*phys_kb = 0;
*stored_kb = 0;
*ratio = 0.0;
return -1;
}
gint
read_disk_swap (gulong *total_kb, gulong *used_kb)
{
*total_kb = 0;
*used_kb = 0;
return -1;
}
#endif /* __linux__ */

59
panel-plugin/zram.h Normal file
View file

@ -0,0 +1,59 @@
/*
* This file is part of Xfce (https://gitlab.xfce.org).
*
* zram monitor: local addition, not part of upstream.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES
* ARE DISCLAIMED.
*/
#ifndef _XFCE_SYSTEMLOAD_ZRAM_H_
#define _XFCE_SYSTEMLOAD_ZRAM_H_
#include <glib.h>
/*
* Physical RAM occupied by all zram devices, in kB.
* Returns 0 when there is no zram device or the data cannot be read.
*
* This is the number that has to be subtracted from "used memory": pages held
* by zram are counted as used RAM, yet they are not memory of any process -
* they are the compressed backing store of the swap.
*/
gulong read_zram_phys (void);
/*
* zram summary across all devices.
*
* zram - physical footprint as a percentage of MemTotal (for the bar)
* phys_kb - physical RAM used, kB (mem_used_total from mm_stat)
* stored_kb - uncompressed size of the stored pages, kB (orig_data_size)
* ratio - stored/compressed, 0.0 when nothing is stored
*
* Returns 0 on success, -1 when there is no zram device.
*/
gint read_zram (gulong *zram, gulong *phys_kb, gulong *stored_kb, gdouble *ratio);
/*
* Swap totals EXCLUDING zram devices, in kB, read from /proc/swaps.
*
* SwapTotal/SwapFree in /proc/meminfo lump zram together with disk swap, and
* for zram they report the uncompressed size - a number that corresponds to
* nothing physical. The real question the swap bar should answer is "how much
* has spilled onto the disk", and that is what this returns.
*
* Returns 0 on success, -1 when /proc/swaps cannot be read.
*/
gint read_disk_swap (gulong *total_kb, gulong *used_kb);
#endif /* _XFCE_SYSTEMLOAD_ZRAM_H_ */

View file

@ -176,3 +176,30 @@ msgstr "Copyright © 2003-2022\n"
#: ../panel-plugin/uptime.c:52
msgid "File /proc/uptime not found!"
msgstr "Файл /proc/uptime не найден!"
#: ../panel-plugin/systemload.cc
msgid "_Format:"
msgstr "_Формат:"
#: ../panel-plugin/systemload.cc
msgid "built-in format"
msgstr "встроенный формат"
#: ../panel-plugin/systemload.cc
#, no-c-format
msgid ""
"%d days, %h hours, %m minutes, %s seconds\n"
"%H %M %S - the same with two digits (07)\n"
"%t - total hours, %% - percent sign\n"
"\\n - new line\n"
"{...} - hidden when every number inside is 0\n"
"Example: {%dd }%H:%M\n"
"Leave empty for the built-in format"
msgstr ""
"%d дни, %h часы, %m минуты, %s секунды\n"
"%H %M %S - то же, всегда две цифры (07)\n"
"%t - часы всего, %% - знак процента\n"
"\\n - новая строка\n"
"{...} - скрывается, если все числа внутри равны 0\n"
"Пример: {%dд }%H:%M\n"
"Пустое поле - встроенный формат"

8
tests/run.sh Normal file
View file

@ -0,0 +1,8 @@
#!/bin/bash
# Build and run the standalone format_uptime() test.
set -e
dir="$(cd "$(dirname "$0")" && pwd)"
out="$dir/build"
mkdir -p "$out"
g++ -Wall -Wextra -o "$out/test_format_uptime" "$dir/test_format_uptime.cc" "$dir/../panel-plugin/uptime.cc" $(pkg-config --cflags --libs glib-2.0)
"$out/test_format_uptime"

View file

@ -0,0 +1,48 @@
/* Standalone check of format_uptime(); not part of the package build.
* Build and run: tests/run.sh */
#include <stdio.h>
#include <string.h>
#include "../panel-plugin/uptime.h"
static int failures = 0;
static void check(const char *format, gulong uptime, const char *expected)
{
gchar *got = format_uptime(format, uptime);
if (strcmp(got, expected) != 0)
{
printf("FAIL format=\"%s\" uptime=%lu\n expected=\"%s\"\n got =\"%s\"\n",
format, uptime, expected, got);
failures++;
}
g_free(got);
}
int main(void)
{
const gulong T = 2 * 86400 + 5 * 3600 + 7 * 60 + 9; /* 2d 5h 7m 9s */
check("%d %h %m %s", T, "2 5 7 9");
check("%H:%M:%S", T, "05:07:09");
check("%t", T, "53");
check("100%%", T, "100%");
check("%x%", T, "%x%"); /* unknown and trailing % kept */
check("line1\\nline2", T, "line1\nline2");
check("a\\\\b \\{x\\}", T, "a\\b {x}");
check("ends with \\", T, "ends with \\");
/* Cyrillic passes through byte-wise */
check("{%dд }%hч %Mм", T, "2д 5ч 07м");
check("{%dд }%hч %Mм", 5 * 3600 + 7 * 60, "5ч 07м"); /* zero days hidden */
check("{%dд\\n}%H:%M", 3600, "01:00");
check("{%dд\\n}%H:%M", T, "2д\n05:07");
check("{const}", 0, ""); /* no numbers = all zero */
check("{open %d", 0, "{open 0"); /* unclosed block literal */
check("stray}", 0, "stray}");
check("", T, "");
check(NULL, T, "");
if (failures == 0)
printf("OK all checks passed\n");
return failures ? 1 : 0;
}