Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package otpclient for openSUSE:Factory checked in at 2025-12-28 19:19:21 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/otpclient (Old) and /work/SRC/openSUSE:Factory/.otpclient.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "otpclient" Sun Dec 28 19:19:21 2025 rev:41 rq:1324490 version:4.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/otpclient/otpclient.changes 2025-05-09 18:54:02.011756083 +0200 +++ /work/SRC/openSUSE:Factory/.otpclient.new.1928/otpclient.changes 2025-12-28 19:19:30.335656377 +0100 @@ -1,0 +2,18 @@ +Sat Dec 27 13:32:28 UTC 2025 - Paolo Stivanin <[email protected]> + +- Update to 4.2.0: + * ADDED: interactive search (ctrl-f) + * IMPROVED: search now matches query against type, account label, and issuer uniformly + * IMPROVED: Streamlined treeview model population to read JSON directly with safe defaults + * IMPROVED: Simplified OTP update flow and tightened reorder/delete safety and cleanup + * IMPROVED: Centralized app/db default initialization and early cleanup paths in app.c + * IMPROVED: Tightened error handling by clearing config migration errors and freeing the config path + * IMPROVED: Made early-exit cleanup safer by avoiding double-freeing the database key + * IMPROVED: Added a helper to clear password entries and reset visibility on successful submit + * IMPROVED: Cleared old/new password fields before the dialog closes to avoid brief exposure + * IMPROVED: Initialized settings defaults when the config load fails and persisted them to otpclient.cfg + * IMPROVED: Added warning dialog only when saving fallback defaults fails + * IMPROVED: cli: improve robustness and correctness in string and file handling + * FIXED: duplicate windows and tray icons on re-activation (#409) + +------------------------------------------------------------------- Old: ---- v4.1.0.tar.gz v4.1.0.tar.gz.asc New: ---- v4.2.0.tar.gz v4.2.0.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ otpclient.spec ++++++ --- /var/tmp/diff_new_pack.92vsuS/_old 2025-12-28 19:19:31.099687746 +0100 +++ /var/tmp/diff_new_pack.92vsuS/_new 2025-12-28 19:19:31.103687911 +0100 @@ -1,7 +1,7 @@ # # spec file for package otpclient # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2025 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %define uclname OTPClient Name: otpclient -Version: 4.1.0 +Version: 4.2.0 Release: 0 Summary: Simple GTK+ client for managing TOTP and HOTP License: GPL-3.0-or-later ++++++ v4.1.0.tar.gz -> v4.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/CMakeLists.txt new/OTPClient-4.2.0/CMakeLists.txt --- old/OTPClient-4.1.0/CMakeLists.txt 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/CMakeLists.txt 2025-12-23 13:40:00.000000000 +0100 @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.16) -project(OTPClient VERSION "4.1.0" LANGUAGES "C") +project(OTPClient VERSION "4.2.0" LANGUAGES "C") include(GNUInstallDirs) configure_file("src/common/version.h.in" "version.h") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/SECURITY.md new/OTPClient-4.2.0/SECURITY.md --- old/OTPClient-4.1.0/SECURITY.md 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/SECURITY.md 2025-12-23 13:40:00.000000000 +0100 @@ -8,7 +8,7 @@ |---------|--------------------|-------------| |---------|--------------------|-------------| | 4.1.x | :white_check_mark: | - | -| 4.0.x | :white_check_mark: | 30-Jun-2025 | +| 4.0.x | :x: | 30-Jun-2025 | | 3.7.x | :x: | 30-Sep-2024 | | 3.6.x | :x: | 31-Aug-2024 | | 3.5.x | :x: | 31-Mar-2024 | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/data/com.github.paolostivanin.OTPClient.appdata.xml new/OTPClient-4.2.0/data/com.github.paolostivanin.OTPClient.appdata.xml --- old/OTPClient-4.1.0/data/com.github.paolostivanin.OTPClient.appdata.xml 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/data/com.github.paolostivanin.OTPClient.appdata.xml 2025-12-23 13:40:00.000000000 +0100 @@ -89,9 +89,37 @@ </content_rating> <releases> + <release version="4.2.0" date="2025-12-23"> + <description> + <p>OTPClient 4.2.0 the followin improvements:</p> + <ul> + <li>ADDED: interactive search (ctrl-f)</li> + <li>IMPROVED: search now matches query against type, account label, and issuer uniformly</li> + <li>IMPROVED: Streamlined treeview model population to read JSON directly with safe defaults</li> + <li>IMPROVED: Simplified OTP update flow and tightened reorder/delete safety and cleanup</li> + <li>IMPROVED: Centralized app/db default initialization and early cleanup paths in app.c</li> + <li>IMPROVED: Tightened error handling by clearing config migration errors and freeing the config path</li> + <li>IMPROVED: Made early-exit cleanup safer by avoiding double-freeing the database key</li> + <li>IMPROVED: Added a helper to clear password entries and reset visibility on successful submit</li> + <li>IMPROVED: Cleared old/new password fields before the dialog closes to avoid brief exposure</li> + <li>IMPROVED: Initialized settings defaults when the config load fails and persisted them to otpclient.cfg</li> + <li>IMPROVED: Added warning dialog only when saving fallback defaults fails</li> + <li>IMPROVED: cli: improve robustness and correctness in string and file handling</li> + <li>FIXED: duplicate windows and tray icons on re-activation (#409)</li> + </ul> + </description> + </release> + <release version="4.1.1" date="2025-05-13"> + <description> + <p>OTPClient 4.1.1 the following improvements:</p> + <ul> + <li>FIXED: build issue on Flatpak</li> + </ul> + </description> + </release> <release version="4.1.0" date="2025-05-07"> <description> - <p>OTPClient 4.1.0 the followin improvements:</p> + <p>OTPClient 4.1.0 the following improvements:</p> <ul> <li>ADDED: minimize to tray with ayatana-appindicator3 (#386 thanks a lot @len-foss)</li> <li>IMPROVED: only show memlock warning dialog when secure memory is unavailable (#397)</li> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/cli/exec-action.c new/OTPClient-4.2.0/src/cli/exec-action.c --- old/OTPClient-4.1.0/src/cli/exec-action.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/cli/exec-action.c 2025-12-23 13:40:00.000000000 +0100 @@ -264,8 +264,10 @@ g_free (db_path); return NULL; } else { - // remove the newline char - db_path[g_utf8_strlen (db_path, -1) - 1] = '\0'; + // Remove the trailing newline (if present). This is UTF-8 safe because '\n' is a single-byte ASCII + // character and fgets appends it as a separate byte; no multibyte code point is modified. + char *nl = strchr (db_path, '\n'); + if (nl) { *nl = '\0'; } if (!g_file_test (db_path, G_FILE_TEST_EXISTS)) { g_printerr (_("File '%s' does not exist\n"), db_path); g_free (cfg_file_path); @@ -309,9 +311,12 @@ g_print ("\n"); tcsetattr (STDIN_FILENO, TCSAFLUSH, &old); - pwd[g_utf8_strlen (pwd, -1) - 1] = '\0'; + // Trim trailing newline if present. Safe for UTF-8 because '\n' is a single-byte terminator + // added by fgets; we do not touch preceding multibyte characters. + char *nl = strchr (pwd, '\n'); + if (nl) { *nl = '\0'; } - gchar *realloc_pwd = gcry_realloc (pwd, g_utf8_strlen (pwd, -1) + 1); + gchar *realloc_pwd = gcry_realloc (pwd, strlen (pwd) + 1); return realloc_pwd; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/common/common.c new/OTPClient-4.2.0/src/common/common.c --- old/OTPClient-4.1.0/src/common/common.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/common/common.c 2025-12-23 13:40:00.000000000 +0100 @@ -68,9 +68,15 @@ gchar * secure_strdup (const gchar *src) { - gchar *sec_buf = gcry_calloc_secure (strlen (src) + 1, 1); - memcpy (sec_buf, src, strlen (src) + 1); - + if (src == NULL) { + return NULL; + } + size_t len = strlen (src); + gchar *sec_buf = gcry_calloc_secure (len + 1, 1); + if (sec_buf == NULL) { + return NULL; + } + memcpy (sec_buf, src, len + 1); return sec_buf; } @@ -78,13 +84,33 @@ guchar * hexstr_to_bytes (const gchar *hexstr) { - size_t len = g_utf8_strlen (hexstr, -1); - size_t final_len = len / 2; - guchar *chrs = (guchar *)g_malloc ((final_len+1) * sizeof(*chrs)); - for (size_t i = 0, j = 0; j < final_len; i += 2, j++) - chrs[j] = (hexstr[i] % 32 + 9) % 25 * 16 + (hexstr[i+1] % 32 + 9) % 25; - chrs[final_len] = '\0'; - return chrs; + if (hexstr == NULL) { + return NULL; + } + // Hex strings should be ASCII; use strlen, not g_utf8_strlen + size_t len = strlen (hexstr); + if (len == 0 || (len % 2) != 0) { + // invalid length + return NULL; + } + + size_t out_len = len / 2; + guchar *bytes = (guchar *) g_malloc (out_len + 1); // +1 to NUL-terminate if needed by callers + if (bytes == NULL) { + return NULL; + } + + for (size_t i = 0, j = 0; j < out_len; i += 2, j++) { + int hi = g_ascii_xdigit_value (hexstr[i]); + int lo = g_ascii_xdigit_value (hexstr[i + 1]); + if (hi < 0 || lo < 0) { + g_free (bytes); + return NULL; + } + bytes[j] = (guchar) ((hi << 4) | lo); + } + bytes[out_len] = '\0'; + return bytes; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/common/file-size.c new/OTPClient-4.2.0/src/common/file-size.c --- old/OTPClient-4.1.0/src/common/file-size.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/common/file-size.c 2025-12-23 13:40:00.000000000 +0100 @@ -1,23 +1,33 @@ #include <gio/gio.h> +// Returns -1 on error, 0 if file doesn't exist, or the file size on success. goffset get_file_size (const gchar *file_path) { - GError *error = NULL; - if (file_path == NULL || !g_file_test (file_path, G_FILE_TEST_EXISTS)) { return 0; } + GError *error = NULL; GFile *file = g_file_new_for_path (file_path); - GFileInfo *info = g_file_query_info (G_FILE(file), "standard::*", G_FILE_QUERY_INFO_NONE, NULL, &error); + if (file == NULL) { + return -1; + } + + // Query only the size to avoid unnecessary I/O on other attributes + GFileInfo *info = g_file_query_info (file, "standard::size", G_FILE_QUERY_INFO_NONE, NULL, &error); if (info == NULL) { - g_printerr ("%s\n", error->message); - g_clear_error (&error); + if (error != NULL) { + g_printerr ("Failed to query file size: %s\n", error->message); + g_clear_error (&error); + } + g_object_unref (file); return -1; } + goffset file_size = g_file_info_get_size (info); + g_object_unref (info); g_object_unref (file); return file_size; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/about_diag_cb.c new/OTPClient-4.2.0/src/gui/about_diag_cb.c --- old/OTPClient-4.1.0/src/gui/about_diag_cb.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/about_diag_cb.c 2025-12-23 13:40:00.000000000 +0100 @@ -13,8 +13,6 @@ const gchar *authors[] = {"Paolo Stivanin <[email protected]>", NULL}; const gchar *artists[] = {"Tobias Bernard (bertob) <https://tobiasbernard.com>", NULL}; - const gchar *partial_path = "share/icons/hicolor/scalable/apps/com.github.paolostivanin.OTPClient.svg"; - gchar *icon_abs_path = g_strconcat (INSTALL_PREFIX, "/", partial_path, NULL); GtkWidget *ab_diag = gtk_about_dialog_new (); gtk_window_set_transient_for (GTK_WINDOW(ab_diag), GTK_WINDOW(app_data->main_window)); @@ -27,9 +25,7 @@ gtk_about_dialog_set_website (GTK_ABOUT_DIALOG(ab_diag), "https://github.com/paolostivanin/OTPClient"); gtk_about_dialog_set_authors (GTK_ABOUT_DIALOG(ab_diag), authors); gtk_about_dialog_set_artists (GTK_ABOUT_DIALOG(ab_diag), artists); - GdkPixbuf *logo = gdk_pixbuf_new_from_file (icon_abs_path, NULL); - gtk_about_dialog_set_logo (GTK_ABOUT_DIALOG(ab_diag), logo); - g_free (icon_abs_path); + gtk_about_dialog_set_logo_icon_name (GTK_ABOUT_DIALOG(ab_diag), "com.github.paolostivanin.OTPClient"); g_signal_connect (ab_diag, "response", G_CALLBACK (gtk_widget_destroy), NULL); gtk_widget_show_all (ab_diag); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/app.c new/OTPClient-4.2.0/src/gui/app.c --- old/OTPClient-4.1.0/src/gui/app.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/app.c 2025-12-23 13:40:00.000000000 +0100 @@ -74,42 +74,55 @@ static void set_open_db_action (GtkWidget *btn, gpointer user_data); +static void init_app_defaults (AppData *app_data); + +static void init_db_defaults (AppData *app_data); + +static void cleanup_app_data (AppData *app_data); + +static GQuark app_data_quark = 0; + + +static void +ensure_app_data_quark (void) +{ + if (G_UNLIKELY (app_data_quark == 0)) + app_data_quark = g_quark_from_static_string ("otpclient-app-data"); +} + void activate (GtkApplication *app, gpointer user_data UNUSED) { - gint32 memlock_value = 0; - gint32 memlock_ret_value = set_memlock_value (&memlock_value); + ensure_app_data_quark (); - AppData *app_data = g_new0 (AppData, 1); + AppData *app_data = g_object_get_qdata (G_OBJECT (app), app_data_quark); + if (app_data != NULL) { + gtk_window_present (GTK_WINDOW (app_data->main_window)); + return; + } - app_data->app_locked = FALSE; + gint32 memlock_value = 0; + gint32 memlock_ret_value = set_memlock_value (&memlock_value); - gint width = 0, height = 0; - app_data->show_next_otp = FALSE; // next otp not shown by default - app_data->disable_notifications = FALSE; // notifications enabled by default - app_data->search_column = 0; // account - app_data->auto_lock = FALSE; // disabled by default - app_data->inactivity_timeout = 0; // never - app_data->use_dark_theme = FALSE; // light theme by default - app_data->use_secret_service = TRUE; // secret service enabled by default - app_data->is_reorder_active = FALSE; // when app is started, reorder is not set - // open_db_file_action is set only on first startup and not when the db is deleted but the cfg file is there, therefore we need a default action - app_data->open_db_file_action = GTK_FILE_CHOOSER_ACTION_SAVE; + app_data = g_new0 (AppData, 1); + init_app_defaults (app_data); app_data->builder = get_builder_from_partial_path (UI_PARTIAL_PATH); app_data->add_popover_builder = get_builder_from_partial_path (AP_PARTIAL_PATH); app_data->settings_popover_builder = get_builder_from_partial_path (SP_PARTIAL_PATH); - set_config_data (&width, &height, app_data); - app_data->db_data = g_new0 (DatabaseData, 1); - app_data->db_data->key_stored = FALSE; // at startup, we don't know whether the key is stored or not + init_db_defaults (app_data); + + gint width = 0; + gint height = 0; + set_config_data (&width, &height, app_data); create_main_window (width, height, app_data); if (app_data->main_window == NULL) { g_printerr ("%s\n", _("Couldn't locate the ui file, exiting...")); - g_free (app_data->db_data); + cleanup_app_data (app_data); g_application_quit (G_APPLICATION(app)); return; } @@ -121,7 +134,7 @@ "<a href=\"https://github.com/paolostivanin/OTPClient/wiki/Secure-Memory-Limitations\">secure memory</a> wiki page before re-running OTPClient.")); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); - g_free (app_data->db_data); + cleanup_app_data (app_data); g_application_quit (G_APPLICATION(app)); return; } @@ -130,7 +143,7 @@ if (init_msg != NULL) { show_message_dialog (app_data->main_window, init_msg, GTK_MESSAGE_ERROR); g_free (init_msg); - g_free (app_data->db_data); + cleanup_app_data (app_data); g_application_quit (G_APPLICATION(app)); return; } @@ -144,9 +157,9 @@ app_data->db_data->db_path = db_path; } else { // Use the default path only if no path is set in config - app_data->db_data->db_path = g_build_filenam e(g_get_user_data_dir (), "otpclient-db.enc", NULL); + app_data->db_data->db_path = g_build_filename(g_get_user_data_dir (), "otpclient-db.enc", NULL); gchar *cfg_file_path = g_build_filename (g_get_user_data_dir (), "otpclient.cfg", NULL); - if (g_file_tes t(cfg_file_path, G_FILE_TEST_EXISTS)) { + if (g_file_test(cfg_file_path, G_FILE_TEST_EXISTS)) { g_key_file_set_string (kf, "config", "db_path", app_data->db_data->db_path); g_key_file_save_to_file (kf, cfg_file_path, NULL); } @@ -179,8 +192,7 @@ case GTK_RESPONSE_CANCEL: default: gtk_widget_destroy (app_data->diag_rcdb); - g_free (app_data->db_data); - g_free (app_data); + cleanup_app_data (app_data); g_application_quit (G_APPLICATION(app)); return; case GTK_RESPONSE_OK: @@ -190,8 +202,7 @@ app_data->db_data->db_path = get_db_path (app_data); if (app_data->db_data->db_path == NULL) { - g_free (app_data->db_data); - g_free (app_data); + cleanup_app_data (app_data); g_application_quit (G_APPLICATION(app)); return; } @@ -218,8 +229,7 @@ if (app_data->db_data->key == NULL) { retry_change_file: if (change_file (app_data) == QUIT_APP) { - g_free (app_data->db_data); - g_free (app_data); + cleanup_app_data (app_data); g_application_quit (G_APPLICATION(app)); return; } @@ -236,8 +246,7 @@ ), memlock_value); g_printerr ("%s\n", msg); g_free (msg); - g_free (app_data->db_data); - g_free (app_data); + cleanup_app_data (app_data); g_application_quit (G_APPLICATION(app)); return; } @@ -246,10 +255,9 @@ load_db (app_data->db_data, &err); if (err != NULL && !g_error_matches (err, missing_file_gquark (), MISSING_FILE_ERRCODE)) { show_message_dialog (app_data->main_window, err->message, GTK_MESSAGE_ERROR); - gcry_free (app_data->db_data->key); + g_clear_pointer (&app_data->db_data->key, gcry_free); if (g_error_matches (err, memlock_error_gquark (), MEMLOCK_ERRCODE)) { - g_free (app_data->db_data); - g_free (app_data); + cleanup_app_data (app_data); g_clear_error (&err); g_application_quit (G_APPLICATION(app)); return; @@ -289,10 +297,7 @@ GtkToggleButton *reorder_toggle_btn = GTK_TOGGLE_BUTTON(gtk_builder_get_object (app_data->builder, "reorder_toggle_btn_id")); g_signal_connect (app_data->main_window, "toggle-reorder-button", G_CALLBACK(toggle_button_cb), reorder_toggle_btn); g_signal_connect (reorder_toggle_btn, "toggled", G_CALLBACK(reorder_rows_cb), app_data); - g_signal_connect (app_data->main_window, "key_press_event", G_CALLBACK(key_pressed_cb), NULL); - - g_signal_connect (app_data->main_window, "key_press_event", G_CALLBACK(key_pressed_cb), NULL); - + g_signal_connect (app_data->main_window, "key_press_event", G_CALLBACK(key_pressed_cb), app_data); g_signal_connect (app_data->main_window, "destroy", G_CALLBACK(destroy_cb), app_data); app_data->source_id = g_timeout_add_full (G_PRIORITY_DEFAULT, 1000, traverse_liststore, app_data, NULL); @@ -303,21 +308,43 @@ app_data->last_user_activity = g_date_time_new_now_local (); app_data->source_id_last_activity = g_timeout_add_seconds (1, check_inactivity, app_data); + g_object_set_qdata (G_OBJECT (app), app_data_quark, app_data); + gtk_widget_show_all (app_data->main_window); + gtk_widget_hide(app_data->search_entry); } static gboolean key_pressed_cb (GtkWidget *window, GdkEventKey *event_key, - gpointer user_data UNUSED) + gpointer user_data) { + CAST_USER_DATA(AppData, app_data, user_data); switch (event_key->keyval) { case GDK_KEY_q: if (event_key->state & GDK_CONTROL_MASK) { gtk_window_close (GTK_WINDOW(window)); } break; + case GDK_KEY_f: + if (event_key->state & GDK_CONTROL_MASK) { + GtkWidget *search_entry = app_data->search_entry; + if (search_entry != NULL) { + gboolean is_visible = gtk_widget_get_visible (search_entry); + gtk_widget_set_visible (search_entry, !is_visible); + if (!is_visible) { + gtk_widget_grab_focus (search_entry); + } else { + gtk_entry_set_text (GTK_ENTRY(search_entry), ""); + if (app_data->tree_view != NULL) { + gtk_widget_grab_focus (GTK_WIDGET(app_data->tree_view)); + } + } + } + return TRUE; + } + break; } return FALSE; } @@ -336,7 +363,6 @@ *height = g_key_file_get_integer (kf, "config", "window_height", NULL); app_data->show_next_otp = g_key_file_get_boolean (kf, "config", "show_next_otp", NULL); app_data->disable_notifications = g_key_file_get_boolean (kf, "config", "notifications", NULL); - app_data->search_column = g_key_file_get_integer (kf, "config", "search_column", NULL); app_data->auto_lock = g_key_file_get_boolean (kf, "config", "auto_lock", NULL); app_data->inactivity_timeout = g_key_file_get_integer (kf, "config", "inactivity_timeout", NULL); app_data->use_dark_theme = g_key_file_get_boolean (kf, "config", "dark_theme", NULL); @@ -351,6 +377,7 @@ // key was not found, so we already migrated to the new format app_data->use_secret_service = g_key_file_get_boolean (kf, "config", "use_secret_service", NULL); } + g_clear_error (&err); // end migration g_object_set (gtk_settings_get_default (), "gtk-application-prefer-dark-theme", app_data->use_dark_theme, NULL); g_key_file_free (kf); @@ -379,6 +406,7 @@ g_free (err_msg); g_clear_error (&err); } + g_free (cfg_file_path); } static void @@ -580,6 +608,11 @@ gpointer user_data) { CAST_USER_DATA(AppData, app_data, user_data); + + ensure_app_data_quark (); + GtkApplication *app = GTK_APPLICATION(gtk_window_get_application (GTK_WINDOW (window))); + g_object_set_qdata (G_OBJECT(app), app_data_quark, NULL); + save_sort_order (app_data->tree_view); g_source_remove (app_data->source_id); g_source_remove (app_data->source_id_last_activity); @@ -605,6 +638,12 @@ g_object_unref (app_data->builder); g_object_unref (app_data->add_popover_builder); g_object_unref (app_data->settings_popover_builder); + if (app_data->filter_model != NULL) { + g_object_unref (app_data->filter_model); + } + if (app_data->list_store != NULL) { + g_object_unref (app_data->list_store); + } g_free (app_data); gcry_control (GCRYCTL_TERM_SECMEM); } @@ -615,7 +654,11 @@ { gint id; GtkSortType order; - gtk_tree_sortable_get_sort_column_id (GTK_TREE_SORTABLE(GTK_LIST_STORE(gtk_tree_view_get_model (tree_view))), &id, &order); + GtkTreeModel *model = gtk_tree_view_get_model (tree_view); + if (GTK_IS_TREE_MODEL_FILTER (model)) { + model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER(model)); + } + gtk_tree_sortable_get_sort_column_id (GTK_TREE_SORTABLE(model), &id, &order); // store data only if it was changed if (id >= 0) { store_data ("column_id", id, "sort_order", order); @@ -671,3 +714,58 @@ app_data->open_db_file_action = g_strcmp0 (gtk_widget_get_name (btn), "diag_rc_restoredb_btn") == 0 ? GTK_FILE_CHOOSER_ACTION_OPEN : GTK_FILE_CHOOSER_ACTION_SAVE; gtk_dialog_response (GTK_DIALOG(app_data->diag_rcdb), GTK_RESPONSE_OK); } + +static void +init_app_defaults (AppData *app_data) +{ + app_data->app_locked = FALSE; + app_data->show_next_otp = FALSE; // next otp not shown by default + app_data->disable_notifications = FALSE; // notifications enabled by default + app_data->auto_lock = FALSE; // disabled by default + app_data->inactivity_timeout = 0; // never + app_data->use_dark_theme = FALSE; // light theme by default + app_data->use_secret_service = TRUE; // secret service enabled by default + app_data->is_reorder_active = FALSE; // when app is started, reorder is not set + app_data->use_tray = FALSE; // do not use tray by default + // open_db_file_action is set only on first startup and not when the db is deleted but the cfg file is there, therefore we need a default action + app_data->open_db_file_action = GTK_FILE_CHOOSER_ACTION_SAVE; +} + +static void +init_db_defaults (AppData *app_data) +{ + app_data->db_data->key_stored = FALSE; // at startup, we don't know whether the key is stored or not + app_data->db_data->max_file_size_from_memlock = 0; + app_data->db_data->objects_hash = NULL; + app_data->db_data->data_to_add = NULL; +} + +static void +cleanup_app_data (AppData *app_data) +{ + if (app_data == NULL) { + return; + } + + if (app_data->main_window != NULL) { + gtk_widget_destroy (app_data->main_window); + } + + if (app_data->builder != NULL) { + g_object_unref (app_data->builder); + } + if (app_data->add_popover_builder != NULL) { + g_object_unref (app_data->add_popover_builder); + } + if (app_data->settings_popover_builder != NULL) { + g_object_unref (app_data->settings_popover_builder); + } + + if (app_data->db_data != NULL) { + g_clear_pointer (&app_data->db_data->db_path, g_free); + g_clear_pointer (&app_data->db_data->key, gcry_free); + g_free (app_data->db_data); + } + + g_free (app_data); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/change-db-cb.c new/OTPClient-4.2.0/src/gui/change-db-cb.c --- old/OTPClient-4.1.0/src/gui/change-db-cb.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/change-db-cb.c 2025-12-23 13:40:00.000000000 +0100 @@ -65,7 +65,7 @@ gtk_widget_hide (changedb_diag); return QUIT_APP; } - gtk_widget_destroy (changedb_diag); + gtk_widget_hide (changedb_diag); return CHANGE_OK; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/data.h new/OTPClient-4.2.0/src/gui/data.h --- old/OTPClient-4.1.0/src/gui/data.h 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/data.h 2025-12-23 13:40:00.000000000 +0100 @@ -18,6 +18,9 @@ GtkWidget *main_window; GtkTreeView *tree_view; + GtkListStore *list_store; + GtkTreeModelFilter *filter_model; + GtkWidget *search_entry; #ifdef ENABLE_MINIMIZE_TO_TRAY AppIndicator *indicator; #endif @@ -26,7 +29,6 @@ gboolean show_next_otp; gboolean disable_notifications; - gint search_column; gboolean auto_lock; gint inactivity_timeout; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/edit-row-cb.c new/OTPClient-4.2.0/src/gui/edit-row-cb.c --- old/OTPClient-4.1.0/src/gui/edit-row-cb.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/edit-row-cb.c 2025-12-23 13:40:00.000000000 +0100 @@ -44,12 +44,8 @@ CAST_USER_DATA(AppData, app_data, user_data); edit_data->db_data = app_data->db_data; - GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); - - edit_data->list_store = GTK_LIST_STORE(model); - - if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (app_data->tree_view), &model, &edit_data->iter)) { - gtk_tree_model_get (model, &edit_data->iter, COLUMN_ACC_LABEL, &edit_data->current_label, COLUMN_ACC_ISSUER, &edit_data->current_issuer, -1); + if (get_selected_liststore_iter (app_data, &edit_data->list_store, &edit_data->iter)) { + gtk_tree_model_get (GTK_TREE_MODEL(edit_data->list_store), &edit_data->iter, COLUMN_ACC_LABEL, &edit_data->current_label, COLUMN_ACC_ISSUER, &edit_data->current_issuer, -1); show_edit_dialog (edit_data, app_data); g_free (edit_data->current_label); g_free (edit_data->current_issuer); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/gui-misc.c new/OTPClient-4.2.0/src/gui/gui-misc.c --- old/OTPClient-4.1.0/src/gui/gui-misc.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/gui-misc.c 2025-12-23 13:40:00.000000000 +0100 @@ -303,4 +303,30 @@ update_model (app_data); g_slist_free_full (app_data->db_data->data_to_add, json_free); app_data->db_data->data_to_add = NULL; +} + + +gboolean +get_selected_liststore_iter (AppData *app_data, + GtkListStore **list_store, + GtkTreeIter *iter) +{ + GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); + GtkTreeIter view_iter; + if (!gtk_tree_selection_get_selected (gtk_tree_view_get_selection (app_data->tree_view), &model, &view_iter)) { + return FALSE; + } + + if (GTK_IS_TREE_MODEL_FILTER (model)) { + GtkTreeIter child_iter; + GtkTreeModel *child_model = gtk_tree_model_filter_get_model (GTK_TREE_MODEL_FILTER(model)); + gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER(model), &child_iter, &view_iter); + *list_store = GTK_LIST_STORE(child_model); + *iter = child_iter; + return TRUE; + } + + *list_store = GTK_LIST_STORE(model); + *iter = view_iter; + return TRUE; } \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/gui-misc.h new/OTPClient-4.2.0/src/gui/gui-misc.h --- old/OTPClient-4.1.0/src/gui/gui-misc.h 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/gui-misc.h 2025-12-23 13:40:00.000000000 +0100 @@ -14,6 +14,10 @@ guint get_row_number_from_iter (GtkListStore *list_store, GtkTreeIter iter); +gboolean get_selected_liststore_iter (AppData *app_data, + GtkListStore **list_store, + GtkTreeIter *iter); + void send_ok_cb (GtkWidget *entry, gpointer user_data); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/liststore-misc.c new/OTPClient-4.2.0/src/gui/liststore-misc.c --- old/OTPClient-4.1.0/src/gui/liststore-misc.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/liststore-misc.c 2025-12-23 13:40:00.000000000 +0100 @@ -33,7 +33,9 @@ traverse_liststore (gpointer user_data) { CAST_USER_DATA(AppData, app_data, user_data); - gtk_tree_model_foreach (GTK_TREE_MODEL(gtk_tree_view_get_model (app_data->tree_view)), foreach_func_update_otps, app_data); + if (app_data->list_store != NULL) { + gtk_tree_model_foreach (GTK_TREE_MODEL(app_data->list_store), foreach_func_update_otps, app_data); + } return TRUE; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/password-cb.c new/OTPClient-4.2.0/src/gui/password-cb.c --- old/OTPClient-4.1.0/src/gui/password-cb.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/password-cb.c 2025-12-23 13:40:00.000000000 +0100 @@ -122,6 +122,17 @@ static void +reset_entry_after_submit (GtkWidget *entry) +{ + if (entry == NULL) { + return; + } + gtk_entry_set_text (GTK_ENTRY(entry), ""); + gtk_entry_set_visibility (GTK_ENTRY(entry), FALSE); +} + + +static void check_pwd_cb (GtkWidget *entry, gpointer user_data) { @@ -137,6 +148,8 @@ return; } if (g_strcmp0 (gtk_entry_get_text (GTK_ENTRY(entry_widgets->entry1)), gtk_entry_get_text (GTK_ENTRY(entry_widgets->entry2))) == 0) { + reset_entry_after_submit (entry_widgets->entry_old); + reset_entry_after_submit (entry_widgets->entry2); password_cb (entry, (gpointer *)&entry_widgets->pwd); entry_widgets->retry = FALSE; } else { @@ -154,6 +167,7 @@ gsize len = g_utf8_strlen (text, -1) + 1; *pwd = gcry_calloc_secure (len, 1); strncpy (*pwd, text, len); + reset_entry_after_submit (entry); GtkWidget *top_level = gtk_widget_get_toplevel (entry); gtk_dialog_response (GTK_DIALOG (top_level), GTK_RESPONSE_CLOSE); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/settings-cb.c new/OTPClient-4.2.0/src/gui/settings-cb.c --- old/OTPClient-4.1.0/src/gui/settings-cb.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/settings-cb.c 2025-12-23 13:40:00.000000000 +0100 @@ -57,37 +57,42 @@ GError *err = NULL; GKeyFile *kf = g_key_file_new (); if (!g_key_file_load_from_file (kf, cfg_file_path, G_KEY_FILE_NONE, &err)) { - gchar *msg = g_strconcat ("Couldn't get data from config file: ", err->message, NULL); - show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); - g_free (msg); - g_free (cfg_file_path); - g_key_file_free (kf); - g_clear_error (&err); - g_free (settings_data); - return; - } - - // if key is not found, g_key_file_get_boolean returns FALSE and g_key_file_get_integer returns 0. - // Therefore, having these values as default is exactly what we want. So no need to check whether the key is missing. - app_data->show_next_otp = g_key_file_get_boolean (kf, "config", "show_next_otp", NULL); - app_data->disable_notifications = g_key_file_get_boolean (kf, "config", "notifications", NULL); - app_data->search_column = g_key_file_get_integer (kf, "config", "search_column", NULL); - app_data->auto_lock = g_key_file_get_boolean (kf, "config", "auto_lock", NULL); - app_data->inactivity_timeout = g_key_file_get_integer (kf, "config", "inactivity_timeout", NULL); - app_data->use_dark_theme = g_key_file_get_boolean (kf, "config", "dark_theme", NULL); - app_data->use_secret_service = g_key_file_get_boolean (kf, "config", "use_secret_service", &err); - app_data->use_tray = g_key_file_get_boolean (kf, "config", "use_tray", NULL); - if (err != NULL && g_error_matches (err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { - // if the key is not found, we set it to TRUE and save it to the config file. - app_data->use_secret_service = TRUE; + // if config file is not set, we use the default values. g_clear_error (&err); + g_key_file_set_boolean (kf, "config", "show_next_otp", app_data->show_next_otp); + g_key_file_set_boolean (kf, "config", "notifications", app_data->disable_notifications); + g_key_file_set_boolean (kf, "config", "auto_lock", app_data->auto_lock); + g_key_file_set_integer (kf, "config", "inactivity_timeout", app_data->inactivity_timeout); + g_key_file_set_boolean (kf, "config", "dark_theme", app_data->use_dark_theme); + g_key_file_set_boolean (kf, "config", "use_secret_service", app_data->use_secret_service); + g_key_file_set_boolean (kf, "config", "use_tray", app_data->use_tray); + if (!g_key_file_save_to_file (kf, cfg_file_path, &err)) { + gchar *msg = g_strconcat (_("Couldn't save default settings: "), err->message, NULL); + show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_WARNING); + g_free (msg); + g_clear_error (&err); + } + } else { + // if key is not found, g_key_file_get_boolean returns FALSE and g_key_file_get_integer returns 0. + // Therefore, having these values as default is exactly what we want. So no need to check whether the key is missing. + app_data->show_next_otp = g_key_file_get_boolean (kf, "config", "show_next_otp", NULL); + app_data->disable_notifications = g_key_file_get_boolean (kf, "config", "notifications", NULL); + app_data->auto_lock = g_key_file_get_boolean (kf, "config", "auto_lock", NULL); + app_data->inactivity_timeout = g_key_file_get_integer (kf, "config", "inactivity_timeout", NULL); + app_data->use_dark_theme = g_key_file_get_boolean (kf, "config", "dark_theme", NULL); + app_data->use_secret_service = g_key_file_get_boolean (kf, "config", "use_secret_service", &err); + app_data->use_tray = g_key_file_get_boolean (kf, "config", "use_tray", NULL); + if (err != NULL && g_error_matches (err, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_KEY_NOT_FOUND)) { + // if the key is not found, we set it to TRUE and save it to the config file. + app_data->use_secret_service = TRUE; + g_clear_error (&err); + } } GtkBuilder *builder = get_builder_from_partial_path(UI_PARTIAL_PATH); GtkWidget *dialog = GTK_WIDGET(gtk_builder_get_object (builder, "settings_diag_id")); GtkWidget *sno_switch = GTK_WIDGET(gtk_builder_get_object (builder, "nextotp_switch_id")); GtkWidget *dn_switch = GTK_WIDGET(gtk_builder_get_object (builder, "notif_switch_id")); - GtkWidget *sc_cb = GTK_WIDGET(gtk_builder_get_object (builder, "search_by_cb_id")); settings_data->al_switch = GTK_WIDGET(gtk_builder_get_object (builder, "autolock_switch_id")); g_signal_connect (settings_data->al_switch, "state-set", G_CALLBACK(handle_secretservice_switch), settings_data); settings_data->inactivity_cb = GTK_WIDGET(gtk_builder_get_object (builder, "autolock_inactive_cb_id")); @@ -111,10 +116,7 @@ gtk_switch_set_active (GTK_SWITCH(dt_switch), app_data->use_dark_theme); gtk_switch_set_active (GTK_SWITCH(settings_data->dss_switch), app_data->use_secret_service); gtk_switch_set_active (GTK_SWITCH(settings_data->tray_switch), app_data->use_tray); - gchar *active_id_string = g_strdup_printf ("%d", app_data->search_column); - gtk_combo_box_set_active_id (GTK_COMBO_BOX(sc_cb), active_id_string); - g_free (active_id_string); - active_id_string = g_strdup_printf ("%d", app_data->inactivity_timeout); + gchar *active_id_string = g_strdup_printf ("%d", app_data->inactivity_timeout); gtk_combo_box_set_active_id (GTK_COMBO_BOX(settings_data->inactivity_cb), active_id_string); g_free (active_id_string); @@ -127,7 +129,6 @@ case GTK_RESPONSE_OK: app_data->show_next_otp = gtk_switch_get_active (GTK_SWITCH(sno_switch)); app_data->disable_notifications = gtk_switch_get_active (GTK_SWITCH(dn_switch)); - app_data->search_column = (gint)g_ascii_strtoll (gtk_combo_box_get_active_id (GTK_COMBO_BOX(sc_cb)), NULL, 10); app_data->auto_lock = gtk_switch_get_active (GTK_SWITCH(settings_data->al_switch)); app_data->inactivity_timeout = (gint)g_ascii_strtoll (gtk_combo_box_get_active_id (GTK_COMBO_BOX(settings_data->inactivity_cb)), NULL, 10); app_data->use_dark_theme = gtk_switch_get_active (GTK_SWITCH(dt_switch)); @@ -135,7 +136,6 @@ app_data->use_tray = gtk_switch_get_active (GTK_SWITCH(settings_data->tray_switch)); g_key_file_set_boolean (kf, "config", "show_next_otp", app_data->show_next_otp); g_key_file_set_boolean (kf, "config", "notifications", app_data->disable_notifications); - g_key_file_set_integer (kf, "config", "search_column", app_data->search_column); g_key_file_set_boolean (kf, "config", "auto_lock", app_data->auto_lock); g_key_file_set_integer (kf, "config", "inactivity_timeout", app_data->inactivity_timeout); g_key_file_set_boolean (kf, "config", "dark_theme", app_data->use_dark_theme); @@ -148,7 +148,9 @@ if (!g_key_file_save_to_file (kf, cfg_file_path, NULL)) { g_printerr ("%s\n", _("Error while saving the config file.")); } - gtk_tree_view_set_search_column (GTK_TREE_VIEW(app_data->tree_view), app_data->search_column + 1); + if (app_data->filter_model != NULL) { + gtk_tree_model_filter_refilter (app_data->filter_model); + } break; case GTK_RESPONSE_CANCEL: break; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/show-qr-cb.c new/OTPClient-4.2.0/src/gui/show-qr-cb.c --- old/OTPClient-4.1.0/src/gui/show-qr-cb.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/show-qr-cb.c 2025-12-23 13:40:00.000000000 +0100 @@ -25,11 +25,10 @@ { CAST_USER_DATA(AppData, app_data, user_data); - GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); - GtkListStore *list_store = GTK_LIST_STORE(model); + GtkListStore *list_store = NULL; GtkTreeIter iter; - if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (app_data->tree_view), &model, &iter) == FALSE) { + if (!get_selected_liststore_iter (app_data, &list_store, &iter)) { show_message_dialog (app_data->main_window, "Error: a row must be selected in order to get the QR Code.", GTK_MESSAGE_ERROR); return; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/treeview.c new/OTPClient-4.2.0/src/gui/treeview.c --- old/OTPClient-4.1.0/src/gui/treeview.c 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/treeview.c 2025-12-23 13:40:00.000000000 +0100 @@ -6,17 +6,11 @@ #include "message-dialogs.h" #include "edit-row-cb.h" #include "show-qr-cb.h" +#include "gui-misc.h" -typedef struct parsed_json_data_t { - gchar **types; - gchar **labels; - gchar **issuers; - GArray *periods; -} ParsedData; - -static void set_json_data (json_t *array, - ParsedData *pjd); +static const gchar *json_get_string_or_empty (json_t *obj, + const gchar *key); static void add_data_to_model (DatabaseData *db_data, GtkListStore *store); @@ -33,9 +27,29 @@ GtkTreeIter *iter, gpointer user_data); -static void free_pjd (ParsedData *pjd); +static gboolean on_treeview_button_press_event (GtkWidget *treeview, + GdkEventButton *event, + gpointer user_data); + +static gboolean filter_visible_func (GtkTreeModel *model, + GtkTreeIter *iter, + gpointer user_data); + +static gboolean row_matches_query (GtkTreeModel *model, + GtkTreeIter *iter, + const gchar *query_folded); + +static void search_entry_changed_cb (GtkEntry *entry, + gpointer user_data); + +static void search_entry_activate_cb (GtkEntry *entry, + gpointer user_data); + +static void select_first_row (AppData *app_data); -static gboolean on_treeview_button_press_event (GtkWidget *treeview, GdkEventButton *event, gpointer user_data); +static gboolean get_liststore_iter_from_path (AppData *app_data, + GtkTreePath *path, + GtkTreeIter *iter); void @@ -43,17 +57,24 @@ { app_data->tree_view = GTK_TREE_VIEW(gtk_builder_get_object (app_data->builder, "treeview_id")); - GtkListStore *list_store = gtk_list_store_new (NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, - G_TYPE_UINT, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_INT); + app_data->list_store = gtk_list_store_new (NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, + G_TYPE_UINT, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_INT); add_columns (app_data->tree_view); - add_data_to_model (app_data->db_data, list_store); + add_data_to_model (app_data->db_data, app_data->list_store); - gtk_tree_view_set_model (app_data->tree_view, GTK_TREE_MODEL(list_store)); + app_data->filter_model = GTK_TREE_MODEL_FILTER(gtk_tree_model_filter_new (GTK_TREE_MODEL(app_data->list_store), NULL)); + gtk_tree_model_filter_set_visible_func (app_data->filter_model, filter_visible_func, app_data, NULL); - // model has id 0 for type, 1 for label, 2 for issuer, etc while ui file has 0 label and 1 issuer. That's why the "+1" - gtk_tree_view_set_search_column (GTK_TREE_VIEW(app_data->tree_view), app_data->search_column + 1); + gtk_tree_view_set_model (app_data->tree_view, GTK_TREE_MODEL(app_data->filter_model)); + + app_data->search_entry = GTK_WIDGET(gtk_builder_get_object (app_data->builder, "search_entry_id")); + if (app_data->search_entry != NULL) { + gtk_tree_view_set_search_entry (GTK_TREE_VIEW(app_data->tree_view), GTK_ENTRY(app_data->search_entry)); + g_signal_connect (app_data->search_entry, "changed", G_CALLBACK(search_entry_changed_cb), app_data); + g_signal_connect (app_data->search_entry, "activate", G_CALLBACK(search_entry_activate_cb), app_data); + } GtkBindingSet *tv_binding_set = gtk_binding_set_by_class (GTK_TREE_VIEW_GET_CLASS(app_data->tree_view)); g_signal_new ("hide-all-otps", G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); @@ -68,7 +89,7 @@ // signal emitted when right-clicked on a row (shows edit/delete context menu) g_signal_connect(app_data->tree_view, "button-press-event", G_CALLBACK(on_treeview_button_press_event), app_data); - g_object_unref (list_store); + select_first_row (app_data); } @@ -76,46 +97,53 @@ update_model (AppData *app_data) { if (app_data->tree_view != NULL) { - GtkListStore *store = GTK_LIST_STORE(gtk_tree_view_get_model (app_data->tree_view)); + GtkListStore *store = app_data->list_store; + if (store == NULL) { + return; + } gtk_list_store_clear (store); add_data_to_model (app_data->db_data, store); + if (app_data->filter_model != NULL) { + gtk_tree_model_filter_refilter (app_data->filter_model); + } } } void -row_selected_cb (GtkTreeView *tree_view, +row_selected_cb (GtkTreeView *tree_view UNUSED, GtkTreePath *path, GtkTreeViewColumn *column UNUSED, gpointer user_data) { CAST_USER_DATA(AppData, app_data, user_data); if (app_data->is_reorder_active == FALSE) { - GtkTreeModel *model = gtk_tree_view_get_model (tree_view); - GtkTreeIter iter; - gtk_tree_model_get_iter (model, &iter, path); + if (!get_liststore_iter_from_path (app_data, path, &iter)) { + return; + } - gchar *otp_type, *otp_value; - gtk_tree_model_get (model, &iter, COLUMN_TYPE, &otp_type, -1); - gtk_tree_model_get (model, &iter, COLUMN_OTP, &otp_value, -1); + gchar *otp_type = NULL; + gchar *otp_value = NULL; + gtk_tree_model_get (GTK_TREE_MODEL(app_data->list_store), &iter, + COLUMN_TYPE, &otp_type, + COLUMN_OTP, &otp_value, + -1); GDateTime *now = g_date_time_new_now_local (); GTimeSpan diff = g_date_time_difference (now, app_data->db_data->last_hotp_update); - if (otp_value != NULL && g_utf8_strlen (otp_value, -1) > 3) { - // OTP is already set, so we update the value only if it is an HOTP - if (g_ascii_strcasecmp (otp_type, "HOTP") == 0) { - if (diff >= G_USEC_PER_SEC * HOTP_RATE_LIMIT_IN_SEC) { - set_otp (GTK_LIST_STORE (model), iter, app_data); - g_free (otp_value); - gtk_tree_model_get (model, &iter, COLUMN_OTP, &otp_value, -1); - } - } - } else { - // OTP is not already set, so we set it - set_otp (GTK_LIST_STORE (model), iter, app_data); + gboolean should_update = (otp_value == NULL || g_utf8_strlen (otp_value, -1) <= 3); + if (!should_update && otp_type != NULL && g_ascii_strcasecmp (otp_type, "HOTP") == 0) { + should_update = (diff >= G_USEC_PER_SEC * HOTP_RATE_LIMIT_IN_SEC); + } + + if (should_update) { + set_otp (app_data->list_store, iter, app_data); g_free (otp_value); - gtk_tree_model_get (model, &iter, COLUMN_OTP, &otp_value, -1); + otp_value = NULL; + gtk_tree_model_get (GTK_TREE_MODEL(app_data->list_store), &iter, + COLUMN_OTP, &otp_value, + -1); } // and, in any case, we copy the otp to the clipboard and send a notification gtk_clipboard_set_text (app_data->clipboard, otp_value, -1); @@ -138,21 +166,25 @@ GSList *nodes_order_slist = NULL; GtkTreeIter iter; guint current_db_pos; - GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); + GtkTreeModel *model = GTK_TREE_MODEL(app_data->list_store); gint slist_len = 0; gboolean valid = gtk_tree_model_get_iter_first (model, &iter); while (valid) { GtkTreePath *path = gtk_tree_model_get_path (model, &iter); + if (path == NULL) { + valid = gtk_tree_model_iter_next(model, &iter); + continue; + } gtk_tree_model_get (model, &iter, COLUMN_POSITION_IN_DB, ¤t_db_pos, -1); - if (gtk_tree_path_get_indices (path)[0] != current_db_pos) { + gint *indices = gtk_tree_path_get_indices (path); + if (indices != NULL && indices[0] != (gint)current_db_pos) { NodeInfo *node_info = g_new0 (NodeInfo, 1); json_t *obj = json_array_get (app_data->db_data->in_memory_json_data, current_db_pos); - node_info->newpos = gtk_tree_path_get_indices (path)[0]; + node_info->newpos = indices[0]; node_info->hash = json_object_get_hash (obj); - nodes_order_slist = g_slist_append (nodes_order_slist, g_memdup2 (node_info, sizeof (NodeInfo))); + nodes_order_slist = g_slist_append (nodes_order_slist, node_info); slist_len++; - g_free (node_info); } gtk_tree_path_free (path); valid = gtk_tree_model_iter_next(model, &iter); @@ -174,7 +206,6 @@ json_decref (obj); } } - g_free (ni); } // update the database and reload the changes @@ -197,7 +228,7 @@ } regenerate_model (app_data); - g_slist_free (nodes_order_slist); + g_slist_free_full (nodes_order_slist, g_free); } @@ -215,10 +246,9 @@ { g_return_if_fail (app_data->tree_view != NULL); - GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); - GtkTreeSelection *selection = gtk_tree_view_get_selection (GTK_TREE_VIEW(app_data->tree_view)); + GtkListStore *list_store = NULL; GtkTreeIter iter; - if (!gtk_tree_selection_get_selected (selection, &model, &iter)) { + if (!get_selected_liststore_iter (app_data, &list_store, &iter)) { show_message_dialog (app_data->main_window, "No row has been selected. Nothing will be deleted.", GTK_MESSAGE_ERROR); return; } @@ -243,21 +273,21 @@ } gint db_item_position_to_delete; - gtk_tree_model_get (model, &iter, COLUMN_POSITION_IN_DB, &db_item_position_to_delete, -1); + gtk_tree_model_get (GTK_TREE_MODEL(list_store), &iter, COLUMN_POSITION_IN_DB, &db_item_position_to_delete, -1); json_array_remove (app_data->db_data->in_memory_json_data, db_item_position_to_delete); - gtk_list_store_remove (GTK_LIST_STORE(model), &iter); + gtk_list_store_remove (list_store, &iter); // json_array_remove shifts all items, so we have to take care of updating the real item's position in the database gint row_db_pos; - gboolean valid = gtk_tree_model_get_iter_first(model, &iter); + gboolean valid = gtk_tree_model_get_iter_first (GTK_TREE_MODEL(list_store), &iter); while (valid) { - gtk_tree_model_get (model, &iter, COLUMN_POSITION_IN_DB, &row_db_pos, -1); + gtk_tree_model_get (GTK_TREE_MODEL(list_store), &iter, COLUMN_POSITION_IN_DB, &row_db_pos, -1); if (row_db_pos > db_item_position_to_delete) { gint shifted_position = row_db_pos - 1; - gtk_list_store_set (GTK_LIST_STORE(model), &iter, COLUMN_POSITION_IN_DB, shifted_position, -1); + gtk_list_store_set (list_store, &iter, COLUMN_POSITION_IN_DB, shifted_position, -1); } - valid = gtk_tree_model_iter_next(model, &iter); + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(list_store), &iter); } GError *err = NULL; @@ -266,12 +296,14 @@ gchar *msg = g_strconcat ("The database update <b>FAILED</b>. The error message is:\n", err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); + g_clear_error (&err); } else { reload_db (app_data->db_data, &err); if (err != NULL) { gchar *msg = g_strconcat ("The database update <b>FAILED</b>. The error message is:\n", err->message, NULL); show_message_dialog (app_data->main_window, msg, GTK_MESSAGE_ERROR); g_free (msg); + g_clear_error (&err); } } } @@ -310,8 +342,8 @@ GtkWidget *menu = gtk_menu_new (); GtkWidget *menu_item = gtk_menu_item_new_with_label ("Edit row"); - g_signal_connect(menu_item, "activate", G_CALLBACK (edit_row_cb), app_data); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + g_signal_connect (menu_item, "activate", G_CALLBACK (edit_row_cb), app_data); + gtk_menu_shell_append (GTK_MENU_SHELL(menu), menu_item); menu_item = gtk_menu_item_new_with_label ("Delete row"); g_signal_connect (menu_item, "activate", G_CALLBACK (on_delete_activate), app_data); @@ -332,13 +364,16 @@ static void -hide_all_otps_cb (GtkTreeView *tree_view, +hide_all_otps_cb (GtkTreeView *tree_view UNUSED, gpointer user_data) { - gtk_tree_model_foreach (GTK_TREE_MODEL(gtk_tree_view_get_model (tree_view)), clear_all_otps, user_data); + CAST_USER_DATA(AppData, app_data, user_data); + if (app_data->list_store == NULL) { + return; + } + gtk_tree_model_foreach (GTK_TREE_MODEL(app_data->list_store), clear_all_otps, user_data); } - static gboolean clear_all_otps (GtkTreeModel *model, GtkTreePath *path UNUSED, @@ -359,26 +394,140 @@ } +static gboolean +filter_visible_func (GtkTreeModel *model, + GtkTreeIter *iter, + gpointer user_data) +{ + CAST_USER_DATA(AppData, app_data, user_data); + if (app_data->search_entry == NULL) { + return TRUE; + } + + const gchar *query = gtk_entry_get_text (GTK_ENTRY(app_data->search_entry)); + if (query == NULL || *query == '\0') { + return TRUE; + } + + gchar *query_folded = g_utf8_strdown (query, -1); + gboolean match = row_matches_query (model, iter, query_folded); + g_free (query_folded); + + return match; +} + + +static gboolean +row_matches_query (GtkTreeModel *model, + GtkTreeIter *iter, + const gchar *query_folded) +{ + gchar *type = NULL; + gchar *label = NULL; + gchar *issuer = NULL; + gtk_tree_model_get (model, iter, + COLUMN_TYPE, &type, + COLUMN_ACC_LABEL, &label, + COLUMN_ACC_ISSUER, &issuer, + -1); + + gboolean match = FALSE; + if (type != NULL) { + gchar *type_folded = g_utf8_strdown (type, -1); + match = (g_strstr_len (type_folded, -1, query_folded) != NULL); + g_free (type_folded); + } + + if (!match && label != NULL) { + gchar *label_folded = g_utf8_strdown (label, -1); + match = (g_strstr_len (label_folded, -1, query_folded) != NULL); + g_free (label_folded); + } + + if (!match && issuer != NULL) { + gchar *issuer_folded = g_utf8_strdown (issuer, -1); + match = (g_strstr_len (issuer_folded, -1, query_folded) != NULL); + g_free (issuer_folded); + } + + g_free (type); + g_free (label); + g_free (issuer); + + return match; +} + +static void +search_entry_changed_cb (GtkEntry *entry UNUSED, + gpointer user_data) +{ + CAST_USER_DATA(AppData, app_data, user_data); + if (app_data->filter_model != NULL) { + gtk_tree_model_filter_refilter (app_data->filter_model); + } + select_first_row (app_data); +} + static void -set_json_data (json_t *array, - ParsedData *pjd) +search_entry_activate_cb (GtkEntry *entry UNUSED, + gpointer user_data) { - gsize array_len = json_array_size (array); - pjd->types = (gchar **)g_malloc0 ((array_len + 1) * sizeof(gchar *)); - pjd->labels = (gchar **)g_malloc0 ((array_len + 1) * sizeof(gchar *)); - pjd->issuers = (gchar **)g_malloc0 ((array_len + 1) * sizeof(gchar *)); - pjd->periods = g_array_new (FALSE, FALSE, sizeof(gint)); - for (guint i = 0; i < array_len; i++) { - json_t *obj = json_array_get (array, i); - pjd->types[i] = g_strdup (json_string_value (json_object_get (obj, "type"))); - pjd->labels[i] = g_strdup (json_string_value (json_object_get (obj, "label"))); - pjd->issuers[i] = g_strdup (json_string_value (json_object_get (obj, "issuer"))); - json_int_t period = json_integer_value (json_object_get (obj, "period")); - g_array_append_val (pjd->periods, period); - } - pjd->types[array_len] = NULL; - pjd->labels[array_len] = NULL; - pjd->issuers[array_len] = NULL; + CAST_USER_DATA(AppData, app_data, user_data); + GtkTreeModel *model = GTK_TREE_MODEL(app_data->filter_model); + GtkTreeSelection *selection = gtk_tree_view_get_selection (app_data->tree_view); + if (model == NULL) { + return; + } + + if (gtk_tree_selection_count_selected_rows (selection) == 0) { + select_first_row (app_data); + } + + GList *paths = gtk_tree_selection_get_selected_rows (selection, &model); + if (paths != NULL) { + GtkTreePath *path = g_list_first (paths)->data; + GtkTreeViewColumn *column = gtk_tree_view_get_column (app_data->tree_view, 0); + gtk_tree_view_row_activated (app_data->tree_view, path, column); + g_list_free_full (paths, (GDestroyNotify)gtk_tree_path_free); + } +} + +static void +select_first_row (AppData *app_data) +{ + if (app_data->filter_model == NULL) { + return; + } + GtkTreeIter iter; + GtkTreeModel *model = GTK_TREE_MODEL(app_data->filter_model); + GtkTreeSelection *selection = gtk_tree_view_get_selection (app_data->tree_view); + gtk_tree_selection_unselect_all (selection); + if (gtk_tree_model_get_iter_first (model, &iter)) { + GtkTreePath *path = gtk_tree_model_get_path (model, &iter); + gtk_tree_selection_select_path (selection, path); + gtk_tree_view_scroll_to_cell (app_data->tree_view, path, NULL, FALSE, 0.0f, 0.0f); + gtk_tree_path_free (path); + } +} + +static gboolean +get_liststore_iter_from_path (AppData *app_data, + GtkTreePath *path, + GtkTreeIter *iter) +{ + GtkTreeModel *model = gtk_tree_view_get_model (app_data->tree_view); + GtkTreeIter view_iter; + if (!gtk_tree_model_get_iter (model, &view_iter, path)) { + return FALSE; + } + + if (GTK_IS_TREE_MODEL_FILTER (model)) { + gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER(model), iter, &view_iter); + return TRUE; + } + + *iter = view_iter; + return TRUE; } @@ -386,26 +535,31 @@ add_data_to_model (DatabaseData *db_data, GtkListStore *store) { - GtkTreeIter iter; - ParsedData *pjd = g_new0 (ParsedData, 1); + if (db_data == NULL || store == NULL || db_data->in_memory_json_data == NULL) { + return; + } - set_json_data (db_data->in_memory_json_data, pjd); + gsize array_len = json_array_size (db_data->in_memory_json_data); + for (guint i = 0; i < array_len; i++) { + json_t *obj = json_array_get (db_data->in_memory_json_data, i); + const gchar *type = json_get_string_or_empty (obj, "type"); + const gchar *label = json_get_string_or_empty (obj, "label"); + const gchar *issuer = json_get_string_or_empty (obj, "issuer"); + json_t *period_value = json_object_get (obj, "period"); + gint period = json_is_integer (period_value) ? (gint)json_integer_value (period_value) : 0; - gint i = 0; - while (pjd->types[i] != NULL) { + GtkTreeIter iter; gtk_list_store_append (store, &iter); gtk_list_store_set (store, &iter, - COLUMN_TYPE, pjd->types[i], - COLUMN_ACC_LABEL, pjd->labels[i], - COLUMN_ACC_ISSUER, pjd->issuers[i], - COLUMN_PERIOD, g_array_index (pjd->periods, gint, i), + COLUMN_TYPE, type, + COLUMN_ACC_LABEL, label, + COLUMN_ACC_ISSUER, issuer, + COLUMN_PERIOD, period, COLUMN_UPDATED, FALSE, COLUMN_LESS_THAN_A_MINUTE, FALSE, COLUMN_POSITION_IN_DB, i, -1); - i++; } - free_pjd (pjd); } @@ -440,12 +594,15 @@ } -static void -free_pjd (ParsedData *pjd) +static const gchar * +json_get_string_or_empty (json_t *obj, + const gchar *key) { - g_strfreev (pjd->types); - g_strfreev (pjd->labels); - g_strfreev (pjd->issuers); - g_array_free (pjd->periods, TRUE); - g_free (pjd); + if (obj == NULL || key == NULL) { + return ""; + } + + json_t *value = json_object_get (obj, key); + const gchar *string_value = json_string_value (value); + return string_value != NULL ? string_value : ""; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/OTPClient-4.1.0/src/gui/ui/otpclient.ui new/OTPClient-4.2.0/src/gui/ui/otpclient.ui --- old/OTPClient-4.1.0/src/gui/ui/otpclient.ui 2025-05-07 15:52:20.000000000 +0200 +++ new/OTPClient-4.2.0/src/gui/ui/otpclient.ui 2025-12-23 13:40:00.000000000 +0100 @@ -1237,6 +1237,19 @@ <property name="can-focus">False</property> <property name="orientation">vertical</property> <child> + <object class="GtkSearchEntry" id="search_entry_id"> + <property name="visible">False</property> + <property name="can-focus">True</property> + <property name="hexpand">True</property> + <property name="placeholder-text" translatable="yes">Search</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> <object class="GtkScrolledWindow" id="scrolledwin_id"> <property name="visible">True</property> <property name="can-focus">True</property> @@ -2020,17 +2033,6 @@ </packing> </child> <child> - <object class="GtkLabel"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="label" translatable="yes">Search by</property> - </object> - <packing> - <property name="left-attach">0</property> - <property name="top-attach">2</property> - </packing> - </child> - <child> <object class="GtkSwitch" id="notif_switch_id"> <property name="visible">True</property> <property name="can-focus">True</property> @@ -2043,22 +2045,6 @@ </packing> </child> <child> - <object class="GtkComboBoxText" id="search_by_cb_id"> - <property name="visible">True</property> - <property name="can-focus">False</property> - <property name="active">0</property> - <property name="active-id">0</property> - <items> - <item id="0" translatable="yes">Account</item> - <item id="1" translatable="yes">Issuer</item> - </items> - </object> - <packing> - <property name="left-attach">1</property> - <property name="top-attach">2</property> - </packing> - </child> - <child> <object class="GtkSwitch" id="nextotp_switch_id"> <property name="visible">True</property> <property name="can-focus">True</property>
