/** * @file gnome-cmd-search-dialog.cc * @copyright (C) 2001-2006 Marcus Bjurman\n * @copyright (C) 2007-2012 Piotr Eljasiak\n * @copyright (C) 2013-2021 Uwe Scholz\n * * @copyright This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * @copyright This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * @copyright You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include "gnome-cmd-includes.h" #include "gnome-cmd-data.h" #include "gnome-cmd-search-dialog.h" #include "gnome-cmd-dir.h" #include "gnome-cmd-file-list.h" #include "gnome-cmd-file-selector.h" #include "gnome-cmd-main-win.h" #include "gnome-cmd-con-list.h" #include "gnome-cmd-selection-profile-component.h" #include "gnome-cmd-manage-profiles-dialog.h" #include "dirlist.h" #include "filter.h" #include "utils.h" using namespace std; #if 0 static char *msgs[] = {N_("Search local directories only"), N_("Files _not containing text")}; #endif #define PBAR_MAX 50 // Absolute width of a progress bar #define SEARCH_JUMP_SIZE 4096U #define SEARCH_BUFFER_SIZE (SEARCH_JUMP_SIZE * 10U) #define GNOME_SEARCH_TOOL_REFRESH_DURATION 50000 struct GnomeCmdSearchDialogClass { GtkDialogClass parent_class; }; struct SearchFileData { GFileInputStream *inputStream; gchar *uriString; guint64 size; guint64 offset; guint len; gchar mem[SEARCH_BUFFER_SIZE]; // memory to search in the content of a file }; struct SearchData { struct ProtectedData { GList *files; gchar *msg; GMutex mutex; ProtectedData() : files(nullptr), msg(nullptr) {} }; GnomeCmdSearchDialog *dialog; // the directory to start searching from GnomeCmdDir *start_dir = nullptr; Filter *name_filter = nullptr; regex_t *content_regex = nullptr; // the context id of the status bar gint context_id = 0; //the directories which we found matching files in GList *match_dirs = nullptr; GThread *thread = nullptr; ProtectedData pdata; gint update_gui_timeout_id {0}; GPid pid = 0; GCancellable* cancellable = nullptr; gboolean search_done = TRUE; //stops the search routine if set to TRUE. This is done by the stop_button gboolean stopped = TRUE; // set when the search dialog is destroyed, also stops the search of course gboolean dialog_destroyed = FALSE; explicit SearchData(GnomeCmdSearchDialog *dlg); void set_statusmsg(const gchar *msg = nullptr); gchar *BuildSearchCommand(); // searches a given directory for files that matches the criteria given by data void SearchDirRecursive(GnomeCmdDir *dir, long level); // determines if the name of a file matches an regexp gboolean NameMatches(const char *name) { return name_filter->match(name); } // determines if the content of a file matches an regexp gboolean ContentMatches(GFile *f, GFileInfo *info); // loads a file in chunks and returns the content gboolean ReadSearchFile(SearchFileData *searchFileData); gboolean StartGenericSearch(); gboolean StartLocalSearch(); }; struct GnomeCmdSearchDialog::Private { // holds data needed by the search routines SearchData data; GtkWidget *vbox; GnomeCmdSelectionProfileComponent *profile_component; GtkWidget *dir_browser; GnomeCmdFileList *result_list; GtkWidget *statusbar; GtkWidget *pbar; GtkWidget *profile_menu_button; explicit Private(GnomeCmdSearchDialog *dlg); ~Private(); static gchar *translate_menu(const gchar *path, gpointer data); GtkWidget *create_placeholder_menu(GnomeCmdData::SearchConfig &cfg); GtkWidget *create_button_with_menu(gchar *label_text, GnomeCmdData::SearchConfig &cfg); static void manage_profiles(GnomeCmdSearchDialog::Private *priv, guint unused, GtkWidget *menu); static void load_profile(GnomeCmdSearchDialog::Private *priv, guint profile_idx, GtkWidget *menu); static gboolean join_thread_func(GnomeCmdSearchDialog::Private *priv); static gboolean on_list_keypressed (GtkWidget *result_list, GdkEventKey *event, gpointer unused); static void on_dialog_show (GtkWidget *widget, GnomeCmdSearchDialog *dialog); static void on_dialog_hide (GtkWidget *widget, GnomeCmdSearchDialog *dialog); static gboolean on_dialog_delete (GtkWidget *widget, GdkEvent *event, GnomeCmdSearchDialog *dialog); static void on_dialog_size_allocate (GtkWidget *widget, GtkAllocation *allocation, GnomeCmdSearchDialog *dialog); static void on_dialog_response (GtkDialog *window, int response_id, GnomeCmdSearchDialog *dialog); }; inline GnomeCmdSearchDialog::Private::Private(GnomeCmdSearchDialog *dlg): data(dlg) { vbox = nullptr; profile_component = nullptr; dir_browser = nullptr; result_list = nullptr; statusbar = nullptr; pbar = nullptr; profile_menu_button = nullptr; } inline GnomeCmdSearchDialog::Private::~Private() { } gchar *GnomeCmdSearchDialog::Private::translate_menu(const gchar *path, gpointer data) { return _(path); } inline GtkWidget *GnomeCmdSearchDialog::Private::create_placeholder_menu(GnomeCmdData::SearchConfig &cfg) { guint items_size = cfg.profiles.empty() ? 1 : cfg.profiles.size()+3; GtkItemFactoryEntry *items = g_try_new0 (GtkItemFactoryEntry, items_size); g_return_val_if_fail (items!=nullptr, nullptr); GtkItemFactoryEntry *i = items; i->path = g_strdup (_("/_Save Profile As…")); i->callback = (GtkItemFactoryCallback) manage_profiles; i->callback_action = TRUE; i->item_type = (gchar*) ""; i->extra_data = GTK_STOCK_SAVE_AS; ++i; if (!cfg.profiles.empty()) { i->path = g_strdup (_("/_Manage Profiles…")); i->callback = (GtkItemFactoryCallback) manage_profiles; i->item_type = (gchar*) ""; i->extra_data = GTK_STOCK_EDIT; ++i; i->path = g_strdup ("/"); i->item_type = (gchar*) ""; ++i; for (vector::const_iterator p=cfg.profiles.begin(); p!=cfg.profiles.end(); ++p, ++i) { i->path = g_strconcat ("/", p->name.c_str(), nullptr); i->callback = (GtkItemFactoryCallback) load_profile; i->callback_action = (i-items)-3; i->item_type = (gchar*) ""; i->extra_data = GTK_STOCK_REVERT_TO_SAVED; } } GtkItemFactory *ifac = gtk_item_factory_new (GTK_TYPE_MENU, "
", nullptr); gtk_item_factory_create_items (ifac, items_size, items, this); for (guint ii=0; ii"); } inline GtkWidget *GnomeCmdSearchDialog::Private::create_button_with_menu(gchar *label_text, GnomeCmdData::SearchConfig &cfg) { profile_menu_button = gnome_cmd_button_menu_new (label_text, create_placeholder_menu(cfg)); return profile_menu_button; } void GnomeCmdSearchDialog::Private::manage_profiles(GnomeCmdSearchDialog::Private *priv, guint new_profile, GtkWidget *widget) { auto *dialog = reinterpret_cast (gtk_widget_get_ancestor (priv->profile_menu_button, GNOME_CMD_TYPE_SEARCH_DIALOG)); g_return_if_fail (dialog != nullptr); if (new_profile) priv->profile_component->copy(); if (GnomeCmd::ManageProfilesDialog (*dialog,dialog->defaults,new_profile,_("Profiles"),"gnome-commander-search")) { GtkWidget *menu = gtk_widget_get_parent (widget); gnome_cmd_button_menu_disconnect_handler (priv->profile_menu_button, menu); g_object_unref (gtk_item_factory_from_widget (menu)); gnome_cmd_button_menu_connect_handler (priv->profile_menu_button, priv->create_placeholder_menu(dialog->defaults)); } } void GnomeCmdSearchDialog::Private::load_profile(GnomeCmdSearchDialog::Private *priv, guint profile_idx, GtkWidget *widget) { GtkWidget *dialog = gtk_widget_get_ancestor (priv->profile_menu_button, GNOME_CMD_TYPE_SEARCH_DIALOG); g_return_if_fail (dialog!=nullptr); GnomeCmdData::SearchConfig &cfg = GNOME_CMD_SEARCH_DIALOG(dialog)->defaults; g_return_if_fail (profile_idxprofile_component->update(); } inline SearchData::SearchData(GnomeCmdSearchDialog *dlg): dialog(dlg) { } G_DEFINE_TYPE (GnomeCmdSearchDialog, gnome_cmd_search_dialog, GTK_TYPE_DIALOG) inline void free_search_file_data (SearchFileData *searchfileData) { if (searchfileData->inputStream) g_object_unref(searchfileData->inputStream); g_free (searchfileData->uriString); g_free (searchfileData); } gboolean SearchData::ReadSearchFile(SearchFileData *searchFileData) { if (stopped) // if the stop button was pressed, let's abort here { return FALSE; } if (searchFileData->len) { if ((searchFileData->offset + searchFileData->len) >= searchFileData->size) // end, all has been read { return FALSE; } // jump a big step backward to give the regex a chance searchFileData->offset += searchFileData->len - SEARCH_JUMP_SIZE; searchFileData->len = MIN (searchFileData->size - searchFileData->offset, SEARCH_BUFFER_SIZE - 1); } else // first time call of this function { searchFileData->len = MIN (searchFileData->size, SEARCH_BUFFER_SIZE - 1); if (!g_seekable_can_seek ((GSeekable *) searchFileData->inputStream)) { g_warning (_("Failed to read file %s: File is not searchable"), searchFileData->uriString); return FALSE; } } GError *error = nullptr; if (!g_seekable_seek ((GSeekable *) searchFileData->inputStream, searchFileData->offset, G_SEEK_SET, cancellable, &error) || error) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning (_("Failed to read file %s: %s"), searchFileData->uriString, error->message); g_error_free(error); return FALSE; } g_input_stream_read ((GInputStream*) searchFileData->inputStream, searchFileData->mem, searchFileData->len, cancellable, &error); if (error) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning (_("Failed to read file %s: %s"), searchFileData->uriString, error->message); g_error_free(error); return FALSE; } searchFileData->mem[searchFileData->len] = '\0'; return TRUE; } inline gboolean SearchData::ContentMatches(GFile *f, GFileInfo *info) { g_return_val_if_fail (f != nullptr, FALSE); guint64 size = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE); if (size == 0) return FALSE; GError *error = nullptr; auto searchFileData = g_new0(SearchFileData, 1); searchFileData->size = size; searchFileData->uriString = g_file_get_uri (f); searchFileData->inputStream = g_file_read (f, cancellable, &error); if (error) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning (_("Failed to read file %s: %s"), searchFileData->uriString, error->message); free_search_file_data (searchFileData); g_error_free(error); return FALSE; } regmatch_t match; gboolean retValue = FALSE; while (ReadSearchFile(searchFileData)) if (regexec (content_regex, searchFileData->mem, 1, &match, 0) != REG_NOMATCH) // ToDo: this only works on text files (i.e. files which don't contain character '\0') { // stop on first match retValue = TRUE; break; } free_search_file_data (searchFileData); return retValue; } inline gboolean handle_list_keypress (GnomeCmdFileList *fl, GdkEventKey *event) { switch (event->keyval) { case GDK_F3: gnome_cmd_file_list_view (fl, gnome_cmd_data.options.use_internal_viewer); return TRUE; case GDK_F4: gnome_cmd_file_list_edit (fl); return TRUE; default: return FALSE; } } inline void SearchData::set_statusmsg(const gchar *msg) { gtk_statusbar_push (GTK_STATUSBAR (dialog->priv->statusbar), context_id, msg ? msg : ""); } void SearchData::SearchDirRecursive(GnomeCmdDir *dir, long level) { if (!dir) return; if (stopped) // if the stop button was pressed, let's abort here return; // update the search status data if (!dialog_destroyed) { g_mutex_lock (&pdata.mutex); g_free (pdata.msg); gchar *path = gnome_cmd_dir_get_display_path (dir); pdata.msg = g_strdup_printf (_("Searching in: %s"), path); g_free (path); g_mutex_unlock (&pdata.mutex); } auto files = gnome_cmd_dir_get_files (dir); gboolean isGnomeCmdFile = TRUE; if (!files || gnome_cmd_dir_is_local (dir)) { // if list is not available or it's a local directory then create a new list, otherwise use already available list // gnome_cmd_dir_list_files is not used for creating a list, beacause it's tied to the GUI and that's not usable from other threads files = sync_dir_list(GNOME_CMD_FILE (dir)->gFile, cancellable); isGnomeCmdFile = FALSE; } // first process the files for (GList *i=files; i; i=i->next) { if (stopped) // if the stop button was pressed, let's abort here break; GFileInfo *info = isGnomeCmdFile ? ((GnomeCmdFile *) i->data)->gFileInfo : (GFileInfo *) i->data; // if the file is a regular one, it might match the search criteria if (g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_STANDARD_TYPE) == G_FILE_TYPE_REGULAR) { // if the name doesn't match, let's go to the next file if (!NameMatches (g_file_info_get_display_name (info))) continue; // if the user wants to we should do some content matching here if (dialog->defaults.default_profile.content_search) { GFile *file = isGnomeCmdFile ? ((GnomeCmdFile *) i->data)->gFile : g_file_get_child (((GnomeCmdFile *) dir)->gFile, g_file_info_get_name (info)); gboolean matches = ContentMatches (file, info); if (!isGnomeCmdFile) g_object_unref (file); if (!matches) continue; } // the file matched the search criteria, let's add it to the list GnomeCmdFile *f; if (isGnomeCmdFile) f = (GnomeCmdFile *) i->data; else { g_object_ref (info); f = gnome_cmd_file_new (info, dir); } g_mutex_lock (&pdata.mutex); pdata.files = g_list_append (pdata.files, f->ref()); g_mutex_unlock (&pdata.mutex); // also ref each directory that has a matching file if (g_list_index (match_dirs, dir) == -1) match_dirs = g_list_append (match_dirs, gnome_cmd_dir_ref (dir)); } } // then process the directories if (level && !stopped) for (GList *i=files; i; i=i->next) { if (stopped) // if the stop button was pressed, let's abort here break; GFileInfo *info = isGnomeCmdFile ? ((GnomeCmdFile *) i->data)->gFileInfo : (GFileInfo *) i->data; // if the current file is a directory, let's continue our recursion if (g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_STANDARD_TYPE) == G_FILE_TYPE_DIRECTORY) { // we don't want to go backwards or to follow symlinks const gchar *displayName = g_file_info_get_display_name(info); if (strcmp (displayName, "..") != 0 && strcmp (displayName, ".") != 0 && !g_file_info_get_attribute_boolean(info, G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK)) { GnomeCmdDir *new_dir = (isGnomeCmdFile && GNOME_CMD_IS_DIR (i->data)) ? (GnomeCmdDir *) i->data : gnome_cmd_dir_new_from_gfileinfo (info, dir); if (new_dir) { gnome_cmd_dir_ref (new_dir); SearchDirRecursive(new_dir, level-1); gnome_cmd_dir_unref (new_dir); } } } } if (!isGnomeCmdFile) g_list_free_full(files, g_object_unref); } static gpointer perform_search_operation (SearchData *data) { GnomeCmdDir *start_dir = data->start_dir; // unref all directories which contained matching files from last search if (data->match_dirs) { g_list_foreach (data->match_dirs, (GFunc) gnome_cmd_dir_unref, nullptr); g_list_free (data->match_dirs); data->match_dirs = nullptr; } data->SearchDirRecursive(start_dir, data->dialog->defaults.default_profile.max_depth); // free regexps delete data->name_filter; data->name_filter = nullptr; if (data->dialog->defaults.default_profile.content_search) { regfree (data->content_regex); g_free (data->content_regex); data->content_regex = nullptr; } gnome_cmd_dir_unref (start_dir); data->search_done = TRUE; return nullptr; } #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" static gboolean update_search_status_widgets (SearchData *data) { // update the progress bar gtk_progress_bar_pulse (GTK_PROGRESS_BAR (data->dialog->priv->pbar)); g_mutex_lock (&data->pdata.mutex); GList *files = data->pdata.files; data->pdata.files = nullptr; // update status bar with the latest message data->set_statusmsg(data->pdata.msg); g_mutex_unlock (&data->pdata.mutex); if (files) { GnomeCmdFileList *fl = data->dialog->priv->result_list; // add all files found since last update to the list for (GList *i = files; i; i = i->next) fl->append_file(GNOME_CMD_FILE (i->data)); gnome_cmd_file_list_free (files); } if ((!data->search_done && !data->stopped) || data->pdata.files) return TRUE; if (!data->dialog_destroyed) { int matches = data->dialog->priv->result_list->size(); gchar *fmt = data->stopped ? ngettext("Found %d match — search aborted", "Found %d matches — search aborted", matches) : ngettext("Found %d match", "Found %d matches", matches); gchar *msg = g_strdup_printf (fmt, matches); data->set_statusmsg(msg); g_free (msg); gtk_widget_hide (data->dialog->priv->pbar); gtk_dialog_set_response_sensitive (*data->dialog, GnomeCmdSearchDialog::GCMD_RESPONSE_GOTO, matches>0); gtk_dialog_set_response_sensitive (*data->dialog, GnomeCmdSearchDialog::GCMD_RESPONSE_STOP, FALSE); gtk_dialog_set_response_sensitive (*data->dialog, GnomeCmdSearchDialog::GCMD_RESPONSE_FIND, TRUE); gtk_dialog_set_default_response (*data->dialog, GnomeCmdSearchDialog::GCMD_RESPONSE_FIND); if (matches) { GnomeCmdFileList *flmatches = data->dialog->priv->result_list; gtk_widget_grab_focus (*flmatches); // set focus to result list // select one file, as matches is non-zero, there should be at least one entry if (!flmatches->get_focused_file()) { flmatches->select_row(0); } } } data->update_gui_timeout_id = 0; return FALSE; // returning FALSE here stops the timeout callbacks } #pragma GCC diagnostic pop /** * This function gets called then the search-dialog is about the be destroyed. * The function waits for the last search-thread to finish and then frees the * data structure that has been shared between the search threads and the * main thread. */ gboolean GnomeCmdSearchDialog::Private::join_thread_func (GnomeCmdSearchDialog::Private *priv) { SearchData &data = priv->data; if (data.thread) g_thread_join (data.thread); if (data.cancellable) g_object_unref (data.cancellable); // unref all directories which contained matching files from last search g_mutex_lock (&data.pdata.mutex); if (data.match_dirs) { g_list_foreach (data.match_dirs, (GFunc) gnome_cmd_dir_unref, nullptr); g_list_free (data.match_dirs); data.match_dirs = nullptr; } g_mutex_unlock (&data.pdata.mutex); g_mutex_clear (&data.pdata.mutex); delete priv; return FALSE; } gboolean SearchData::StartGenericSearch() { // create an regex for file name matching name_filter = new Filter(dialog->defaults.default_profile.filename_pattern.c_str(), dialog->defaults.default_profile.match_case, dialog->defaults.default_profile.syntax); // if we're going to search through file content create an regex for that too if (dialog->defaults.default_profile.content_search) { content_regex = g_new0 (regex_t, 1); regcomp (content_regex, dialog->defaults.default_profile.text_pattern.c_str(), dialog->defaults.default_profile.match_case ? 0 : REG_ICASE); } cancellable = g_cancellable_new(); gnome_cmd_dir_ref (start_dir); thread = g_thread_new (nullptr, (GThreadFunc) perform_search_operation, this); return TRUE; } /** * local search - using findutils */ gchar *SearchData::BuildSearchCommand() { gchar *file_pattern_utf8 = g_strdup (dialog->defaults.default_profile.filename_pattern.c_str()); GError *error = nullptr; switch (dialog->defaults.default_profile.syntax) { case Filter::TYPE_FNMATCH: if (!file_pattern_utf8 || !*file_pattern_utf8) { g_free (file_pattern_utf8); file_pattern_utf8 = g_strdup ("*"); } else if (!g_utf8_strchr (file_pattern_utf8, -1, '*') && !g_utf8_strchr (file_pattern_utf8, -1, '?')) { gchar *tmp = file_pattern_utf8; file_pattern_utf8 = g_strconcat ("*", file_pattern_utf8, "*", nullptr); g_free (tmp); } break; case Filter::TYPE_REGEX: if (gchar *tmp = file_pattern_utf8) { file_pattern_utf8 = g_strconcat (".*/.*", file_pattern_utf8, ".*", nullptr); g_free (tmp); } break; default: break; } gchar *file_pattern_locale = g_locale_from_utf8 (file_pattern_utf8, -1, nullptr, nullptr, &error); if (!file_pattern_locale) { gnome_cmd_error_message (file_pattern_utf8, error); g_free (file_pattern_utf8); return nullptr; } gchar *text_pattern_quoted = nullptr; if (dialog->defaults.default_profile.content_search) { const gchar *text_pattern_utf8 = dialog->defaults.default_profile.text_pattern.c_str(); gchar *text_pattern_locale = g_locale_from_utf8 (text_pattern_utf8, -1, nullptr, nullptr, &error); if (!text_pattern_locale) { gnome_cmd_error_message (text_pattern_utf8, error); g_free (file_pattern_utf8); g_free (file_pattern_locale); return nullptr; } text_pattern_quoted = quote_if_needed (text_pattern_locale); g_free (text_pattern_locale); } gchar *file_pattern_quoted = quote_if_needed (file_pattern_locale); gchar *look_in_folder_utf8 = GNOME_CMD_FILE (start_dir)->get_real_path(); gchar *look_in_folder_locale = g_locale_from_utf8 (look_in_folder_utf8, -1, nullptr, nullptr, nullptr); if (!look_in_folder_locale) // if for some reason a path was not returned, fallback to the user's home directory look_in_folder_locale = g_strconcat (g_get_home_dir (), G_DIR_SEPARATOR_S, nullptr); gchar *look_in_folder_quoted = quote_if_needed (look_in_folder_locale); GString *command = g_string_sized_new (512); g_string_append (command, "find "); g_string_append (command, look_in_folder_quoted); g_string_append (command, " -mindepth 1"); // exclude the directory itself if (dialog->defaults.default_profile.max_depth!=-1) g_string_append_printf (command, " -maxdepth %i", dialog->defaults.default_profile.max_depth+1); switch (dialog->defaults.default_profile.syntax) { case Filter::TYPE_FNMATCH: g_string_append_printf (command, " -iname %s", file_pattern_quoted); break; case Filter::TYPE_REGEX: g_string_append_printf (command, " -regextype posix-extended -iregex %s", file_pattern_quoted); break; default: ; } if (dialog->defaults.default_profile.content_search) { static const gchar GREP_COMMAND[] = "grep"; if (dialog->defaults.default_profile.match_case) g_string_append_printf (command, " '!' -type p -exec %s -E -q %s {} \\;", GREP_COMMAND, text_pattern_quoted); else g_string_append_printf (command, " '!' -type p -exec %s -E -q -i %s {} \\;", GREP_COMMAND, text_pattern_quoted); g_free (text_pattern_quoted); } g_string_append (command, " -print"); g_free (file_pattern_utf8); g_free (file_pattern_locale); g_free (file_pattern_quoted); g_free (look_in_folder_utf8); g_free (look_in_folder_locale); g_free (look_in_folder_quoted); return g_string_free (command, FALSE); } static void child_command_set_pgid_cb (gpointer unused) { if (setpgid (0, 0) < 0) g_print (_("Failed to set process group id of child %d: %s.\n"), getpid (), g_strerror (errno)); } static gboolean handle_search_command_stdout_io (GIOChannel *ioc, GIOCondition condition, SearchData *data) { gboolean broken_pipe = FALSE; if (condition & G_IO_IN) { GError *error = nullptr; GString *string = g_string_new (nullptr); GTimer *timer = g_timer_new (); g_timer_start (timer); while (!ioc->is_readable); do { gint status; if (data->stopped) { broken_pipe = TRUE; break; } do { status = g_io_channel_read_line_string (ioc, string, nullptr, &error); if (status == G_IO_STATUS_EOF) broken_pipe = TRUE; else if (status == G_IO_STATUS_AGAIN) while (gtk_events_pending ()) { if (data->stopped) { broken_pipe = TRUE; break; } gtk_main_iteration (); } } while (status == G_IO_STATUS_AGAIN && !broken_pipe); if (broken_pipe) break; if (status != G_IO_STATUS_NORMAL) { if (error) { g_warning ("handle_search_command_stdout_io(): %s", error->message); g_error_free (error); } continue; } string = g_string_truncate (string, string->len - 1); if (string->len <= 1) continue; gchar *utf8 = g_filename_display_name (string->str); GnomeCmdFile *f = gnome_cmd_file_new (utf8); if (f) data->dialog->priv->result_list->append_file(f); g_free (utf8); gulong duration; g_timer_elapsed (timer, &duration); if (duration > GNOME_SEARCH_TOOL_REFRESH_DURATION) { while (gtk_events_pending ()) { if (data->stopped) { broken_pipe = TRUE; break; } gtk_main_iteration (); } if (broken_pipe) break; g_timer_reset (timer); } } while (g_io_channel_get_buffer_condition (ioc) & G_IO_IN); g_string_free (string, TRUE); g_timer_destroy (timer); } if (!(condition & G_IO_IN) || broken_pipe) { g_io_channel_shutdown (ioc, TRUE, nullptr); g_spawn_close_pid(data->pid); data->pid = 0; data->search_done = TRUE; return FALSE; } return TRUE; } gboolean SearchData::StartLocalSearch() { gchar *command = BuildSearchCommand(); g_return_val_if_fail (command!=nullptr, FALSE); DEBUG ('g', "running: %s\n", command); GError *error = nullptr; gchar **argv = nullptr; gint child_stdout; if (!g_shell_parse_argv (command, nullptr, &argv, &error)) { gnome_cmd_error_message (_("Error parsing the search command."), error); g_free (command); g_strfreev (argv); return FALSE; } g_free (command); if (!g_spawn_async_with_pipes (nullptr, argv, nullptr, GSpawnFlags (G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL), child_command_set_pgid_cb, nullptr, &pid, nullptr, &child_stdout, nullptr, &error)) { gnome_cmd_error_message (_("Error running the search command."), error); g_strfreev (argv); return FALSE; } #ifdef G_OS_WIN32 GIOChannel *ioc_stdout = g_io_channel_win32_new_fd (child_stdout); #else GIOChannel *ioc_stdout = g_io_channel_unix_new (child_stdout); #endif g_io_channel_set_encoding (ioc_stdout, nullptr, nullptr); g_io_channel_set_flags (ioc_stdout, G_IO_FLAG_NONBLOCK, nullptr); g_io_add_watch (ioc_stdout, GIOCondition (G_IO_IN | G_IO_HUP), (GIOFunc) handle_search_command_stdout_io, this); g_io_channel_unref (ioc_stdout); g_strfreev (argv); return TRUE; } gboolean GnomeCmdSearchDialog::Private::on_list_keypressed(GtkWidget *result_list, GdkEventKey *event, gpointer unused) { if (GNOME_CMD_FILE_LIST (result_list)->key_pressed(event) || handle_list_keypress (GNOME_CMD_FILE_LIST (result_list), event)) { g_signal_stop_emission_by_name (result_list, "key-press-event"); return TRUE; } return FALSE; } void GnomeCmdSearchDialog::Private::on_dialog_show(GtkWidget *widget, GnomeCmdSearchDialog *dialog) { dialog->priv->profile_component->update(); dialog->priv->data.start_dir = main_win->fs(ACTIVE)->get_directory(); gchar *uri = gnome_cmd_dir_get_uri_str (dialog->priv->data.start_dir); gtk_file_chooser_set_current_folder_uri (GTK_FILE_CHOOSER (dialog->priv->dir_browser), uri); g_free (uri); } void GnomeCmdSearchDialog::Private::on_dialog_hide(GtkWidget *widget, GnomeCmdSearchDialog *dialog) { dialog->priv->profile_component->copy(); dialog->priv->result_list->remove_all_files(); } gboolean GnomeCmdSearchDialog::Private::on_dialog_delete(GtkWidget *widget, GdkEvent *event, GnomeCmdSearchDialog *dialog) { return event->type==GDK_DELETE; } void GnomeCmdSearchDialog::Private::on_dialog_size_allocate(GtkWidget *widget, GtkAllocation *allocation, GnomeCmdSearchDialog *dialog) { dialog->defaults.width = allocation->width; dialog->defaults.height = allocation->height; } void GnomeCmdSearchDialog::Private::on_dialog_response(GtkDialog *window, int response_id, GnomeCmdSearchDialog *dialog) { switch (response_id) { case GCMD_RESPONSE_STOP: { dialog->priv->data.stopped = TRUE; gtk_dialog_set_response_sensitive (*dialog, GCMD_RESPONSE_STOP, FALSE); gtk_dialog_set_default_response (*dialog, GCMD_RESPONSE_FIND); if (dialog->priv->data.cancellable) g_cancellable_cancel (dialog->priv->data.cancellable); #ifdef G_OS_UNIX if (dialog->priv->data.pid) kill (dialog->priv->data.pid, SIGTERM); #endif } break; case GCMD_RESPONSE_FIND: { SearchData &data = dialog->priv->data; data.search_done = TRUE; data.stopped = TRUE; data.dialog_destroyed = FALSE; if (data.cancellable) g_cancellable_cancel (data.cancellable); if (data.thread) { g_thread_join (data.thread); data.thread = nullptr; } if (data.cancellable) { g_object_unref (data.cancellable); data.cancellable = nullptr; } data.context_id = gtk_statusbar_get_context_id (GTK_STATUSBAR (dialog->priv->statusbar), "info"); data.content_regex = nullptr; data.match_dirs = nullptr; auto uriString = gtk_file_chooser_get_uri (GTK_FILE_CHOOSER (dialog->priv->dir_browser)); auto gUri = g_uri_parse (uriString, G_URI_FLAGS_NONE, nullptr); g_free (uriString); auto dirPathString = g_uri_get_path (gUri); GnomeCmdCon *con = gnome_cmd_dir_get_connection (data.start_dir); if (strncmp(dirPathString, gnome_cmd_con_get_root_path (con), con->root_path->len) != 0) { if (!g_strcmp0(g_uri_get_scheme (gUri), "file") != 0) { g_uri_unref (gUri); gnome_cmd_show_message (*dialog, stringify(g_strdup_printf (_("Failed to change directory outside of %s"), gnome_cmd_con_get_root_path (con)))); break; } else data.start_dir = gnome_cmd_dir_new (get_home_con (), gnome_cmd_con_create_path (get_home_con (), dirPathString)); } else data.start_dir = gnome_cmd_dir_new (con, gnome_cmd_con_create_path (con, dirPathString + con->root_path->len)); g_uri_unref (gUri); gnome_cmd_dir_ref (data.start_dir); // save default settings dialog->priv->profile_component->copy(); if (!dialog->defaults.default_profile.filename_pattern.empty()) { gnome_cmd_data.search_defaults.name_patterns.add(dialog->defaults.default_profile.filename_pattern.c_str()); dialog->priv->profile_component->set_name_patterns_history(dialog->defaults.name_patterns.ents); } if (dialog->defaults.default_profile.content_search && !dialog->defaults.default_profile.text_pattern.empty()) { gnome_cmd_data.search_defaults.content_patterns.add(dialog->defaults.default_profile.text_pattern.c_str()); gnome_cmd_data.intviewer_defaults.text_patterns.add(dialog->defaults.default_profile.text_pattern.c_str()); dialog->priv->profile_component->set_content_patterns_history(dialog->defaults.content_patterns.ents); } data.search_done = FALSE; data.stopped = FALSE; dialog->priv->result_list->remove_all_files(); gchar *base_dir_utf8 = GNOME_CMD_FILE (data.start_dir)->get_real_path(); dialog->priv->result_list->set_base_dir(base_dir_utf8); if (gnome_cmd_con_is_local (con) ? data.StartLocalSearch() : data.StartGenericSearch()) { data.set_statusmsg(); gtk_widget_show (dialog->priv->pbar); data.update_gui_timeout_id = g_timeout_add (gnome_cmd_data.gui_update_rate, (GSourceFunc) update_search_status_widgets, &data); gtk_dialog_set_response_sensitive (*dialog, GCMD_RESPONSE_GOTO, FALSE); gtk_dialog_set_response_sensitive (*dialog, GCMD_RESPONSE_STOP, TRUE); gtk_dialog_set_response_sensitive (*dialog, GCMD_RESPONSE_FIND, FALSE); gtk_dialog_set_default_response (*dialog, GCMD_RESPONSE_STOP); } gnome_cmd_dir_unref (data.start_dir); } break; case GCMD_RESPONSE_GOTO: { GnomeCmdFile *f = dialog->priv->result_list->get_selected_file(); if (!f) break; GnomeCmdFileSelector *fs = main_win->fs(ACTIVE); GnomeCmdCon *con = fs->get_connection(); gchar *fpath = f->GetPathStringThroughParent(); gsize offset = strncmp(fpath, gnome_cmd_con_get_root_path (con), con->root_path->len)==0 ? con->root_path->len : 0; gchar *dpath = g_path_get_dirname (fpath + offset); if (fs->file_list()->locked) { GnomeCmdDir *dir = gnome_cmd_dir_new (con, gnome_cmd_con_create_path (con, dpath)); fs->new_tab(dir); } else fs->file_list()->goto_directory(dpath); fs->file_list()->focus_file(f->get_name(), TRUE); g_free (fpath); g_free (dpath); } #if defined (__GNUC__) && __GNUC__ >= 7 __attribute__ ((fallthrough)); #endif case GTK_RESPONSE_NONE: case GTK_RESPONSE_DELETE_EVENT: case GTK_RESPONSE_CANCEL: case GTK_RESPONSE_CLOSE: gtk_widget_hide (*dialog); g_signal_stop_emission_by_name (dialog, "response"); break; case GTK_RESPONSE_HELP: gnome_cmd_help_display ("gnome-commander.xml", "gnome-commander-search"); g_signal_stop_emission_by_name (dialog, "response"); break; case GCMD_RESPONSE_PROFILES: break; default : g_assert_not_reached (); } } static void gnome_cmd_search_dialog_init (GnomeCmdSearchDialog *dialog) { dialog->priv = new GnomeCmdSearchDialog::Private(dialog); gtk_window_set_title (*dialog, _("Search…")); gtk_window_set_resizable (*dialog, TRUE); gtk_dialog_set_has_separator (*dialog, FALSE); gtk_container_set_border_width (GTK_CONTAINER (dialog), 5); gtk_box_set_spacing (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), 2); dialog->priv->vbox = gtk_vbox_new (FALSE, 6); gtk_container_set_border_width (GTK_CONTAINER (dialog->priv->vbox), 5); gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), dialog->priv->vbox, TRUE, TRUE, 0); // file list GtkWidget *sw = gtk_scrolled_window_new (nullptr, nullptr); gtk_box_pack_start (GTK_BOX (dialog->priv->vbox), sw, TRUE, TRUE, 0); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); dialog->priv->result_list = new GnomeCmdFileList(GnomeCmdFileList::COLUMN_NAME,GTK_SORT_ASCENDING); gtk_widget_set_size_request (*dialog->priv->result_list, -1, 200); gtk_container_add (GTK_CONTAINER (sw), *dialog->priv->result_list); gtk_container_set_border_width (GTK_CONTAINER (dialog->priv->result_list), 4); // status dialog->priv->statusbar = gtk_statusbar_new (); gtk_statusbar_set_has_resize_grip (GTK_STATUSBAR (dialog->priv->statusbar), FALSE); gtk_box_pack_start (GTK_BOX (dialog->priv->vbox), dialog->priv->statusbar, FALSE, TRUE, 0); // progress dialog->priv->pbar = create_progress_bar (*dialog); gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR (dialog->priv->pbar), FALSE); gtk_progress_bar_set_pulse_step (GTK_PROGRESS_BAR (dialog->priv->pbar), 1.0 / (gdouble) PBAR_MAX); gtk_box_pack_start (GTK_BOX (dialog->priv->statusbar),dialog->priv-> pbar, FALSE, TRUE, 0); dialog->priv->result_list->update_style(); gtk_widget_show (dialog->priv->vbox); gtk_widget_show_all (sw); gtk_widget_show (dialog->priv->statusbar); gtk_widget_hide (dialog->priv->pbar); g_mutex_init(&dialog->priv->data.pdata.mutex); } static void gnome_cmd_search_dialog_finalize (GObject *object) { GnomeCmdSearchDialog *dialog = GNOME_CMD_SEARCH_DIALOG (object); SearchData &data = dialog->priv->data; if (data.update_gui_timeout_id) g_source_remove (data.update_gui_timeout_id); #ifdef G_OS_UNIX if (data.pid) kill (data.pid, SIGTERM); #endif // stop and wait for search thread to exit data.stopped = TRUE; data.dialog_destroyed = TRUE; if (data.cancellable) g_cancellable_cancel (data.cancellable); g_timeout_add (1, (GSourceFunc) GnomeCmdSearchDialog::Private::join_thread_func, dialog->priv); G_OBJECT_CLASS (gnome_cmd_search_dialog_parent_class)->finalize (object); } static void gnome_cmd_search_dialog_class_init (GnomeCmdSearchDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = gnome_cmd_search_dialog_finalize; } void GnomeCmdSearchDialog::show_and_set_focus() { gtk_widget_show(*this); priv->profile_component->set_focus(); } void GnomeCmdSearchDialog::update_style() { priv->result_list->update_style(); } GnomeCmdSearchDialog::GnomeCmdSearchDialog(GnomeCmdData::SearchConfig &cfg): defaults(cfg) { gtk_window_set_default_size (*this, defaults.width, defaults.height); if (gnome_cmd_data.options.search_window_is_transient) { gtk_window_set_transient_for (*this, *main_win); } else { gtk_window_set_type_hint (*this, GDK_WINDOW_TYPE_HINT_NORMAL); } GtkWidget *button = priv->create_button_with_menu(_("Profiles…"), cfg); gtk_dialog_add_action_widget (*this, button, GCMD_RESPONSE_PROFILES); gtk_dialog_add_buttons (*this, GTK_STOCK_HELP, GTK_RESPONSE_HELP, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, GTK_STOCK_JUMP_TO, GCMD_RESPONSE_GOTO, GTK_STOCK_STOP, GCMD_RESPONSE_STOP, GTK_STOCK_FIND, GCMD_RESPONSE_FIND, nullptr); gtk_dialog_set_response_sensitive (*this, GCMD_RESPONSE_GOTO, FALSE); gtk_dialog_set_response_sensitive (*this, GCMD_RESPONSE_STOP, FALSE); gtk_dialog_set_default_response (*this, GCMD_RESPONSE_FIND); // search in priv->dir_browser = gtk_file_chooser_button_new (_("Select Directory"), GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER); gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (priv->dir_browser), FALSE); priv->profile_component = new GnomeCmdSelectionProfileComponent(cfg.default_profile, priv->dir_browser, _("_Look in folder:")); gtk_box_pack_start (GTK_BOX (priv->vbox), *priv->profile_component, FALSE, FALSE, 0); gtk_box_reorder_child (GTK_BOX (priv->vbox), *priv->profile_component, 0); if (!defaults.name_patterns.empty()) priv->profile_component->set_name_patterns_history(defaults.name_patterns.ents); priv->profile_component->set_content_patterns_history(defaults.content_patterns.ents); priv->profile_component->set_default_activation(*this); gtk_widget_show_all (button); gtk_widget_show (*priv->profile_component); g_signal_connect (priv->result_list, "key-press-event", G_CALLBACK (Private::on_list_keypressed), this); g_signal_connect (this, "show", G_CALLBACK (Private::on_dialog_show), this); g_signal_connect (this, "hide", G_CALLBACK (Private::on_dialog_hide), this); g_signal_connect (this, "delete-event", G_CALLBACK (Private::on_dialog_delete), this); g_signal_connect (this, "size-allocate", G_CALLBACK (Private::on_dialog_size_allocate), this); g_signal_connect (this, "response", G_CALLBACK (Private::on_dialog_response), this); } GnomeCmdSearchDialog::~GnomeCmdSearchDialog() { }