From 720ce5de41b19995c7a593c3a29537a5996cfa0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Corentin=20No=C3=ABl?= Date: Wed, 2 Oct 2024 13:07:21 +0200 Subject: [PATCH 01/17] Add System Logs view Allows to graphically access the system logs. --- data/gresource.xml | 4 + data/system-logs-symbolic.svg | 41 ++++++++ src/Plug.vala | 1 + src/Utils/SystemdLogModel.vala | 152 +++++++++++++++++++++++++++++ src/Views/OperatingSystemView.vala | 15 +++ src/Widgets/LogsDialog.vala | 68 +++++++++++++ src/meson.build | 5 +- vapi/libsystemd.vapi | 128 ++++++++++++++++++++++++ 8 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 data/system-logs-symbolic.svg create mode 100644 src/Utils/SystemdLogModel.vala create mode 100644 src/Widgets/LogsDialog.vala create mode 100644 vapi/libsystemd.vapi diff --git a/data/gresource.xml b/data/gresource.xml index ddaa79c17..e1c556eb8 100644 --- a/data/gresource.xml +++ b/data/gresource.xml @@ -3,4 +3,8 @@ OperatingSystemView.css + + + system-logs-symbolic.svg + diff --git a/data/system-logs-symbolic.svg b/data/system-logs-symbolic.svg new file mode 100644 index 000000000..3cff47aea --- /dev/null +++ b/data/system-logs-symbolic.svg @@ -0,0 +1,41 @@ + + + + + + + diff --git a/src/Plug.vala b/src/Plug.vala index 346f3fba2..ea16f2b23 100644 --- a/src/Plug.vala +++ b/src/Plug.vala @@ -52,6 +52,7 @@ public class About.Plug : Switchboard.Plug { public override Gtk.Widget get_widget () { if (toolbarview == null) { + Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).add_resource_path ("/io/elementary/settings/system/icons"); operating_system_view = new OperatingSystemView (); var hardware_view = new HardwareView (); diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala new file mode 100644 index 000000000..5cdf46762 --- /dev/null +++ b/src/Utils/SystemdLogModel.vala @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + */ + +public class About.SystemdLogRow : GLib.Object { + public string origin { get; construct; } + public string message { get; construct; } + + public SystemdLogRow (string origin, string message) { + Object(origin: origin, message: message); + } +} + +public class About.SystemdLogModel : GLib.Object, GLib.ListModel { + private GLib.HashTable cached_rows; + private uint current_line = uint.MAX; + private uint num_entries = 0; + private Systemd.Journal journal; + + construct { + cached_rows = new GLib.HashTable(GLib.direct_hash, GLib.direct_equal); + create_journal (); + load_data.begin (); + } + + private void create_journal () { + int res = Systemd.Journal.open_namespace (out journal, null, LOCAL_ONLY); + if (res != 0) { + critical ("%s", strerror(-res)); + return; + } + + Systemd.Id128 boot_id; + res = Systemd.Id128.boot (out boot_id); + if (res != 0) { + critical ("%s", strerror(-res)); + return; + } + + journal.add_match ("_BOOT_ID=%s".printf(boot_id.str).data); + journal.add_conjunction (); + } + + private uint load_next_entries () { + if (current_line != num_entries) { + int res = journal.seek_head (); + current_line = 0; + if (res != 0) { + critical ("%s", strerror(-res)); + return 0; + } + + if (num_entries > 0) { + res = journal.next_skip (num_entries); + current_line += num_entries; + if (res < 0) { + critical ("%s", strerror(-res)); + return 0; + } + } + } + + int res = journal.next (); + if (res < 0) { + critical ("%s", strerror(-res)); + return 0; + } + + current_line += res; + num_entries += res; + return res; + } + + private async void load_data () { + GLib.Idle.add(() => { + uint added = 0, old_num_entries = 0; + lock (journal) { + old_num_entries = num_entries; + added = load_next_entries (); + } + if (added > 0) { + items_changed (old_num_entries, 0, added); + return Source.CONTINUE; + } else { + return Source.REMOVE; + } + }, GLib.Priority.LOW); + } + + public Object? get_item (uint position) { + About.SystemdLogRow? row = null; + lock (journal) { + row = cached_rows.get (position); + if (row != null) { + return row; + } + + int res = journal.seek_head (); + current_line = 0; + if (res != 0) { + critical ("%s", strerror(-res)); + return null; + } + + res = journal.next (); + if (res < 0) { + critical ("%s", strerror(-res)); + return null; + } + + res = journal.next_skip (position); + current_line += position; + if (res < 0) { + critical ("%s", strerror(-res)); + return null; + } + + unowned uint8[] data; + unowned uint8[] comm_data; + res = journal.get_data ("MESSAGE", out data); + if (res != 0) { + critical ("%s", strerror(-res)); + return null; + } + + res = journal.get_data ("_COMM", out comm_data); + if (res != 0) { + //critical ("%s %s", strerror(-res), (string) data); + comm_data = "_COMM=kernel".data; + } + + row = new About.SystemdLogRow (((string)comm_data).offset ("_COMM=".length), ((string)data).offset("MESSAGE=".length)); + cached_rows.set (position, row); + row.weak_ref((obj) => { + cached_rows.foreach_remove ((key, val) => { + return val == obj; + }); + }); + } + + return (owned)row; + } + + public Type get_item_type () { + return typeof(About.SystemdLogRow); + } + + public uint get_n_items () { + return num_entries; + } +} diff --git a/src/Views/OperatingSystemView.vala b/src/Views/OperatingSystemView.vala index 6a9f62b36..8fea00bdf 100644 --- a/src/Views/OperatingSystemView.vala +++ b/src/Views/OperatingSystemView.vala @@ -174,6 +174,13 @@ public class About.OperatingSystemView : Gtk.Box { kernel_version_label.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); kernel_version_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + var log_button = new Gtk.Button.from_icon_name ("system-logs-symbolic") { + tooltip_text = _("System logs"), + halign = END, + valign = BASELINE_CENTER, + hexpand = false + }; + packages = new Gtk.StringList (null); updates_image = new Gtk.Image () { @@ -350,6 +357,7 @@ public class About.OperatingSystemView : Gtk.Box { }; software_grid.attach (logo_overlay, 0, 0, 1, 4); software_grid.attach (title, 1, 0); + software_grid.attach (log_button, 2, 0); software_grid.attach (kernel_version_label, 1, 2); software_grid.attach (updates_list, 1, 3); @@ -396,6 +404,13 @@ public class About.OperatingSystemView : Gtk.Box { launch_uri (((SponsorUsRow) row).uri); }); + log_button.clicked.connect (() => { + var logs_dialog = new LogsDialog () { + transient_for = (Gtk.Window) get_root () + }; + logs_dialog.present (); + }); + links_list.row_activated.connect ((row) => { launch_uri (((LinkRow) row).uri); }); diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala new file mode 100644 index 000000000..df6231b73 --- /dev/null +++ b/src/Widgets/LogsDialog.vala @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + */ + +public class About.LogsDialog : Granite.Dialog { + + public LogsDialog () { + } + + construct { + title = _("System Logs"); + modal = true; + + var title_label = new Gtk.Label ( + _("System Logs") + ) { + halign = START + }; + title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + + var log_listbox = new Gtk.ListBox () { + vexpand = true, + selection_mode = NONE + }; + var model = new About.SystemdLogModel (); + log_listbox.bind_model (model, (obj) => { + unowned var row = (About.SystemdLogRow) obj; + + var origin_label = new Gtk.Label (row.origin); + origin_label.add_css_class (Granite.STYLE_CLASS_H4_LABEL); + var message_label = new Gtk.Label (row.message) { + wrap = true, + hexpand = true, + halign = START, + }; + + var box = new Gtk.Box (HORIZONTAL, 6); + box.append (origin_label); + box.append (message_label); + + return box; + }); + + var scrolled = new Gtk.ScrolledWindow () { + child = log_listbox, + hscrollbar_policy = NEVER, + max_content_height = 400, + propagate_natural_height = true + }; + + var frame = new Gtk.Frame (null) { + child = scrolled + }; + + var box = new Gtk.Box (VERTICAL, 12); + box.append (title_label); + box.append (frame); + + get_content_area ().append (box); + + add_button (_("Close"), Gtk.ResponseType.CLOSE); + + response.connect (() => { + close (); + }); + } +} diff --git a/src/meson.build b/src/meson.build index 57bd2a4a8..4e256796b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ plug_files = files( 'Interfaces/FirmwareClient.vala', 'Interfaces/LoginManager.vala', 'Utils/ARMPartDecoder.vala', + 'Utils' / 'SystemdLogModel.vala', 'Views' / 'DriversView.vala', 'Views/FirmwareReleaseView.vala', 'Views/FirmwareView.vala', @@ -12,7 +13,8 @@ plug_files = files( 'Views/OperatingSystemView.vala', 'Widgets/FirmwareUpdateRow.vala', 'Widgets' / 'DriverRow.vala', - 'Widgets' / 'UpdateDetailsDialog.vala' + 'Widgets' / 'UpdateDetailsDialog.vala', + 'Widgets' / 'LogsDialog.vala', ) switchboard_dep = dependency('switchboard-3') @@ -42,6 +44,7 @@ shared_module( dependency('libadwaita-1'), dependency('libgtop-2.0'), dependency('libsoup-3.0'), + dependency('libsystemd'), dependency('packagekit-glib2'), dependency('gudev-1.0'), dependency('udisks2'), diff --git a/vapi/libsystemd.vapi b/vapi/libsystemd.vapi new file mode 100644 index 000000000..35e2fabfe --- /dev/null +++ b/vapi/libsystemd.vapi @@ -0,0 +1,128 @@ +[CCode (lower_case_cprefix = "sd_")] +namespace Systemd { + [Compact, CCode (cname = "sd_journal", cheader_filename = "systemd/sd-journal.h", free_function = "sd_journal_close")] + public class Journal { + [CCode (cname = "int", cprefix = "LOG_", lower_case_cprefix = "sd_journal_", cheader_filename = "systemd/sd-journal.h,syslog.h", has_type_id = false)] + public enum Priority { + EMERG, + ALERT, + CRIT, + ERR, + WARNING, + NOTICE, + INFO, + DEBUG; + + [PrintfFormat] + public int print (string format, ...); + public int printv (string format, va_list ap); + + [CCode (instance_pos = 1.5)] + public int stream_fd (string identifier, bool level_prefix); + [CCode (cname = "_vala_sd_journal_stream")] + public GLib.FileStream? stream (string identifier, bool level_prefix) { + int fd = this.stream_fd (identifier, level_prefix); + return (fd < 0) ? null : GLib.FileStream.fdopen (fd, "w"); + } + } + + public static int send (string format, ...); + public static int sendv (Posix.iovector[] iov); + public static int perror (string message); + + [Flags, CCode (cname = "int", cprefix = "SD_JOURNAL_", has_type_id = false)] + public enum OpenFlags { + LOCAL_ONLY, + RUNTIME_ONLY, + SYSTEM, + CURRENT_USER, + OS_ROOT, + ALL_NAMESPACES, + INCLUDE_DEFAULT_NAMESPACE, + TAKE_DIRECTORY_FD, + ASSUME_IMMUTABLE + } + public static int open (out Systemd.Journal ret, Systemd.Journal.OpenFlags flags); + public static int open_namespace (out Systemd.Journal ret, string? name_space, Systemd.Journal.OpenFlags flags); + public static int open_directory (out Systemd.Journal ret, string path, Systemd.Journal.OpenFlags flags); + public static int open_files (out Systemd.Journal ret, [CCode (array_length = false, array_null_terminated = true)] string[] paths, Systemd.Journal.OpenFlags flags); + + public int previous (); + public int next (); + + public int previous_skip (uint64 skip); + public int next_skip (uint64 skip); + + public int get_realtime_usec (out uint64 ret); + public int get_monotonic_usec (out uint64 ret, out Systemd.Id128 ret_boot_id); + + public int set_data_threshold (size_t sz); + public int get_data_threshold (out size_t sz); + + public int get_data (string field, [CCode (type = "const void**", array_length_type = "size_t")] out unowned uint8[] data); + public int enumerate_data ([CCode (type = "const void**", array_length_type = "size_t")] out unowned uint8[] data); + public void restart_data (); + + public int add_match ([CCode (array_length_type = "size_t")] uint8[] data); + public int add_disjunction (); + public int add_conjunction (); + public void flush_matches (); + + public int seek_head (); + public int seek_tail (); + public int seek_monotonic_usec (Systemd.Id128 boot_id, uint64 usec); + public int seek_realtime_usec (uint64 usec); + public int seek_cursor (string cursor); + + public int get_cursor (out unowned string cursor); + public int test_cursor (string cursor); + + public int get_cutoff_realtime_usec (out uint64 from, out uint64 to); + public int get_cutoff_monotonic_usec (Systemd.Id128 boot_id, out uint64 from, out uint64 to); + + public int get_usage (out uint64 bytes); + + public int query_unique (string field); + public int enumerate_unique ([CCode (type = "const void**", array_length_type = "size_t")] out unowned uint8[] data); + public void restart_unique (); + + public int get_fd (); + public int get_events (); + public int get_timeout (out uint64 timeout_usec); + public int process (); + public int wait (uint64 timeout_usec); + public int reliable_fd (); + + public int get_catalog (out unowned string text); + public int get_catalog_for_message_id (Systemd.Id128 id, out unowned string ret); + } + + [SimpleType, CCode (cname = "sd_id128_t", lower_case_cprefix = "sd_id128_", cheader_filename = "systemd/sd-id128.h", default_value = "SD_ID128_NULL")] + public struct Id128 { + public uint8 bytes[16]; + public uint64 qwords[2]; + + [CCode (cname = "SD_ID128_NULL")] + public const Systemd.Id128 NULL; + + [CCode (cname = "sd_id128_randomize")] + public static int random (out Systemd.Id128 ret); + [CCode (cname = "sd_id128_get_machine")] + public static int machine (out Systemd.Id128 ret); + [CCode (cname = "sd_id128_get_boot")] + public static int boot (out Systemd.Id128 ret); + public static int from_string (string s, out Systemd.Id128 ret); + + [CCode (cname = "_vala_sd_id128_to_string")] + public string to_string () { + return this.str; + } + + public static bool equal (Systemd.Id128 a, Systemd.Id128 b); + + public unowned string str { + [CCode (cname = "SD_ID128_TO_STRING")] + get; + } + } +} From 464c5f0946cc0229e43bd843999d972265d2bf98 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Wed, 9 Apr 2025 19:27:44 +0200 Subject: [PATCH 02/17] Go from tail, load just in time, etc. --- src/Utils/SystemdLogModel.vala | 190 ++++++++++++++++++++------------- src/Widgets/LogsDialog.vala | 69 ++++++++---- 2 files changed, 160 insertions(+), 99 deletions(-) diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala index 5cdf46762..9f682c149 100644 --- a/src/Utils/SystemdLogModel.vala +++ b/src/Utils/SystemdLogModel.vala @@ -3,29 +3,66 @@ * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) */ -public class About.SystemdLogRow : GLib.Object { - public string origin { get; construct; } - public string message { get; construct; } +public class About.SystemdLogEntry : GLib.Object { + public unowned SystemdLogModel model { get; construct; } + public int pos { get; construct; } - public SystemdLogRow (string origin, string message) { - Object(origin: origin, message: message); + public string origin { get; private set; default = ""; } + public string message { get; private set; default = ""; } + + private bool loaded = false; + + public SystemdLogEntry (SystemdLogModel model, int pos) { + Object (model: model, pos: pos); + } + + public void queue_load () { + if (loaded) { + return; + } + loaded = true; + + Idle.add (load, GLib.Priority.LOW); + } + + private bool load () { + model.goto (pos); + + unowned uint8[] data; + unowned uint8[] comm_data; + int res = model.journal.get_data ("MESSAGE", out data); + if (res != 0) { + critical ("%s", strerror(-res)); + return Source.REMOVE; + } + + res = model.journal.get_data ("_COMM", out comm_data); + if (res != 0) { + critical ("%s %s", strerror(-res), (string) comm_data); + comm_data = "_COMM=kernel".data; + } + + origin = ((string) comm_data).offset ("_COMM=".length); + message = ((string) data).offset("MESSAGE=".length); + + return Source.REMOVE; } } public class About.SystemdLogModel : GLib.Object, GLib.ListModel { - private GLib.HashTable cached_rows; - private uint current_line = uint.MAX; + private const int CHUNK = 2000; + + private Systemd.Journal _journal; + public Systemd.Journal journal { get { return _journal; } } + + private GLib.HashTable cached_entries; + private int current_line = int.MAX; private uint num_entries = 0; - private Systemd.Journal journal; construct { - cached_rows = new GLib.HashTable(GLib.direct_hash, GLib.direct_equal); - create_journal (); - load_data.begin (); - } + cached_entries = new GLib.HashTable(GLib.direct_hash, GLib.direct_equal); - private void create_journal () { - int res = Systemd.Journal.open_namespace (out journal, null, LOCAL_ONLY); + int res = Systemd.Journal.open_namespace (out _journal, null, LOCAL_ONLY); if (res != 0) { critical ("%s", strerror(-res)); return; @@ -40,6 +77,47 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { journal.add_match ("_BOOT_ID=%s".printf(boot_id.str).data); journal.add_conjunction (); + journal.seek_tail (); + journal.previous (); + current_line = 0; + + load_data.begin (); + } + + public void goto (int pos) { + if (current_line == pos) { + return; + } + + var diff = current_line - pos; + + int res; + if (diff < 0) { + res = journal.previous_skip (-diff); + } else { + res = journal.next_skip (diff); + } + + if (res < 0) { + critical ("Failed to go to pos %d: %s", pos, strerror(-res)); + return; + } + + current_line = pos; + } + + private void load_chunk () { + int to_load = CHUNK; + Idle.add (() => { + load_next_entry (); + to_load--; + return (to_load > 0 ? Source.CONTINUE : Source.REMOVE); + }, GLib.Priority.LOW); + } + + private void load_next_entry () { + num_entries++; + items_changed (num_entries - 1, 0, 1); } private uint load_next_entries () { @@ -53,7 +131,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { if (num_entries > 0) { res = journal.next_skip (num_entries); - current_line += num_entries; + // current_line += num_entries; if (res < 0) { critical ("%s", strerror(-res)); return 0; @@ -74,76 +152,36 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { private async void load_data () { GLib.Idle.add(() => { - uint added = 0, old_num_entries = 0; - lock (journal) { - old_num_entries = num_entries; - added = load_next_entries (); - } - if (added > 0) { - items_changed (old_num_entries, 0, added); - return Source.CONTINUE; - } else { - return Source.REMOVE; - } + load_chunk (); + return Source.REMOVE; + // uint added = 0, old_num_entries = 0; + // lock (journal) { + // old_num_entries = num_entries; + // added = load_next_entries (); + // } + // if (added > 0) { + // items_changed (old_num_entries, 0, added); + // return Source.CONTINUE; + // } else { + // return Source.REMOVE; + // } }, GLib.Priority.LOW); } public Object? get_item (uint position) { - About.SystemdLogRow? row = null; - lock (journal) { - row = cached_rows.get (position); - if (row != null) { - return row; - } - - int res = journal.seek_head (); - current_line = 0; - if (res != 0) { - critical ("%s", strerror(-res)); - return null; - } - - res = journal.next (); - if (res < 0) { - critical ("%s", strerror(-res)); - return null; - } - - res = journal.next_skip (position); - current_line += position; - if (res < 0) { - critical ("%s", strerror(-res)); - return null; - } - - unowned uint8[] data; - unowned uint8[] comm_data; - res = journal.get_data ("MESSAGE", out data); - if (res != 0) { - critical ("%s", strerror(-res)); - return null; - } - - res = journal.get_data ("_COMM", out comm_data); - if (res != 0) { - //critical ("%s %s", strerror(-res), (string) data); - comm_data = "_COMM=kernel".data; - } - - row = new About.SystemdLogRow (((string)comm_data).offset ("_COMM=".length), ((string)data).offset("MESSAGE=".length)); - cached_rows.set (position, row); - row.weak_ref((obj) => { - cached_rows.foreach_remove ((key, val) => { - return val == obj; - }); - }); + SystemdLogEntry? entry = null; + entry = cached_entries.get (position); + if (entry != null) { + return entry; } - return (owned)row; + entry = new SystemdLogEntry (this, (int) position); + cached_entries[position] = entry; + return entry; } public Type get_item_type () { - return typeof(About.SystemdLogRow); + return typeof(SystemdLogEntry); } public uint get_n_items () { diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala index df6231b73..c82b4e120 100644 --- a/src/Widgets/LogsDialog.vala +++ b/src/Widgets/LogsDialog.vala @@ -3,6 +3,32 @@ * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) */ +public class About.LogRow : Granite.Bin { + private Gtk.Label sender; + private Gtk.Label message; + + construct { + sender = new Gtk.Label (null); + message = new Gtk.Label (null) { + wrap = true, + hexpand = true, + halign = START, + }; + + var box = new Gtk.Box (HORIZONTAL, 6); + box.append (sender); + box.append (message); + + child = box; + } + + public void bind (SystemdLogEntry row) { + row.bind_property ("origin", sender, "label", SYNC_CREATE); + row.bind_property ("message", message, "label", SYNC_CREATE); + row.queue_load (); + } +} + public class About.LogsDialog : Granite.Dialog { public LogsDialog () { @@ -19,31 +45,16 @@ public class About.LogsDialog : Granite.Dialog { }; title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); - var log_listbox = new Gtk.ListBox () { - vexpand = true, - selection_mode = NONE - }; - var model = new About.SystemdLogModel (); - log_listbox.bind_model (model, (obj) => { - unowned var row = (About.SystemdLogRow) obj; - - var origin_label = new Gtk.Label (row.origin); - origin_label.add_css_class (Granite.STYLE_CLASS_H4_LABEL); - var message_label = new Gtk.Label (row.message) { - wrap = true, - hexpand = true, - halign = START, - }; - - var box = new Gtk.Box (HORIZONTAL, 6); - box.append (origin_label); - box.append (message_label); - - return box; - }); + var factory = new Gtk.SignalListItemFactory (); + factory.setup.connect (setup); + factory.bind.connect (bind); + + var selection_model = new Gtk.NoSelection (new About.SystemdLogModel ()); + + var list_view = new Gtk.ListView (selection_model, factory); var scrolled = new Gtk.ScrolledWindow () { - child = log_listbox, + child = list_view, hscrollbar_policy = NEVER, max_content_height = 400, propagate_natural_height = true @@ -65,4 +76,16 @@ public class About.LogsDialog : Granite.Dialog { close (); }); } + + private void setup (Object obj) { + var item = (Gtk.ListItem) obj; + item.child = new LogRow (); + } + + private void bind (Object obj) { + var item = (Gtk.ListItem) obj; + var entry = (SystemdLogEntry) item.item; + var row = (LogRow) item.child; + row.bind (entry); + } } From 1771119bc9bbb934cff61536f89905e07a66d298 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 11 Apr 2025 20:02:14 +0200 Subject: [PATCH 03/17] Add chunks --- src/Utils/SystemdLogModel.vala | 184 +++++++++++---------------------- src/Widgets/LogsDialog.vala | 24 ++--- 2 files changed, 69 insertions(+), 139 deletions(-) diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala index 9f682c149..2a36a54f5 100644 --- a/src/Utils/SystemdLogModel.vala +++ b/src/Utils/SystemdLogModel.vala @@ -4,65 +4,27 @@ */ public class About.SystemdLogEntry : GLib.Object { - public unowned SystemdLogModel model { get; construct; } - public int pos { get; construct; } + public string origin { get; construct; } + public string message { get; construct; } - public string origin { get; private set; default = ""; } - public string message { get; private set; default = ""; } - - private bool loaded = false; - - public SystemdLogEntry (SystemdLogModel model, int pos) { - Object (model: model, pos: pos); - } - - public void queue_load () { - if (loaded) { - return; - } - loaded = true; - - Idle.add (load, GLib.Priority.LOW); - } - - private bool load () { - model.goto (pos); - - unowned uint8[] data; - unowned uint8[] comm_data; - int res = model.journal.get_data ("MESSAGE", out data); - if (res != 0) { - critical ("%s", strerror(-res)); - return Source.REMOVE; - } - - res = model.journal.get_data ("_COMM", out comm_data); - if (res != 0) { - critical ("%s %s", strerror(-res), (string) comm_data); - comm_data = "_COMM=kernel".data; - } - - origin = ((string) comm_data).offset ("_COMM=".length); - message = ((string) data).offset("MESSAGE=".length); - - return Source.REMOVE; + public SystemdLogEntry (string origin, string message) { + Object (origin: origin, message: message); } } public class About.SystemdLogModel : GLib.Object, GLib.ListModel { - private const int CHUNK = 2000; + private const int CHUNK_SIZE = 200; + private const int64 CHUNK_TIME = 1000; // 1 millisecond - private Systemd.Journal _journal; - public Systemd.Journal journal { get { return _journal; } } + private Systemd.Journal journal; - private GLib.HashTable cached_entries; - private int current_line = int.MAX; - private uint num_entries = 0; + private Gee.ArrayList entries; + private bool eof = false; construct { - cached_entries = new GLib.HashTable(GLib.direct_hash, GLib.direct_equal); + entries = new Gee.ArrayList (); - int res = Systemd.Journal.open_namespace (out _journal, null, LOCAL_ONLY); + int res = Systemd.Journal.open_namespace (out journal, null, LOCAL_ONLY); if (res != 0) { critical ("%s", strerror(-res)); return; @@ -79,105 +41,79 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { journal.add_conjunction (); journal.seek_tail (); journal.previous (); - current_line = 0; - load_data.begin (); + load_chunk (); } - public void goto (int pos) { - if (current_line == pos) { + private void load_chunk () { + if (eof) { return; } - var diff = current_line - pos; + var start_items = get_n_items (); - int res; - if (diff < 0) { - res = journal.previous_skip (-diff); - } else { - res = journal.next_skip (diff); - } + Idle.add (() => { + load_timed (); + return !eof && get_n_items () - start_items < CHUNK_SIZE ? Source.CONTINUE : Source.REMOVE; + }); + } - if (res < 0) { - critical ("Failed to go to pos %d: %s", pos, strerror(-res)); + private void load_timed () { + if (eof) { return; } - current_line = pos; - } + var start_time = get_monotonic_time (); - private void load_chunk () { - int to_load = CHUNK; - Idle.add (() => { - load_next_entry (); - to_load--; - return (to_load > 0 ? Source.CONTINUE : Source.REMOVE); - }, GLib.Priority.LOW); + while (get_monotonic_time () - start_time < CHUNK_TIME) { + if (!load_next_entry ()) { + eof = true; + break; + } + } } - private void load_next_entry () { - num_entries++; - items_changed (num_entries - 1, 0, 1); - } + private bool load_next_entry () { + int res = journal.previous (); + if (res == 0) { + return false; + } - private uint load_next_entries () { - if (current_line != num_entries) { - int res = journal.seek_head (); - current_line = 0; - if (res != 0) { - critical ("%s", strerror(-res)); - return 0; - } + if (res < 0) { + critical ("Failed to go to next aka previous entry: %s", strerror (-res)); + return false; + } - if (num_entries > 0) { - res = journal.next_skip (num_entries); - // current_line += num_entries; - if (res < 0) { - critical ("%s", strerror(-res)); - return 0; - } - } + unowned uint8[] data; + unowned uint8[] comm_data; + res = journal.get_data ("MESSAGE", out data); + if (res != 0) { + critical ("Failed to get message: %s", strerror (-res)); + return true; // Don't eof just skip it } - int res = journal.next (); - if (res < 0) { - critical ("%s", strerror(-res)); - return 0; + res = journal.get_data ("_COMM", out comm_data); + if (res != 0) { + critical ("Failed to get sender: %s", strerror(-res)); + comm_data = "_COMM=kernel".data; } - current_line += res; - num_entries += res; - return res; - } + var origin = ((string) comm_data).offset ("_COMM=".length); + var message = ((string) data).offset("MESSAGE=".length); + + entries.add (new SystemdLogEntry (origin, message)); - private async void load_data () { - GLib.Idle.add(() => { - load_chunk (); - return Source.REMOVE; - // uint added = 0, old_num_entries = 0; - // lock (journal) { - // old_num_entries = num_entries; - // added = load_next_entries (); - // } - // if (added > 0) { - // items_changed (old_num_entries, 0, added); - // return Source.CONTINUE; - // } else { - // return Source.REMOVE; - // } - }, GLib.Priority.LOW); + items_changed (entries.size - 1, 0, 1); + + return true; } public Object? get_item (uint position) { - SystemdLogEntry? entry = null; - entry = cached_entries.get (position); - if (entry != null) { - return entry; + if (position >= entries.size) { + return null; + } else { + return entries[(int) position]; } - - entry = new SystemdLogEntry (this, (int) position); - cached_entries[position] = entry; - return entry; } public Type get_item_type () { @@ -185,6 +121,6 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { } public uint get_n_items () { - return num_entries; + return entries.size; } } diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala index c82b4e120..b33d1e144 100644 --- a/src/Widgets/LogsDialog.vala +++ b/src/Widgets/LogsDialog.vala @@ -4,36 +4,32 @@ */ public class About.LogRow : Granite.Bin { - private Gtk.Label sender; + private Gtk.Label origin; private Gtk.Label message; construct { - sender = new Gtk.Label (null); + origin = new Gtk.Label (null); message = new Gtk.Label (null) { - wrap = true, + ellipsize = END, hexpand = true, halign = START, + single_line_mode = true, }; var box = new Gtk.Box (HORIZONTAL, 6); - box.append (sender); + box.append (origin); box.append (message); child = box; } - public void bind (SystemdLogEntry row) { - row.bind_property ("origin", sender, "label", SYNC_CREATE); - row.bind_property ("message", message, "label", SYNC_CREATE); - row.queue_load (); + public void bind (SystemdLogEntry entry) { + origin.label = entry.origin; + message.label = entry.message; } } public class About.LogsDialog : Granite.Dialog { - - public LogsDialog () { - } - construct { title = _("System Logs"); modal = true; @@ -72,9 +68,7 @@ public class About.LogsDialog : Granite.Dialog { add_button (_("Close"), Gtk.ResponseType.CLOSE); - response.connect (() => { - close (); - }); + response.connect (() => close ()); } private void setup (Object obj) { From 7ec68a0a7223b973c4f0be2450dad18303e63062 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 11 Apr 2025 21:08:04 +0200 Subject: [PATCH 04/17] Add search, stable position, refresh --- src/Utils/SystemdLogModel.vala | 62 +++++++++++++++++++++++++++++++--- src/Widgets/LogsDialog.vala | 31 +++++++++++++++-- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala index 2a36a54f5..d01ef65a1 100644 --- a/src/Utils/SystemdLogModel.vala +++ b/src/Utils/SystemdLogModel.vala @@ -10,6 +10,10 @@ public class About.SystemdLogEntry : GLib.Object { public SystemdLogEntry (string origin, string message) { Object (origin: origin, message: message); } + + public bool matches (string term) { + return origin.contains (term) || message.contains (term); + } } public class About.SystemdLogModel : GLib.Object, GLib.ListModel { @@ -17,6 +21,9 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { private const int64 CHUNK_TIME = 1000; // 1 millisecond private Systemd.Journal journal; + private string current_boot_id; + private uint64 current_tail = 0; + private string current_search_term = ""; private Gee.ArrayList entries; private bool eof = false; @@ -37,10 +44,39 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { return; } - journal.add_match ("_BOOT_ID=%s".printf(boot_id.str).data); + current_boot_id = boot_id.str; + + init (); + } + + private void reset () { + if (!entries.is_empty) { + var removed = entries.size; + entries.clear (); + items_changed (0, removed, 0); + } + + eof = false; + journal.flush_matches (); + } + + private void init () { + //TODO: Add exact matches, allow to filter by boot + journal.add_match ("_BOOT_ID=%s".printf(current_boot_id).data); journal.add_conjunction (); - journal.seek_tail (); - journal.previous (); + + if (current_tail == 0) { + journal.seek_tail (); + journal.previous (); + int res = journal.get_realtime_usec (out current_tail); + if (res != 0) { + critical ("Failed to get tail realtime: %s", strerror(-res)); + return; + } + } else { + journal.seek_realtime_usec (current_tail); + journal.previous (); + } load_chunk (); } @@ -101,13 +137,31 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { var origin = ((string) comm_data).offset ("_COMM=".length); var message = ((string) data).offset("MESSAGE=".length); - entries.add (new SystemdLogEntry (origin, message)); + var entry = new SystemdLogEntry (origin, message); + + if (current_search_term.strip () != "" && !entry.matches (current_search_term)) { + return true; + } + + entries.add (entry); items_changed (entries.size - 1, 0, 1); return true; } + public void search (string term) { + reset (); + current_search_term = term; + init (); + } + + public void refresh () { + reset (); + current_tail = 0; + init (); + } + public Object? get_item (uint position) { if (position >= entries.size) { return null; diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala index b33d1e144..79c00a311 100644 --- a/src/Widgets/LogsDialog.vala +++ b/src/Widgets/LogsDialog.vala @@ -30,22 +30,39 @@ public class About.LogRow : Granite.Bin { } public class About.LogsDialog : Granite.Dialog { + private SystemdLogModel model; + construct { title = _("System Logs"); modal = true; + default_height = 500; + default_width = 500; var title_label = new Gtk.Label ( _("System Logs") ) { - halign = START + hexpand = true, + xalign = 0 }; title_label.add_css_class (Granite.STYLE_CLASS_TITLE_LABEL); + var refresh_button = new Gtk.Button.from_icon_name ("view-refresh-symbolic") { + tooltip_text = _("Load new entries") + }; + + var top_box = new Gtk.Box (HORIZONTAL, 6); + top_box.append (title_label); + top_box.append (refresh_button); + + var search_entry = new Gtk.SearchEntry (); + var factory = new Gtk.SignalListItemFactory (); factory.setup.connect (setup); factory.bind.connect (bind); - var selection_model = new Gtk.NoSelection (new About.SystemdLogModel ()); + model = new SystemdLogModel (); + + var selection_model = new Gtk.NoSelection (model); var list_view = new Gtk.ListView (selection_model, factory); @@ -61,13 +78,17 @@ public class About.LogsDialog : Granite.Dialog { }; var box = new Gtk.Box (VERTICAL, 12); - box.append (title_label); + box.append (top_box); + box.append (search_entry); box.append (frame); get_content_area ().append (box); add_button (_("Close"), Gtk.ResponseType.CLOSE); + refresh_button.clicked.connect (model.refresh); + search_entry.search_changed.connect (on_search_changed); + response.connect (() => close ()); } @@ -82,4 +103,8 @@ public class About.LogsDialog : Granite.Dialog { var row = (LogRow) item.child; row.bind (entry); } + + private void on_search_changed (Gtk.SearchEntry entry) { + model.search (entry.text); + } } From a631ff7ddf1ac11cf7ffed60317f229f848328a8 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Fri, 11 Apr 2025 21:19:45 +0200 Subject: [PATCH 05/17] Use ColumnView --- src/Widgets/LogsDialog.vala | 78 ++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala index 79c00a311..2c6b25405 100644 --- a/src/Widgets/LogsDialog.vala +++ b/src/Widgets/LogsDialog.vala @@ -3,29 +3,44 @@ * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) */ -public class About.LogRow : Granite.Bin { - private Gtk.Label origin; - private Gtk.Label message; +public class About.LogCell : Granite.Bin { + public enum CellType { + ORIGIN, + MESSAGE + } + + public CellType cell_type { get; construct; } + + private Gtk.Label label; + + public LogCell (CellType cell_type) { + Object (cell_type: cell_type); + } construct { - origin = new Gtk.Label (null); - message = new Gtk.Label (null) { + label = new Gtk.Label (null) { ellipsize = END, - hexpand = true, - halign = START, single_line_mode = true, + halign = START, }; - var box = new Gtk.Box (HORIZONTAL, 6); - box.append (origin); - box.append (message); + if (cell_type == ORIGIN) { + label.max_width_chars = 10; + label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + } - child = box; + child = label; } public void bind (SystemdLogEntry entry) { - origin.label = entry.origin; - message.label = entry.message; + switch (cell_type) { + case ORIGIN: + label.label = entry.origin; + break; + case MESSAGE: + label.label = entry.message; + break; + } } } @@ -56,18 +71,30 @@ public class About.LogsDialog : Granite.Dialog { var search_entry = new Gtk.SearchEntry (); - var factory = new Gtk.SignalListItemFactory (); - factory.setup.connect (setup); - factory.bind.connect (bind); - model = new SystemdLogModel (); var selection_model = new Gtk.NoSelection (model); - var list_view = new Gtk.ListView (selection_model, factory); + var origin_factory = new Gtk.SignalListItemFactory (); + origin_factory.setup.connect (setup_origin); + origin_factory.bind.connect (bind); + + var origin_column = new Gtk.ColumnViewColumn (_("Sender"), origin_factory); + + var message_factory = new Gtk.SignalListItemFactory (); + message_factory.setup.connect (setup_message); + message_factory.bind.connect (bind); + + var message_column = new Gtk.ColumnViewColumn (_("Message"), message_factory) { + expand = true + }; + + var column_view = new Gtk.ColumnView (selection_model); + column_view.append_column (origin_column); + column_view.append_column (message_column); var scrolled = new Gtk.ScrolledWindow () { - child = list_view, + child = column_view, hscrollbar_policy = NEVER, max_content_height = 400, propagate_natural_height = true @@ -92,16 +119,21 @@ public class About.LogsDialog : Granite.Dialog { response.connect (() => close ()); } - private void setup (Object obj) { + private void setup_origin (Object obj) { + var item = (Gtk.ListItem) obj; + item.child = new LogCell (ORIGIN); + } + + private void setup_message (Object obj) { var item = (Gtk.ListItem) obj; - item.child = new LogRow (); + item.child = new LogCell (MESSAGE); } private void bind (Object obj) { var item = (Gtk.ListItem) obj; var entry = (SystemdLogEntry) item.item; - var row = (LogRow) item.child; - row.bind (entry); + var cell = (LogCell) item.child; + cell.bind (entry); } private void on_search_changed (Gtk.SearchEntry entry) { From f168753d6bf0a9b44206abc1ea484289b46ec0f6 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 01:50:44 +0200 Subject: [PATCH 06/17] Add time header --- src/Utils/SystemdLogModel.vala | 67 ++++++++++++++++++++++++++++++++-- src/Widgets/LogsDialog.vala | 23 +++++++++++- 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala index d01ef65a1..9f7969161 100644 --- a/src/Utils/SystemdLogModel.vala +++ b/src/Utils/SystemdLogModel.vala @@ -6,9 +6,15 @@ public class About.SystemdLogEntry : GLib.Object { public string origin { get; construct; } public string message { get; construct; } + public string formatted_time { get; construct; } - public SystemdLogEntry (string origin, string message) { - Object (origin: origin, message: message); + public uint section_start { get; set; } + + public SystemdLogEntry (string origin, string message, DateTime time) { + Object ( + origin: origin, message: message, + formatted_time: time.format (Granite.DateTime.get_default_time_format (false, true)) + ); } public bool matches (string term) { @@ -16,7 +22,7 @@ public class About.SystemdLogEntry : GLib.Object { } } -public class About.SystemdLogModel : GLib.Object, GLib.ListModel { +public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionModel { private const int CHUNK_SIZE = 200; private const int64 CHUNK_TIME = 1000; // 1 millisecond @@ -27,6 +33,9 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { private Gee.ArrayList entries; private bool eof = false; + private int current_section_start = 0; + private DateTime? current_section_time; + private HashTable section_end_for_start = new HashTable (null, null); construct { entries = new Gee.ArrayList (); @@ -58,6 +67,9 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { eof = false; journal.flush_matches (); + current_section_start = 0; + current_section_time = null; + section_end_for_start.remove_all (); } private void init () { @@ -137,12 +149,28 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { var origin = ((string) comm_data).offset ("_COMM=".length); var message = ((string) data).offset("MESSAGE=".length); - var entry = new SystemdLogEntry (origin, message); + uint64 time; + res = journal.get_realtime_usec (out time); + if (res != 0) { + critical ("Failed to get time: %s", strerror (-res)); + time = 0; + } + + var dt = new DateTime.from_unix_local ((int64) (time / TimeSpan.SECOND)); + + var entry = new SystemdLogEntry (origin, message, dt); if (current_search_term.strip () != "" && !entry.matches (current_search_term)) { return true; } + if (!update_current_range (dt)) { + section_end_for_start[current_section_start] = entries.size; + current_section_start = entries.size; + } + + entry.section_start = current_section_start; + entries.add (entry); items_changed (entries.size - 1, 0, 1); @@ -150,6 +178,20 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { return true; } + private bool update_current_range (DateTime dt) { + if (current_section_time == null) { + current_section_time = dt; + return true; + } + + if (current_section_time.equal (dt)) { + return true; + } else { + current_section_time = dt; + return false; + } + } + public void search (string term) { reset (); current_search_term = term; @@ -177,4 +219,21 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel { public uint get_n_items () { return entries.size; } + + public void get_section (uint for_position, out uint section_start, out uint section_end) { + if (for_position >= entries.size) { + // Documentation mandates this + section_start = entries.size; + section_end = uint.MAX; + return; + } + + section_start = entries[(int) for_position].section_start; + + if (section_start in section_end_for_start) { + section_end = section_end_for_start[section_start]; + } else { + section_end = entries.size; + } + } } diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala index 2c6b25405..484f52fe8 100644 --- a/src/Widgets/LogsDialog.vala +++ b/src/Widgets/LogsDialog.vala @@ -75,6 +75,10 @@ public class About.LogsDialog : Granite.Dialog { var selection_model = new Gtk.NoSelection (model); + var header_factory = new Gtk.SignalListItemFactory (); + header_factory.setup.connect (setup_header); + header_factory.bind.connect (bind_header); + var origin_factory = new Gtk.SignalListItemFactory (); origin_factory.setup.connect (setup_origin); origin_factory.bind.connect (bind); @@ -89,7 +93,9 @@ public class About.LogsDialog : Granite.Dialog { expand = true }; - var column_view = new Gtk.ColumnView (selection_model); + var column_view = new Gtk.ColumnView (selection_model) { + header_factory = header_factory, + }; column_view.append_column (origin_column); column_view.append_column (message_column); @@ -119,6 +125,21 @@ public class About.LogsDialog : Granite.Dialog { response.connect (() => close ()); } + private void setup_header (Object obj) { + var item = (Gtk.ListHeader) obj; + item.child = new Gtk.Label (null) { + halign = START, + use_markup = true + }; + } + + private void bind_header (Object obj) { + var item = (Gtk.ListHeader) obj; + var entry = (SystemdLogEntry) item.item; + var label = (Gtk.Label) item.child; + label.label = "%s".printf (entry.formatted_time); + } + private void setup_origin (Object obj) { var item = (Gtk.ListItem) obj; item.child = new LogCell (ORIGIN); From df6478060e0bf62145654b01e363106929961eed Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 01:57:59 +0200 Subject: [PATCH 07/17] Load new chunks when reaching the bottom --- src/Utils/SystemdLogModel.vala | 16 +++++++++++++--- src/Widgets/LogsDialog.vala | 7 +++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala index 9f7969161..d21edb841 100644 --- a/src/Utils/SystemdLogModel.vala +++ b/src/Utils/SystemdLogModel.vala @@ -32,6 +32,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod private string current_search_term = ""; private Gee.ArrayList entries; + private bool loading = false; private bool eof = false; private int current_section_start = 0; private DateTime? current_section_time; @@ -93,16 +94,25 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod load_chunk (); } - private void load_chunk () { - if (eof) { + public void load_chunk () { + if (eof || loading) { return; } + loading = true; + var start_items = get_n_items (); Idle.add (() => { load_timed (); - return !eof && get_n_items () - start_items < CHUNK_SIZE ? Source.CONTINUE : Source.REMOVE; + var keep_going = !eof && get_n_items () - start_items < CHUNK_SIZE; + + if (keep_going) { + return Source.CONTINUE; + } else { + loading = false; + return Source.REMOVE; + } }); } diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala index 484f52fe8..13fa6b18e 100644 --- a/src/Widgets/LogsDialog.vala +++ b/src/Widgets/LogsDialog.vala @@ -121,6 +121,7 @@ public class About.LogsDialog : Granite.Dialog { refresh_button.clicked.connect (model.refresh); search_entry.search_changed.connect (on_search_changed); + scrolled.edge_reached.connect (on_edge_reached); response.connect (() => close ()); } @@ -160,4 +161,10 @@ public class About.LogsDialog : Granite.Dialog { private void on_search_changed (Gtk.SearchEntry entry) { model.search (entry.text); } + + private void on_edge_reached (Gtk.PositionType pos) { + if (pos == BOTTOM) { + model.load_chunk (); + } + } } From 65e3c5fc4f2852c1a0185e35e662989c61bcefa0 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 02:04:15 +0200 Subject: [PATCH 08/17] Add comments, allow cancelling chunk load --- src/Utils/SystemdLogModel.vala | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala index d21edb841..8352fe842 100644 --- a/src/Utils/SystemdLogModel.vala +++ b/src/Utils/SystemdLogModel.vala @@ -31,9 +31,11 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod private uint64 current_tail = 0; private string current_search_term = ""; + // These fields are for the listmodel and section model implementation + // and only used for loading and in the implementations private Gee.ArrayList entries; - private bool loading = false; private bool eof = false; + private bool loading = false; private int current_section_start = 0; private DateTime? current_section_time; private HashTable section_end_for_start = new HashTable (null, null); @@ -67,10 +69,12 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod } eof = false; - journal.flush_matches (); + loading = false; // Cancels ongoing load current_section_start = 0; current_section_time = null; section_end_for_start.remove_all (); + + journal.flush_matches (); } private void init () { @@ -104,15 +108,13 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod var start_items = get_n_items (); Idle.add (() => { - load_timed (); - var keep_going = !eof && get_n_items () - start_items < CHUNK_SIZE; - - if (keep_going) { - return Source.CONTINUE; - } else { - loading = false; + if (!loading) { // We were cancelled return Source.REMOVE; } + + load_timed (); + loading = !eof && get_n_items () - start_items < CHUNK_SIZE; + return loading ? Source.CONTINUE : Source.REMOVE; }); } From c0f2b3d5ec48f82bfb820dd9659b30b1f2a66dd1 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 14:11:46 +0200 Subject: [PATCH 09/17] Use relative time --- src/Utils/SystemdLogModel.vala | 58 ++++++++++++++++++++++++---------- src/Widgets/LogsDialog.vala | 2 +- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/Utils/SystemdLogModel.vala b/src/Utils/SystemdLogModel.vala index 8352fe842..1afe84770 100644 --- a/src/Utils/SystemdLogModel.vala +++ b/src/Utils/SystemdLogModel.vala @@ -6,29 +6,57 @@ public class About.SystemdLogEntry : GLib.Object { public string origin { get; construct; } public string message { get; construct; } - public string formatted_time { get; construct; } + public string relative_time { get; construct; } public uint section_start { get; set; } public SystemdLogEntry (string origin, string message, DateTime time) { Object ( origin: origin, message: message, - formatted_time: time.format (Granite.DateTime.get_default_time_format (false, true)) + relative_time: format_time (time) ); } public bool matches (string term) { return origin.contains (term) || message.contains (term); } + + private static string format_time (DateTime time) { + var diff = SystemdLogModel.get_stable_now ().difference (time); + if (diff < TimeSpan.SECOND) { + return _("Now"); + } else if (diff < TimeSpan.MINUTE) { + var seconds = diff / TimeSpan.SECOND; + return dngettext (GETTEXT_PACKAGE, "%ds ago", "%ds ago", (ulong) seconds).printf ((int) seconds); + } else if (diff < TimeSpan.HOUR) { + var minutes = diff / TimeSpan.MINUTE; + var seconds = (diff - minutes * TimeSpan.MINUTE) / TimeSpan.SECOND; + + if (seconds == 0) { + return dngettext (GETTEXT_PACKAGE, "%dm ago", "%dm ago", (ulong) minutes).printf ((int) minutes); + } + + // I think the plural form is according to the last one?? + return dngettext (GETTEXT_PACKAGE, "%dm %ds ago", "%dm %ds ago", (ulong) seconds).printf ((int) minutes, (int) seconds); + } + + return time.format (Granite.DateTime.get_default_time_format ()); + } } public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionModel { + private static DateTime? now; + + public static DateTime get_stable_now () { + return now; + } + private const int CHUNK_SIZE = 200; private const int64 CHUNK_TIME = 1000; // 1 millisecond private Systemd.Journal journal; - private string current_boot_id; - private uint64 current_tail = 0; + private Systemd.Id128 current_boot_id; + private uint64 current_tail_time = 0; private string current_search_term = ""; // These fields are for the listmodel and section model implementation @@ -49,15 +77,12 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod return; } - Systemd.Id128 boot_id; - res = Systemd.Id128.boot (out boot_id); + res = Systemd.Id128.boot (out current_boot_id); if (res != 0) { critical ("%s", strerror(-res)); return; } - current_boot_id = boot_id.str; - init (); } @@ -78,20 +103,22 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod } private void init () { + now = new DateTime.now_utc (); + //TODO: Add exact matches, allow to filter by boot - journal.add_match ("_BOOT_ID=%s".printf(current_boot_id).data); + journal.add_match ("_BOOT_ID=%s".printf(current_boot_id.str).data); journal.add_conjunction (); - if (current_tail == 0) { + if (current_tail_time == 0) { journal.seek_tail (); journal.previous (); - int res = journal.get_realtime_usec (out current_tail); + int res = journal.get_realtime_usec (out current_tail_time); if (res != 0) { critical ("Failed to get tail realtime: %s", strerror(-res)); return; } } else { - journal.seek_realtime_usec (current_tail); + journal.seek_realtime_usec (current_tail_time); journal.previous (); } @@ -154,7 +181,6 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod res = journal.get_data ("_COMM", out comm_data); if (res != 0) { - critical ("Failed to get sender: %s", strerror(-res)); comm_data = "_COMM=kernel".data; } @@ -168,7 +194,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod time = 0; } - var dt = new DateTime.from_unix_local ((int64) (time / TimeSpan.SECOND)); + var dt = new DateTime.from_unix_utc ((int64) (time / TimeSpan.SECOND)); var entry = new SystemdLogEntry (origin, message, dt); @@ -196,7 +222,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod return true; } - if (current_section_time.equal (dt)) { + if (current_section_time.difference (dt) <= TimeSpan.SECOND) { return true; } else { current_section_time = dt; @@ -212,7 +238,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod public void refresh () { reset (); - current_tail = 0; + current_tail_time = 0; init (); } diff --git a/src/Widgets/LogsDialog.vala b/src/Widgets/LogsDialog.vala index 13fa6b18e..b6826d419 100644 --- a/src/Widgets/LogsDialog.vala +++ b/src/Widgets/LogsDialog.vala @@ -138,7 +138,7 @@ public class About.LogsDialog : Granite.Dialog { var item = (Gtk.ListHeader) obj; var entry = (SystemdLogEntry) item.item; var label = (Gtk.Label) item.child; - label.label = "%s".printf (entry.formatted_time); + label.label = "%s".printf (entry.relative_time); } private void setup_origin (Object obj) { From 6ec4485438d6a524a294da4cd710c786c0462513 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 14:13:17 +0200 Subject: [PATCH 10/17] Restructure --- src/{Widgets => LogsDialog}/LogsDialog.vala | 0 src/{Utils => LogsDialog}/SystemdLogModel.vala | 0 src/meson.build | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{Widgets => LogsDialog}/LogsDialog.vala (100%) rename src/{Utils => LogsDialog}/SystemdLogModel.vala (100%) diff --git a/src/Widgets/LogsDialog.vala b/src/LogsDialog/LogsDialog.vala similarity index 100% rename from src/Widgets/LogsDialog.vala rename to src/LogsDialog/LogsDialog.vala diff --git a/src/Utils/SystemdLogModel.vala b/src/LogsDialog/SystemdLogModel.vala similarity index 100% rename from src/Utils/SystemdLogModel.vala rename to src/LogsDialog/SystemdLogModel.vala diff --git a/src/meson.build b/src/meson.build index 4e256796b..e1964c688 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,8 +4,9 @@ plug_files = files( 'DBus' / 'Drivers.vala', 'Interfaces/FirmwareClient.vala', 'Interfaces/LoginManager.vala', + 'LogsDialog' / 'LogsDialog.vala', + 'LogsDialog' / 'SystemdLogModel.vala', 'Utils/ARMPartDecoder.vala', - 'Utils' / 'SystemdLogModel.vala', 'Views' / 'DriversView.vala', 'Views/FirmwareReleaseView.vala', 'Views/FirmwareView.vala', @@ -14,7 +15,6 @@ plug_files = files( 'Widgets/FirmwareUpdateRow.vala', 'Widgets' / 'DriverRow.vala', 'Widgets' / 'UpdateDetailsDialog.vala', - 'Widgets' / 'LogsDialog.vala', ) switchboard_dep = dependency('switchboard-3') From d6a74fe5f6a2830fed6e186a50354cfcaedc7713 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 14:21:16 +0200 Subject: [PATCH 11/17] Slight performance improvements, add some comments --- src/LogsDialog/SystemdLogModel.vala | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/LogsDialog/SystemdLogModel.vala b/src/LogsDialog/SystemdLogModel.vala index 1afe84770..ae31e12fd 100644 --- a/src/LogsDialog/SystemdLogModel.vala +++ b/src/LogsDialog/SystemdLogModel.vala @@ -150,6 +150,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod return; } + var start_entries = entries.size; var start_time = get_monotonic_time (); while (get_monotonic_time () - start_time < CHUNK_TIME) { @@ -158,6 +159,8 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod break; } } + + items_changed (start_entries, 0, entries.size - start_entries); } private bool load_next_entry () { @@ -198,21 +201,21 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod var entry = new SystemdLogEntry (origin, message, dt); + // Filter if we're searching. We drop them and don't add them and use a filter model + // because when searching for e.g. a non existent term this would fill up memory *quick* if (current_search_term.strip () != "" && !entry.matches (current_search_term)) { return true; } + // Update sections (group entries that have a timestamp from the same second) if (!update_current_range (dt)) { section_end_for_start[current_section_start] = entries.size; current_section_start = entries.size; } - entry.section_start = current_section_start; entries.add (entry); - items_changed (entries.size - 1, 0, 1); - return true; } @@ -232,6 +235,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod public void search (string term) { reset (); + //TODO: tokenize etc. current_search_term = term; init (); } From a0d300719a83674167e62dc261a31778ced2ab86 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 14:50:48 +0200 Subject: [PATCH 12/17] Add detailsview --- src/LogsDialog/LogDetailsView.vala | 85 +++++++++++++++++++++++++++++ src/LogsDialog/LogsDialog.vala | 21 ++++++- src/LogsDialog/SystemdLogModel.vala | 9 +-- src/meson.build | 1 + 4 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 src/LogsDialog/LogDetailsView.vala diff --git a/src/LogsDialog/LogDetailsView.vala b/src/LogsDialog/LogDetailsView.vala new file mode 100644 index 000000000..5e8391b6b --- /dev/null +++ b/src/LogsDialog/LogDetailsView.vala @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class About.LogDetailsView : Adw.NavigationPage { + public SystemdLogEntry entry { + set { + origin.label = value.origin; + timestamp.label = value.dt.format ("%s %s".printf ( + Granite.DateTime.get_default_date_format (), + Granite.DateTime.get_default_time_format (false, true) + )); + message.label = value.message; + title = value.origin; + } + } + + private Gtk.Label origin; + private Gtk.Label timestamp; + private Gtk.Label message; + + construct { + var origin_label = new Gtk.Label (_("Sender:")) { + halign = END + }; + origin_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + + origin = new Gtk.Label (null) { + halign = START, + wrap = true, + wrap_mode = WORD_CHAR + }; + + var timestamp_label = new Gtk.Label (_("Timestamp:")) { + halign = END + }; + timestamp_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + + timestamp = new Gtk.Label (null) { + halign = START, + wrap = true, + wrap_mode = WORD_CHAR + }; + + var message_label = new Gtk.Label (_("Message:")) { + halign = END + }; + message_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + + message = new Gtk.Label (null) { + halign = START, + wrap = true, + wrap_mode = WORD_CHAR + }; + + var grid = new Gtk.Grid () { + column_spacing = 3, + row_spacing = 6, + margin_start = 6, + margin_end = 6, + margin_top = 6, + margin_bottom = 6, + halign = CENTER + }; + grid.attach (origin_label, 0, 0); + grid.attach (origin, 1, 0); + grid.attach (timestamp_label, 0, 1); + grid.attach (timestamp, 1, 1); + grid.attach (message_label, 0, 2); + grid.attach (message, 1, 2); + + var scrolled = new Gtk.ScrolledWindow () { + child = grid + }; + + var toolbar_view = new Adw.ToolbarView () { + content = scrolled, + top_bar_style = RAISED + }; + toolbar_view.add_top_bar (new Adw.HeaderBar ()); + + child = toolbar_view; + } +} diff --git a/src/LogsDialog/LogsDialog.vala b/src/LogsDialog/LogsDialog.vala index b6826d419..d8e05dd6a 100644 --- a/src/LogsDialog/LogsDialog.vala +++ b/src/LogsDialog/LogsDialog.vala @@ -1,6 +1,6 @@ /* * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ public class About.LogCell : Granite.Bin { @@ -46,6 +46,8 @@ public class About.LogCell : Granite.Bin { public class About.LogsDialog : Granite.Dialog { private SystemdLogModel model; + private LogDetailsView details_view; + private Adw.NavigationView navigation_view; construct { title = _("System Logs"); @@ -106,8 +108,17 @@ public class About.LogsDialog : Granite.Dialog { propagate_natural_height = true }; + var list_page = new Adw.NavigationPage (scrolled, "list"); + + details_view = new LogDetailsView (); + + navigation_view = new Adw.NavigationView (); + navigation_view.add (list_page); + var frame = new Gtk.Frame (null) { - child = scrolled + child = navigation_view, + hexpand = true, + vexpand = true }; var box = new Gtk.Box (VERTICAL, 12); @@ -121,6 +132,7 @@ public class About.LogsDialog : Granite.Dialog { refresh_button.clicked.connect (model.refresh); search_entry.search_changed.connect (on_search_changed); + column_view.activate.connect (on_activate); scrolled.edge_reached.connect (on_edge_reached); response.connect (() => close ()); @@ -162,6 +174,11 @@ public class About.LogsDialog : Granite.Dialog { model.search (entry.text); } + private void on_activate (uint pos) { + details_view.entry = (SystemdLogEntry) model.get_item (pos); + navigation_view.push (details_view); + } + private void on_edge_reached (Gtk.PositionType pos) { if (pos == BOTTOM) { model.load_chunk (); diff --git a/src/LogsDialog/SystemdLogModel.vala b/src/LogsDialog/SystemdLogModel.vala index ae31e12fd..ff5159f6d 100644 --- a/src/LogsDialog/SystemdLogModel.vala +++ b/src/LogsDialog/SystemdLogModel.vala @@ -1,18 +1,19 @@ /* * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2024 elementary, Inc. (https://elementary.io) + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ public class About.SystemdLogEntry : GLib.Object { public string origin { get; construct; } public string message { get; construct; } + public DateTime dt { get; construct; } public string relative_time { get; construct; } public uint section_start { get; set; } public SystemdLogEntry (string origin, string message, DateTime time) { Object ( - origin: origin, message: message, + origin: origin, message: message, dt: time, relative_time: format_time (time) ); } @@ -150,7 +151,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod return; } - var start_entries = entries.size; + var start_n_items = entries.size; var start_time = get_monotonic_time (); while (get_monotonic_time () - start_time < CHUNK_TIME) { @@ -160,7 +161,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod } } - items_changed (start_entries, 0, entries.size - start_entries); + items_changed (start_n_items, 0, entries.size - start_n_items); } private bool load_next_entry () { diff --git a/src/meson.build b/src/meson.build index e1964c688..525c7a69e 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,6 +4,7 @@ plug_files = files( 'DBus' / 'Drivers.vala', 'Interfaces/FirmwareClient.vala', 'Interfaces/LoginManager.vala', + 'LogsDialog' / 'LogDetailsView.vala', 'LogsDialog' / 'LogsDialog.vala', 'LogsDialog' / 'SystemdLogModel.vala', 'Utils/ARMPartDecoder.vala', From 805e6858335e300b273a0320a3e3712d6f4aa0af Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 14:53:17 +0200 Subject: [PATCH 13/17] Fix CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a562216a..a80f6db06 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y meson libadwaita-1-dev libfwupd-dev libgranite-7-dev libgtk-4-dev libgtop2-dev libgudev-1.0-dev libudisks2-dev libswitchboard-3-dev libappstream-dev libpackagekit-glib2-dev libpolkit-gobject-1-dev libsoup-3.0-dev valac + apt install -y meson libadwaita-1-dev libfwupd-dev libgranite-7-dev libgtk-4-dev libgtop2-dev libgudev-1.0-dev libudisks2-dev libswitchboard-3-dev libappstream-dev libpackagekit-glib2-dev libpolkit-gobject-1-dev libsoup-3.0-dev libsystemd-dev valac - name: Build env: DESTDIR: out From 892052ca09908eec999962f83209b3bc917ee951 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 17:19:26 +0200 Subject: [PATCH 14/17] Reshuffle files, separate files for cell and entry --- src/LogDialog/LogCell.vala | 45 ++++++++++++++++++ .../LogDetailsView.vala | 0 .../LogDialog.vala} | 43 +---------------- src/LogDialog/SystemdLogEntry.vala | 46 +++++++++++++++++++ .../SystemdLogModel.vala | 42 ----------------- src/Views/OperatingSystemView.vala | 5 +- src/meson.build | 8 ++-- 7 files changed, 99 insertions(+), 90 deletions(-) create mode 100644 src/LogDialog/LogCell.vala rename src/{LogsDialog => LogDialog}/LogDetailsView.vala (100%) rename src/{LogsDialog/LogsDialog.vala => LogDialog/LogDialog.vala} (82%) create mode 100644 src/LogDialog/SystemdLogEntry.vala rename src/{LogsDialog => LogDialog}/SystemdLogModel.vala (80%) diff --git a/src/LogDialog/LogCell.vala b/src/LogDialog/LogCell.vala new file mode 100644 index 000000000..8e7a2106d --- /dev/null +++ b/src/LogDialog/LogCell.vala @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class About.LogCell : Granite.Bin { + public enum CellType { + ORIGIN, + MESSAGE + } + + public CellType cell_type { get; construct; } + + private Gtk.Label label; + + public LogCell (CellType cell_type) { + Object (cell_type: cell_type); + } + + construct { + label = new Gtk.Label (null) { + ellipsize = END, + single_line_mode = true, + halign = START, + }; + + if (cell_type == ORIGIN) { + label.max_width_chars = 10; + label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + } + + child = label; + } + + public void bind (SystemdLogEntry entry) { + switch (cell_type) { + case ORIGIN: + label.label = entry.origin; + break; + case MESSAGE: + label.label = entry.message; + break; + } + } +} diff --git a/src/LogsDialog/LogDetailsView.vala b/src/LogDialog/LogDetailsView.vala similarity index 100% rename from src/LogsDialog/LogDetailsView.vala rename to src/LogDialog/LogDetailsView.vala diff --git a/src/LogsDialog/LogsDialog.vala b/src/LogDialog/LogDialog.vala similarity index 82% rename from src/LogsDialog/LogsDialog.vala rename to src/LogDialog/LogDialog.vala index d8e05dd6a..3e317589c 100644 --- a/src/LogsDialog/LogsDialog.vala +++ b/src/LogDialog/LogDialog.vala @@ -3,48 +3,7 @@ * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ -public class About.LogCell : Granite.Bin { - public enum CellType { - ORIGIN, - MESSAGE - } - - public CellType cell_type { get; construct; } - - private Gtk.Label label; - - public LogCell (CellType cell_type) { - Object (cell_type: cell_type); - } - - construct { - label = new Gtk.Label (null) { - ellipsize = END, - single_line_mode = true, - halign = START, - }; - - if (cell_type == ORIGIN) { - label.max_width_chars = 10; - label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); - } - - child = label; - } - - public void bind (SystemdLogEntry entry) { - switch (cell_type) { - case ORIGIN: - label.label = entry.origin; - break; - case MESSAGE: - label.label = entry.message; - break; - } - } -} - -public class About.LogsDialog : Granite.Dialog { +public class About.LogDialog : Granite.Dialog { private SystemdLogModel model; private LogDetailsView details_view; private Adw.NavigationView navigation_view; diff --git a/src/LogDialog/SystemdLogEntry.vala b/src/LogDialog/SystemdLogEntry.vala new file mode 100644 index 000000000..8f47cad72 --- /dev/null +++ b/src/LogDialog/SystemdLogEntry.vala @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + + public class About.SystemdLogEntry : GLib.Object { + public string origin { get; construct; } + public string message { get; construct; } + public DateTime dt { get; construct; } + public string relative_time { get; construct; } + + public uint section_start { get; set; } + + public SystemdLogEntry (string origin, string message, DateTime time) { + Object ( + origin: origin, message: message, dt: time, + relative_time: format_time (time) + ); + } + + public bool matches (string term) { + return origin.contains (term) || message.contains (term); + } + + private static string format_time (DateTime time) { + var diff = SystemdLogModel.get_stable_now ().difference (time); + if (diff < TimeSpan.SECOND) { + return _("Now"); + } else if (diff < TimeSpan.MINUTE) { + var seconds = diff / TimeSpan.SECOND; + return dngettext (GETTEXT_PACKAGE, "%ds ago", "%ds ago", (ulong) seconds).printf ((int) seconds); + } else if (diff < TimeSpan.HOUR) { + var minutes = diff / TimeSpan.MINUTE; + var seconds = (diff - minutes * TimeSpan.MINUTE) / TimeSpan.SECOND; + + if (seconds == 0) { + return dngettext (GETTEXT_PACKAGE, "%dm ago", "%dm ago", (ulong) minutes).printf ((int) minutes); + } + + // I think the plural form is according to the last one?? + return dngettext (GETTEXT_PACKAGE, "%dm %ds ago", "%dm %ds ago", (ulong) seconds).printf ((int) minutes, (int) seconds); + } + + return time.format (Granite.DateTime.get_default_time_format ()); + } +} diff --git a/src/LogsDialog/SystemdLogModel.vala b/src/LogDialog/SystemdLogModel.vala similarity index 80% rename from src/LogsDialog/SystemdLogModel.vala rename to src/LogDialog/SystemdLogModel.vala index ff5159f6d..1c0f500fe 100644 --- a/src/LogsDialog/SystemdLogModel.vala +++ b/src/LogDialog/SystemdLogModel.vala @@ -3,48 +3,6 @@ * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) */ -public class About.SystemdLogEntry : GLib.Object { - public string origin { get; construct; } - public string message { get; construct; } - public DateTime dt { get; construct; } - public string relative_time { get; construct; } - - public uint section_start { get; set; } - - public SystemdLogEntry (string origin, string message, DateTime time) { - Object ( - origin: origin, message: message, dt: time, - relative_time: format_time (time) - ); - } - - public bool matches (string term) { - return origin.contains (term) || message.contains (term); - } - - private static string format_time (DateTime time) { - var diff = SystemdLogModel.get_stable_now ().difference (time); - if (diff < TimeSpan.SECOND) { - return _("Now"); - } else if (diff < TimeSpan.MINUTE) { - var seconds = diff / TimeSpan.SECOND; - return dngettext (GETTEXT_PACKAGE, "%ds ago", "%ds ago", (ulong) seconds).printf ((int) seconds); - } else if (diff < TimeSpan.HOUR) { - var minutes = diff / TimeSpan.MINUTE; - var seconds = (diff - minutes * TimeSpan.MINUTE) / TimeSpan.SECOND; - - if (seconds == 0) { - return dngettext (GETTEXT_PACKAGE, "%dm ago", "%dm ago", (ulong) minutes).printf ((int) minutes); - } - - // I think the plural form is according to the last one?? - return dngettext (GETTEXT_PACKAGE, "%dm %ds ago", "%dm %ds ago", (ulong) seconds).printf ((int) minutes, (int) seconds); - } - - return time.format (Granite.DateTime.get_default_time_format ()); - } -} - public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionModel { private static DateTime? now; diff --git a/src/Views/OperatingSystemView.vala b/src/Views/OperatingSystemView.vala index 8fea00bdf..3fecbded3 100644 --- a/src/Views/OperatingSystemView.vala +++ b/src/Views/OperatingSystemView.vala @@ -405,10 +405,9 @@ public class About.OperatingSystemView : Gtk.Box { }); log_button.clicked.connect (() => { - var logs_dialog = new LogsDialog () { + new LogDialog () { transient_for = (Gtk.Window) get_root () - }; - logs_dialog.present (); + }.present (); }); links_list.row_activated.connect ((row) => { diff --git a/src/meson.build b/src/meson.build index 525c7a69e..ebc457450 100644 --- a/src/meson.build +++ b/src/meson.build @@ -4,9 +4,11 @@ plug_files = files( 'DBus' / 'Drivers.vala', 'Interfaces/FirmwareClient.vala', 'Interfaces/LoginManager.vala', - 'LogsDialog' / 'LogDetailsView.vala', - 'LogsDialog' / 'LogsDialog.vala', - 'LogsDialog' / 'SystemdLogModel.vala', + 'LogDialog' / 'LogCell.vala', + 'LogDialog' / 'LogDetailsView.vala', + 'LogDialog' / 'LogDialog.vala', + 'LogDialog' / 'SystemdLogEntry.vala', + 'LogDialog' / 'SystemdLogModel.vala', 'Utils/ARMPartDecoder.vala', 'Views' / 'DriversView.vala', 'Views/FirmwareReleaseView.vala', From 4042d3ba5dd039b797fc6826b0e7e9fbe8a2d8db Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 17:22:29 +0200 Subject: [PATCH 15/17] Single click activate, start fixing lint --- src/LogDialog/LogDialog.vala | 1 + src/LogDialog/SystemdLogModel.vala | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/LogDialog/LogDialog.vala b/src/LogDialog/LogDialog.vala index 3e317589c..2edcb6d9a 100644 --- a/src/LogDialog/LogDialog.vala +++ b/src/LogDialog/LogDialog.vala @@ -56,6 +56,7 @@ public class About.LogDialog : Granite.Dialog { var column_view = new Gtk.ColumnView (selection_model) { header_factory = header_factory, + single_click_activate = true, }; column_view.append_column (origin_column); column_view.append_column (message_column); diff --git a/src/LogDialog/SystemdLogModel.vala b/src/LogDialog/SystemdLogModel.vala index 1c0f500fe..848d5d374 100644 --- a/src/LogDialog/SystemdLogModel.vala +++ b/src/LogDialog/SystemdLogModel.vala @@ -32,13 +32,13 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod int res = Systemd.Journal.open_namespace (out journal, null, LOCAL_ONLY); if (res != 0) { - critical ("%s", strerror(-res)); + critical ("%s", strerror (-res)); return; } res = Systemd.Id128.boot (out current_boot_id); if (res != 0) { - critical ("%s", strerror(-res)); + critical ("%s", strerror (-res)); return; } @@ -147,7 +147,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod } var origin = ((string) comm_data).offset ("_COMM=".length); - var message = ((string) data).offset("MESSAGE=".length); + var message = ((string) data).offset ("MESSAGE=".length); uint64 time; res = journal.get_realtime_usec (out time); @@ -214,7 +214,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod } public Type get_item_type () { - return typeof(SystemdLogEntry); + return typeof (SystemdLogEntry); } public uint get_n_items () { From 3e0d1c21fd417ac7eafe46dd88de2b28308950e6 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 17:24:46 +0200 Subject: [PATCH 16/17] More lint fixes --- src/LogDialog/SystemdLogModel.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LogDialog/SystemdLogModel.vala b/src/LogDialog/SystemdLogModel.vala index 848d5d374..d652ecf9f 100644 --- a/src/LogDialog/SystemdLogModel.vala +++ b/src/LogDialog/SystemdLogModel.vala @@ -65,7 +65,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod now = new DateTime.now_utc (); //TODO: Add exact matches, allow to filter by boot - journal.add_match ("_BOOT_ID=%s".printf(current_boot_id.str).data); + journal.add_match ("_BOOT_ID=%s".printf (current_boot_id.str).data); journal.add_conjunction (); if (current_tail_time == 0) { @@ -73,7 +73,7 @@ public class About.SystemdLogModel : GLib.Object, GLib.ListModel, Gtk.SectionMod journal.previous (); int res = journal.get_realtime_usec (out current_tail_time); if (res != 0) { - critical ("Failed to get tail realtime: %s", strerror(-res)); + critical ("Failed to get tail realtime: %s", strerror (-res)); return; } } else { From 18a9e45f60b0c34155fc33cc348b5730ef711fc6 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 12 Apr 2025 17:59:25 +0200 Subject: [PATCH 17/17] Fix button position --- src/Views/OperatingSystemView.vala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Views/OperatingSystemView.vala b/src/Views/OperatingSystemView.vala index 3fecbded3..b427ed039 100644 --- a/src/Views/OperatingSystemView.vala +++ b/src/Views/OperatingSystemView.vala @@ -359,10 +359,10 @@ public class About.OperatingSystemView : Gtk.Box { software_grid.attach (title, 1, 0); software_grid.attach (log_button, 2, 0); - software_grid.attach (kernel_version_label, 1, 2); - software_grid.attach (updates_list, 1, 3); - software_grid.attach (sponsor_list, 1, 4); - software_grid.attach (links_list, 1, 5); + software_grid.attach (kernel_version_label, 1, 2, 2); + software_grid.attach (updates_list, 1, 3, 2); + software_grid.attach (sponsor_list, 1, 4, 2); + software_grid.attach (links_list, 1, 5, 2); var clamp = new Adw.Clamp () { child = software_grid,