From ce445191e302eabd0bb97e60a1779270c123f6f3 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Tue, 15 Sep 2026 14:19:14 -0700 Subject: [PATCH 1/8] Add Logs View --- .github/workflows/ci.yml | 2 +- README.md | 2 +- data/stylesheet/_index.scss | 10 ++++++++++ meson.build | 1 + src/MainWindow.vala | 2 ++ src/meson.build | 5 +++++ 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ab4c6756..4b55f9226 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: apt install -y libgala-dev libgee-0.8-dev libglib2.0-dev libgranite-7-dev libgtk-4-dev libadwaita-1-dev \ libdbus-glib-1-dev libgtop2-dev libwingpanel-9-dev libudisks2-dev \ libxnvctrl0 libxnvctrl-dev libcurl4-gnutls-dev libflatpak-dev libjson-glib-dev \ - liblivechart-2-dev libpci-dev \ + liblivechart-2-dev libsystemd-dev libpci-dev \ meson valac sassc git - name: Build run: | diff --git a/README.md b/README.md index 035654cb9..d0cb19bab 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you plan to install WITH a wingpanel-indicator ```bash -sudo apt install sassc valac libgtk-4-dev libgee-0.8-dev libgranite-7-dev libgtop2-dev libadwaita-1-dev libudisks2-dev libjson-glib-dev libflatpak-dev libxnvctrl-dev liblivechart-2-dev libpci-dev libwingpanel-9-dev meson +sudo apt install sassc valac libgtk-4-dev libgee-0.8-dev libgranite-7-dev libgtop2-dev libadwaita-1-dev libudisks2-dev libjson-glib-dev libflatpak-dev libsystemd-dev libxnvctrl-dev liblivechart-2-dev libpci-dev libwingpanel-9-dev meson ``` Alternatively, if you plan to install WITHOUT a wingpanel-indicator diff --git a/data/stylesheet/_index.scss b/data/stylesheet/_index.scss index 3bc5894aa..5db443721 100644 --- a/data/stylesheet/_index.scss +++ b/data/stylesheet/_index.scss @@ -167,3 +167,13 @@ header.small-label { } } } + +columnview { + cell { + padding: 0.5em; + } + + row:nth-child(odd) { + background: #{'@bg_color'}; + } +} diff --git a/meson.build b/meson.build index 9e29ed18f..5d7297372 100644 --- a/meson.build +++ b/meson.build @@ -35,6 +35,7 @@ app_dependencies = [ dependency('gobject-2.0'), dependency('libgtop-2.0'), dependency('libadwaita-1', version: '>=1.0.0'), + dependency('libsystemd'), dependency('gtk4-x11'), dependency('udisks2'), dependency('json-glib-1.0'), diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 1d0e3e3a5..876495082 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -27,12 +27,14 @@ public class Monitor.MainWindow : Gtk.ApplicationWindow { var resources = new Resources (); process_view = new ProcessView (); + var log_view = new LogView (); var system_view = new SystemView (resources); var stack = new Gtk.Stack () { transition_type = SLIDE_LEFT_RIGHT }; stack.add_titled (process_view, "process_view", _("Processes")); + stack.add_titled (log_view, "log-view", _("Logs")); stack.add_titled (system_view, "system_view", _("System")); var stack_switcher = new Gtk.StackSwitcher () { diff --git a/src/meson.build b/src/meson.build index 036d04339..6bfc3d36d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,6 +20,11 @@ source_app_files = [ 'Views/SystemView/SystemStorageView.vala', 'Views/SystemView/SystemGPUView.vala', + 'Views/LogView/LogCell.vala', + 'Views/LogView/LogView.vala', + 'Views/LogView/SystemdLogEntry.vala', + 'Views/LogView/SystemdLogModel.vala', + # Widgets related only to ProcessInfoView 'Views/ProcessView/ProcessInfoView/ProcessInfoHeader.vala', 'Views/ProcessView/ProcessInfoView/ProcessInfoCPURAM.vala', From 0277bb9ee5235316dbc02e8b58474d5aa4591304 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Tue, 15 Sep 2026 14:19:14 -0700 Subject: [PATCH 2/8] Add Logs View Add missing files --- src/Views/LogView/LogCell.vala | 47 +++++ src/Views/LogView/LogView.vala | 112 ++++++++++++ src/Views/LogView/SystemdLogEntry.vala | 46 +++++ src/Views/LogView/SystemdLogModel.vala | 240 +++++++++++++++++++++++++ vapi/libsystemd.vapi | 128 +++++++++++++ 5 files changed, 573 insertions(+) create mode 100644 src/Views/LogView/LogCell.vala create mode 100644 src/Views/LogView/LogView.vala create mode 100644 src/Views/LogView/SystemdLogEntry.vala create mode 100644 src/Views/LogView/SystemdLogModel.vala create mode 100644 vapi/libsystemd.vapi diff --git a/src/Views/LogView/LogCell.vala b/src/Views/LogView/LogCell.vala new file mode 100644 index 000000000..f2394cd85 --- /dev/null +++ b/src/Views/LogView/LogCell.vala @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Monitor.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); + + switch (cell_type) { + case ORIGIN: + label.add_css_class (Granite.CssClass.DIM); + label.width_chars = 15; + label.xalign = 1; + break; + case MESSAGE: + label.halign = START; + break; + } + + 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/Views/LogView/LogView.vala b/src/Views/LogView/LogView.vala new file mode 100644 index 000000000..fb150edcc --- /dev/null +++ b/src/Views/LogView/LogView.vala @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Monitor.LogView : Granite.Bin { + private SystemdLogModel model; + + construct { + + var search_entry = new Gtk.SearchEntry () { + hexpand = true, + placeholder_text = _("Search") + }; + + var refresh_button = new Gtk.Button.from_icon_name ("view-refresh-symbolic") { + tooltip_text = _("Load new entries") + }; + + var top_box = new Granite.Box (HORIZONTAL) { + margin_top = 12, + margin_end = 12, + margin_start = 12 + }; + top_box.append (search_entry); + top_box.append (refresh_button); + + model = new SystemdLogModel (); + + 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); + + 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) { + header_factory = header_factory, + reorderable = false, + vexpand = true + }; + column_view.append_column (origin_column); + column_view.append_column (message_column); + + var scrolled = new Gtk.ScrolledWindow () { + child = column_view + }; + + var box = new Granite.Box (VERTICAL); + box.append (top_box); + box.append (search_entry); + box.append (scrolled); + + child = box; + + refresh_button.clicked.connect (model.refresh); + search_entry.search_changed.connect (on_search_changed); + scrolled.edge_reached.connect (on_edge_reached); + } + + private void setup_header (Object obj) { + var item = (Gtk.ListHeader) obj; + item.child = new Granite.HeaderLabel (""); + } + + private void bind_header (Object obj) { + var item = (Gtk.ListHeader) obj; + var entry = (SystemdLogEntry) item.item; + var label = (Granite.HeaderLabel) item.child; + label.label = entry.relative_time; + } + + 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 LogCell (MESSAGE); + } + + private void bind (Object obj) { + var item = (Gtk.ListItem) obj; + var entry = (SystemdLogEntry) item.item; + var cell = (LogCell) item.child; + cell.bind (entry); + } + + 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 (); + } + } +} diff --git a/src/Views/LogView/SystemdLogEntry.vala b/src/Views/LogView/SystemdLogEntry.vala new file mode 100644 index 000000000..5beb2d913 --- /dev/null +++ b/src/Views/LogView/SystemdLogEntry.vala @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + + public class Monitor.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/Views/LogView/SystemdLogModel.vala b/src/Views/LogView/SystemdLogModel.vala new file mode 100644 index 000000000..4225466a8 --- /dev/null +++ b/src/Views/LogView/SystemdLogModel.vala @@ -0,0 +1,240 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + */ + +public class Monitor.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 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 + // and only used for loading and in the implementations + private Gee.ArrayList entries; + 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); + + construct { + entries = new Gee.ArrayList (); + + int res = Systemd.Journal.open_namespace (out journal, null, LOCAL_ONLY); + if (res != 0) { + critical ("%s", strerror (-res)); + return; + } + + res = Systemd.Id128.boot (out current_boot_id); + if (res != 0) { + critical ("%s", strerror (-res)); + return; + } + + init (); + } + + private void reset () { + if (!entries.is_empty) { + var removed = entries.size; + entries.clear (); + items_changed (0, removed, 0); + } + + eof = false; + 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 () { + 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_conjunction (); + + if (current_tail_time == 0) { + journal.seek_tail (); + journal.previous (); + 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_time); + journal.previous (); + } + + load_chunk (); + } + + public void load_chunk () { + if (eof || loading) { + return; + } + + loading = true; + + var start_items = get_n_items (); + + Idle.add (() => { + 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; + }); + } + + private void load_timed () { + if (eof) { + return; + } + + var start_n_items = entries.size; + var start_time = get_monotonic_time (); + + while (get_monotonic_time () - start_time < CHUNK_TIME) { + if (!load_next_entry ()) { + eof = true; + break; + } + } + + items_changed (start_n_items, 0, entries.size - start_n_items); + } + + private bool load_next_entry () { + int res = journal.previous (); + if (res == 0) { + return false; + } + + if (res < 0) { + critical ("Failed to go to next aka previous entry: %s", strerror (-res)); + return false; + } + + 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 + } + + res = journal.get_data ("_COMM", out comm_data); + if (res != 0) { + comm_data = "_COMM=kernel".data; + } + + var origin = ((string) comm_data).offset ("_COMM=".length); + var message = ((string) data).offset ("MESSAGE=".length); + + 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_utc ((int64) (time / TimeSpan.SECOND)); + + 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); + + return true; + } + + private bool update_current_range (DateTime dt) { + if (current_section_time == null) { + current_section_time = dt; + return true; + } + + if (current_section_time.difference (dt) <= TimeSpan.SECOND) { + return true; + } else { + current_section_time = dt; + return false; + } + } + + public void search (string term) { + reset (); + //TODO: tokenize etc. + current_search_term = term; + init (); + } + + public void refresh () { + reset (); + current_tail_time = 0; + init (); + } + + public Object? get_item (uint position) { + if (position >= entries.size) { + return null; + } else { + return entries[(int) position]; + } + } + + public Type get_item_type () { + return typeof (SystemdLogEntry); + } + + 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/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 522ea91a366ce80c46c515471402d39247456592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 15 Sep 2026 15:02:17 -0700 Subject: [PATCH 3/8] Lint --- src/Views/LogView/LogView.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/LogView/LogView.vala b/src/Views/LogView/LogView.vala index fb150edcc..67bfdfee6 100644 --- a/src/Views/LogView/LogView.vala +++ b/src/Views/LogView/LogView.vala @@ -4,7 +4,7 @@ */ public class Monitor.LogView : Granite.Bin { - private SystemdLogModel model; + private SystemdLogModel model; construct { From 25a526b3653a6f56dff4e1575660f67e8e9921ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 15 Sep 2026 15:48:42 -0700 Subject: [PATCH 4/8] update POTFILES --- po/POTFILES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/po/POTFILES b/po/POTFILES index fd7e9f425..1adf58c23 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -3,6 +3,10 @@ src/MainWindow.vala src/Utils.vala src/Indicator/Widgets/IndicatorWidgetFrequency.vala src/Indicator/Widgets/PopoverWidget.vala +src/views/LogView/LogCell.vala +src/views/LogView/LogView.vala +src/views/LogView/SystemdLogEntry.vala +src/views/LogView/SystemdLogModel.vala src/Views/ProcessView/ProcessInfoView/ProcessInfoHeader.vala src/Views/ProcessView/ProcessInfoView/ProcessInfoIOStats.vala src/Views/ProcessView/ProcessInfoView/ProcessInfoView.vala From f85eb56212e73418b95145fe43969cee6dcbffce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Wed, 16 Sep 2026 11:27:18 -0700 Subject: [PATCH 5/8] Re-use search bar --- src/MainWindow.vala | 5 +++-- src/Views/LogView/LogView.vala | 22 +++++----------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index cede6a246..e27b91210 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -92,10 +92,10 @@ public class Monitor.MainWindow : Gtk.ApplicationWindow { var dbusserver = DBusServer.get_default (); - search_revealer.reveal_child = stack.visible_child == process_view; + search_revealer.reveal_child = stack.visible_child != system_view; stack.notify["visible-child"].connect (() => { toolbox.reveal_bottom_bars = stack.visible_child == process_view; - search_revealer.reveal_child = stack.visible_child == process_view; + search_revealer.reveal_child = stack.visible_child != system_view; }); new Thread ("upd", () => { @@ -120,6 +120,7 @@ public class Monitor.MainWindow : Gtk.ApplicationWindow { search_entry.search_changed.connect (() => { process_view.treeview_model.filtered.needle = search_entry.text; + log_view.on_search_changed (search_entry.text); search_entry.grab_focus (); }); diff --git a/src/Views/LogView/LogView.vala b/src/Views/LogView/LogView.vala index 67bfdfee6..ad410fda1 100644 --- a/src/Views/LogView/LogView.vala +++ b/src/Views/LogView/LogView.vala @@ -8,22 +8,12 @@ public class Monitor.LogView : Granite.Bin { construct { - var search_entry = new Gtk.SearchEntry () { - hexpand = true, - placeholder_text = _("Search") - }; - var refresh_button = new Gtk.Button.from_icon_name ("view-refresh-symbolic") { - tooltip_text = _("Load new entries") - }; - - var top_box = new Granite.Box (HORIZONTAL) { margin_top = 12, margin_end = 12, - margin_start = 12 + margin_start = 12, + tooltip_text = _("Load new entries") }; - top_box.append (search_entry); - top_box.append (refresh_button); model = new SystemdLogModel (); @@ -60,14 +50,12 @@ public class Monitor.LogView : Granite.Bin { }; var box = new Granite.Box (VERTICAL); - box.append (top_box); - box.append (search_entry); + box.append (refresh_button); box.append (scrolled); child = box; refresh_button.clicked.connect (model.refresh); - search_entry.search_changed.connect (on_search_changed); scrolled.edge_reached.connect (on_edge_reached); } @@ -100,8 +88,8 @@ public class Monitor.LogView : Granite.Bin { cell.bind (entry); } - private void on_search_changed (Gtk.SearchEntry entry) { - model.search (entry.text); + public void on_search_changed (string needle) { + model.search (needle); } private void on_edge_reached (Gtk.PositionType pos) { From dd5b0274cab676fadfb2581851fc8b6fc6b82666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Wed, 16 Sep 2026 11:46:25 -0700 Subject: [PATCH 6/8] Scroll to refresh --- src/Views/LogView/LogView.vala | 41 +++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Views/LogView/LogView.vala b/src/Views/LogView/LogView.vala index ad410fda1..b5c96a6a1 100644 --- a/src/Views/LogView/LogView.vala +++ b/src/Views/LogView/LogView.vala @@ -5,14 +5,15 @@ public class Monitor.LogView : Granite.Bin { private SystemdLogModel model; + private Gtk.ProgressBar refresh_progress; + private Gtk.Revealer refresh_revealer; + private uint refresh_timeout = -1; construct { + refresh_progress = new Gtk.ProgressBar (); - var refresh_button = new Gtk.Button.from_icon_name ("view-refresh-symbolic") { - margin_top = 12, - margin_end = 12, - margin_start = 12, - tooltip_text = _("Load new entries") + refresh_revealer = new Gtk.Revealer () { + child = refresh_progress }; model = new SystemdLogModel (); @@ -49,14 +50,14 @@ public class Monitor.LogView : Granite.Bin { child = column_view }; - var box = new Granite.Box (VERTICAL); - box.append (refresh_button); + var box = new Granite.Box (VERTICAL, NONE); + box.append (refresh_revealer); box.append (scrolled); child = box; - refresh_button.clicked.connect (model.refresh); scrolled.edge_reached.connect (on_edge_reached); + scrolled.edge_overshot.connect (on_edge_overshot); } private void setup_header (Object obj) { @@ -97,4 +98,28 @@ public class Monitor.LogView : Granite.Bin { model.load_chunk (); } } + + private void on_edge_overshot (Gtk.PositionType pos) { + if (pos == TOP) { + if (refresh_timeout == -1) { + refresh_timeout = Timeout.add_once (200, () => { + reset_refresh_progress (); + }); + } + + refresh_revealer.reveal_child = true; + refresh_progress.fraction += 0.25; + + if (refresh_progress.fraction >= 1) { + model.refresh (); + reset_refresh_progress (); + } + } + } + + private void reset_refresh_progress () { + refresh_revealer.reveal_child = false; + refresh_progress.fraction = 0; + refresh_timeout = -1; + } } From a43139758335cd3db56ea67d00c29dad0ff8c4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Wed, 16 Sep 2026 12:40:59 -0700 Subject: [PATCH 7/8] Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stanisław <6031763+stsdc@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0cb19bab..49087d61d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ sudo apt install sassc valac libgtk-4-dev libgee-0.8-dev libgranite-7-dev libgto Alternatively, if you plan to install WITHOUT a wingpanel-indicator ```bash -sudo apt install sassc valac libgtk-4-dev libgee-0.8-dev libgranite-7-dev libgtop2-dev libadwaita-1-dev libudisks2-dev libjson-glib-dev libflatpak-dev libxnvctrl-dev liblivechart-2-dev libpci-dev meson +sudo apt install sassc valac libgtk-4-dev libgee-0.8-dev libgranite-7-dev libgtop2-dev libadwaita-1-dev libudisks2-dev libjson-glib-dev libflatpak-dev libsystemd-dev libxnvctrl-dev liblivechart-2-dev libpci-dev meson ``` From 7301d6a44442d0d35e40d75ac9cd0ea5161e5e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Wed, 16 Sep 2026 12:41:13 -0700 Subject: [PATCH 8/8] Update MainWindow.vala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stanisław <6031763+stsdc@users.noreply.github.com> --- src/MainWindow.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MainWindow.vala b/src/MainWindow.vala index e27b91210..35eef5401 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -34,7 +34,7 @@ public class Monitor.MainWindow : Gtk.ApplicationWindow { transition_type = SLIDE_LEFT_RIGHT }; stack.add_titled (process_view, "process_view", _("Processes")); - stack.add_titled (log_view, "log-view", _("Logs")); + stack.add_titled (log_view, "log_view", _("Logs")); stack.add_titled (system_view, "system_view", _("System")); var stack_switcher = new Gtk.StackSwitcher () {