From 0a59ec699f946a8ca92b784374301e96ad55bf2b Mon Sep 17 00:00:00 2001 From: 4aiman Date: Tue, 3 Nov 2026 22:24:11 +0300 Subject: [PATCH] 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. --- .gitignore | 1 + debian/changelog | 32 +++++ panel-plugin/Makefile.am | 4 +- panel-plugin/memswap.cc | 31 ++++- panel-plugin/settings.cc | 181 ++++++++++++++++++++++++++++- panel-plugin/settings.h | 22 ++++ panel-plugin/systemload.cc | 181 +++++++++++++++++++++++++---- panel-plugin/uptime.cc | 83 +++++++++++++ panel-plugin/uptime.h | 15 +++ panel-plugin/zram.cc | 226 ++++++++++++++++++++++++++++++++++++ panel-plugin/zram.h | 59 ++++++++++ po/ru.po | 27 +++++ tests/run.sh | 8 ++ tests/test_format_uptime.cc | 48 ++++++++ 14 files changed, 892 insertions(+), 26 deletions(-) create mode 100644 panel-plugin/zram.cc create mode 100644 panel-plugin/zram.h create mode 100644 tests/run.sh create mode 100644 tests/test_format_uptime.cc diff --git a/.gitignore b/.gitignore index 2edb1b8..32fe23f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/debian/changelog b/debian/changelog index f52fb29..f5a58d0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -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 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 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 diff --git a/panel-plugin/Makefile.am b/panel-plugin/Makefile.am index b2f4ab9..e671545 100644 --- a/panel-plugin/Makefile.am +++ b/panel-plugin/Makefile.am @@ -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 \ diff --git a/panel-plugin/memswap.cc b/panel-plugin/memswap.cc index c0fdfbc..0984643 100644 --- a/panel-plugin/memswap.cc +++ b/panel-plugin/memswap.cc @@ -34,6 +34,7 @@ #include #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) diff --git a/panel-plugin/settings.cc b/panel-plugin/settings.cc index 10a474c..255b4f6 100644 --- a/panel-plugin/settings.cc +++ b/panel-plugin/settings.cc @@ -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; diff --git a/panel-plugin/settings.h b/panel-plugin/settings.h index 9746f38..53af03b 100644 --- a/panel-plugin/settings.h +++ b/panel-plugin/settings.h @@ -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); diff --git a/panel-plugin/systemload.cc b/panel-plugin/systemload.cc index 99e9edb..37b25ed 100644 --- a/panel-plugin/systemload.cc +++ b/panel-plugin/systemload.cc @@ -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); } diff --git a/panel-plugin/uptime.cc b/panel-plugin/uptime.cc index 8abebec..9ed245f 100644 --- a/panel-plugin/uptime.cc +++ b/panel-plugin/uptime.cc @@ -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); +} diff --git a/panel-plugin/uptime.h b/panel-plugin/uptime.h index 6ab23a2..5a18847 100644 --- a/panel-plugin/uptime.h +++ b/panel-plugin/uptime.h @@ -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_ */ diff --git a/panel-plugin/zram.cc b/panel-plugin/zram.cc new file mode 100644 index 0000000..9cb8d86 --- /dev/null +++ b/panel-plugin/zram.cc @@ -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 +#endif + +#include +#include + +#include "zram.h" + +#ifdef __linux__ + +#include +#include +#include + +/* + * /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__ */ diff --git a/panel-plugin/zram.h b/panel-plugin/zram.h new file mode 100644 index 0000000..16502b1 --- /dev/null +++ b/panel-plugin/zram.h @@ -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 + +/* + * 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_ */ diff --git a/po/ru.po b/po/ru.po index c80ca38..6a761bf 100644 --- a/po/ru.po +++ b/po/ru.po @@ -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" +"Пустое поле - встроенный формат" diff --git a/tests/run.sh b/tests/run.sh new file mode 100644 index 0000000..09bd146 --- /dev/null +++ b/tests/run.sh @@ -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" diff --git a/tests/test_format_uptime.cc b/tests/test_format_uptime.cc new file mode 100644 index 0000000..64a407f --- /dev/null +++ b/tests/test_format_uptime.cc @@ -0,0 +1,48 @@ +/* Standalone check of format_uptime(); not part of the package build. + * Build and run: tests/run.sh */ +#include +#include +#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; +}