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 98cb7a084..8b148a480 100644
--- a/src/Plug.vala
+++ b/src/Plug.vala
@@ -51,6 +51,7 @@ public class About.Plug : Switchboard.Plug {
public override Gtk.Widget get_widget () {
if (main_grid == 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 b99d3ddf9..c2ff31fe5 100644
--- a/src/Views/OperatingSystemView.vala
+++ b/src/Views/OperatingSystemView.vala
@@ -300,6 +300,13 @@ public class About.OperatingSystemView : Gtk.Box {
hexpand = true
};
+ var log_button = new Gtk.Button.from_icon_name ("system-logs-symbolic") {
+ tooltip_text = _("System logs"),
+ halign = END,
+ valign = BASELINE_CENTER,
+ hexpand = false
+ };
+
var button_grid = new Gtk.Box (HORIZONTAL, 6);
button_grid.append (settings_restore_button);
button_grid.append (bug_button);
@@ -312,10 +319,11 @@ 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);
- software_grid.attach (links_list, 1, 4);
+ software_grid.attach (kernel_version_label, 1, 2, 2);
+ software_grid.attach (updates_list, 1, 3, 2);
+ software_grid.attach (links_list, 1, 4, 2);
var clamp = new Adw.Clamp () {
child = software_grid
@@ -353,6 +361,13 @@ public class About.OperatingSystemView : Gtk.Box {
}
});
+ 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 949033662..70cc660eb 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')
@@ -41,6 +43,7 @@ shared_module(
dependency('gtk4'),
dependency('libadwaita-1'),
dependency('libgtop-2.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;
+ }
+ }
+}