diff options
Diffstat (limited to 'gtk/gtkfilesel.c')
-rw-r--r-- | gtk/gtkfilesel.c | 2161 |
1 files changed, 2161 insertions, 0 deletions
diff --git a/gtk/gtkfilesel.c b/gtk/gtkfilesel.c new file mode 100644 index 0000000000..77f83a87a3 --- /dev/null +++ b/gtk/gtkfilesel.c @@ -0,0 +1,2161 @@ +/* GTK - The GIMP Toolkit + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the Free + * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ +#include <stdio.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/param.h> +#include <dirent.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <pwd.h> +#include "fnmatch.h" + +#include "gdk/gdkkeysyms.h" +#include "gtkbutton.h" +#include "gtkentry.h" +#include "gtkfilesel.h" +#include "gtkhbox.h" +#include "gtklabel.h" +#include "gtklist.h" +#include "gtklistitem.h" +#include "gtkmain.h" +#include "gtkscrolledwindow.h" +#include "gtksignal.h" +#include "gtkvbox.h" + + +#define DIR_LIST_WIDTH 160 +#define DIR_LIST_HEIGHT 175 +#define FILE_LIST_WIDTH 160 +#define FILE_LIST_HEIGHT 175 + + +typedef struct _CompletionState CompletionState; +typedef struct _CompletionDir CompletionDir; +typedef struct _CompletionDirSent CompletionDirSent; +typedef struct _CompletionDirEntry CompletionDirEntry; +typedef struct _CompletionUserDir CompletionUserDir; +typedef struct _PossibleCompletion PossibleCompletion; + +/* Non-external file completion decls and structures */ + +/* A contant telling PRCS how many directories to cache. Its actually + * kept in a list, so the geometry isn't important. */ +#define CMPL_DIRECTORY_CACHE_SIZE 10 + +/* A constant used to determine whether a substring was an exact + * match by first_diff_index() + */ +#define PATTERN_MATCH -1 +/* The arguments used by all fnmatch() calls below + */ +#define FNMATCH_FLAGS (FNM_PATHNAME | FNM_PERIOD) + +#define CMPL_ERRNO_TOO_LONG ((1<<16)-1) + +/* This structure contains all the useful information about a directory + * for the purposes of filename completion. These structures are cached + * in the CompletionState struct. CompletionDir's are reference counted. + */ +struct _CompletionDirSent +{ + ino_t inode; + time_t mtime; + + gint entry_count; + gchar *name_buffer; /* memory segment containing names of all entries */ + + struct _CompletionDirEntry *entries; +}; + +struct _CompletionDir +{ + CompletionDirSent *sent; + + gchar *fullname; + gint fullname_len; + + struct _CompletionDir *cmpl_parent; + gint cmpl_index; + gchar *cmpl_text; +}; + +/* This structure contains pairs of directory entry names with a flag saying + * whether or not they are a valid directory. NOTE: This information is used + * to provide the caller with information about whether to update its completions + * or try to open a file. Since directories are cached by the directory mtime, + * a symlink which points to an invalid file (which will not be a directory), + * will not be reevaluated if that file is created, unless the containing + * directory is touched. I consider this case to be worth ignoring (josh). + */ +struct _CompletionDirEntry +{ + gint is_dir; + gchar *entry_name; +}; + +struct _CompletionUserDir +{ + gchar *login; + gchar *homedir; +}; + +struct _PossibleCompletion +{ + /* accessible fields, all are accessed externally by functions + * declared above + */ + gchar *text; + gint is_a_completion; + gint is_directory; + + /* Private fields + */ + gint text_alloc; +}; + +struct _CompletionState +{ + gint last_valid_char; + gchar *updated_text; + gint updated_text_len; + gint updated_text_alloc; + gint re_complete; + + gchar *user_dir_name_buffer; + gint user_directories_len; + gchar *user_home_dir; + + gchar *last_completion_text; + + gint user_completion_index; /* if >= 0, currently completing ~user */ + + struct _CompletionDir *completion_dir; /* directory completing from */ + struct _CompletionDir *active_completion_dir; + + struct _PossibleCompletion the_completion; + + struct _CompletionDir *reference_dir; /* initial directory */ + + GList* directory_storage; + GList* directory_sent_storage; + + struct _CompletionUserDir *user_directories; +}; + + +/* File completion functions which would be external, were they used + * outside of this file. + */ + +static CompletionState* cmpl_init_state (void); +static void cmpl_free_state (CompletionState *cmpl_state); +static gint cmpl_state_okay (CompletionState* cmpl_state); +static gchar* cmpl_strerror (gint); + +static PossibleCompletion* cmpl_completion_matches(gchar *text_to_complete, + gchar **remaining_text, + CompletionState *cmpl_state); + +/* Returns a name for consideration, possibly a completion, this name + * will be invalid after the next call to cmpl_next_completion. + */ +static char* cmpl_this_completion (PossibleCompletion*); + +/* True if this completion matches the given text. Otherwise, this + * output can be used to have a list of non-completions. + */ +static gint cmpl_is_a_completion (PossibleCompletion*); + +/* True if the completion is a directory + */ +static gint cmpl_is_directory (PossibleCompletion*); + +/* Obtains the next completion, or NULL + */ +static PossibleCompletion* cmpl_next_completion (CompletionState*); + +/* Updating completions: the return value of cmpl_updated_text() will + * be text_to_complete completed as much as possible after the most + * recent call to cmpl_completion_matches. For the present + * application, this is the suggested replacement for the user's input + * string. You must CALL THIS AFTER ALL cmpl_text_completions have + * been received. + */ +static gchar* cmpl_updated_text (CompletionState* cmpl_state); + +/* After updating, to see if the completion was a directory, call + * this. If it was, you should consider re-calling completion_matches. + */ +static gint cmpl_updated_dir (CompletionState* cmpl_state); + +/* Current location: if using file completion, return the current + * directory, from which file completion begins. More specifically, + * the cwd concatenated with all exact completions up to the last + * directory delimiter('/'). + */ +static gchar* cmpl_reference_position (CompletionState* cmpl_state); + +/* backing up: if cmpl_completion_matches returns NULL, you may query + * the index of the last completable character into cmpl_updated_text. + */ +static gint cmpl_last_valid_char (CompletionState* cmpl_state); + +/* When the user selects a non-directory, call cmpl_completion_fullname + * to get the full name of the selected file. + */ +static gchar* cmpl_completion_fullname (gchar*, CompletionState* cmpl_state); + + +/* Directory operations. */ +static CompletionDir* open_ref_dir (gchar* text_to_complete, + gchar** remaining_text, + CompletionState* cmpl_state); +static CompletionDir* open_dir (gchar* dir_name, + CompletionState* cmpl_state); +static CompletionDir* open_user_dir (gchar* text_to_complete, + CompletionState *cmpl_state); +static CompletionDir* open_relative_dir (gchar* dir_name, CompletionDir* dir, + CompletionState *cmpl_state); +static CompletionDirSent* open_new_dir (gchar* dir_name, struct stat* sbuf); +static gint correct_dir_fullname (CompletionDir* cmpl_dir); +static gint correct_parent (CompletionDir* cmpl_dir, + struct stat *sbuf); +static gchar* find_parent_dir_fullname (gchar* dirname); +static CompletionDir* attach_dir (CompletionDirSent* sent, + gchar* dir_name, + CompletionState *cmpl_state); +static void free_dir_sent (CompletionDirSent* sent); +static void free_dir (CompletionDir *dir); +static void prune_memory_usage(CompletionState *cmpl_state); + +/* Completion operations */ +static PossibleCompletion* attempt_homedir_completion(gchar* text_to_complete, + CompletionState *cmpl_state); +static PossibleCompletion* attempt_file_completion(CompletionState *cmpl_state); +static CompletionDir* find_completion_dir(gchar* text_to_complete, + gchar** remaining_text, + CompletionState* cmpl_state); +static PossibleCompletion* append_completion_text(gchar* text, + CompletionState* cmpl_state); +static gint get_pwdb(CompletionState* cmpl_state); +static gint first_diff_index(gchar* pat, gchar* text); +static gint compare_user_dir(const void* a, const void* b); +static gint compare_cmpl_dir(const void* a, const void* b); +static void update_cmpl(PossibleCompletion* poss, + CompletionState* cmpl_state); + +static void gtk_file_selection_class_init (GtkFileSelectionClass *klass); +static void gtk_file_selection_init (GtkFileSelection *filesel); +static void gtk_file_selection_destroy (GtkObject *object); +static gint gtk_file_selection_key_press (GtkWidget *widget, + GdkEventKey *event, + gpointer user_data); +static gint gtk_file_selection_file_button (GtkWidget *widget, + GdkEventButton *event, + gpointer user_data); +static gint gtk_file_selection_dir_button (GtkWidget *widget, + GdkEventButton *event, + gpointer user_data); +static void gtk_file_selection_file_list_changed (GtkList *gtklist, + gpointer func_data); +static void gtk_file_selection_dir_list_changed (GtkList *gtklist, + gpointer func_data); +static void gtk_file_selection_populate (GtkFileSelection *fs, + gchar *rel_path, + gint try_complete); +static void gtk_file_selection_abort (GtkFileSelection *fs); +static void gtk_file_selection_free_filename (GtkWidget *widget, + gpointer client_data); + + +static GtkWindowClass *parent_class = NULL; + +static gchar *list_changed_key = "_gtk_selection_changed_handler_key"; + +/* Saves errno when something cmpl does fails. */ +static gint cmpl_errno; + + +guint +gtk_file_selection_get_type () +{ + static guint file_selection_type = 0; + + if (!file_selection_type) + { + GtkTypeInfo filesel_info = + { + "GtkFileSelection", + sizeof (GtkFileSelection), + sizeof (GtkFileSelectionClass), + (GtkClassInitFunc) gtk_file_selection_class_init, + (GtkObjectInitFunc) gtk_file_selection_init, + (GtkArgFunc) NULL, + }; + + file_selection_type = gtk_type_unique (gtk_window_get_type (), &filesel_info); + } + + return file_selection_type; +} + +static void +gtk_file_selection_class_init (GtkFileSelectionClass *class) +{ + GtkObjectClass *object_class; + + object_class = (GtkObjectClass*) class; + + parent_class = gtk_type_class (gtk_window_get_type ()); + + object_class->destroy = gtk_file_selection_destroy; +} + +static void +gtk_file_selection_init (GtkFileSelection *filesel) +{ + GtkWidget *dir_vbox; + GtkWidget *file_vbox; + GtkWidget *entry_vbox; + GtkWidget *listbox; + GtkWidget *label; + GtkWidget *list_hbox; + GtkWidget *action_area; + gint key; + + filesel->cmpl_state = cmpl_init_state (); + + /* The dialog-sized vertical box */ + filesel->main_vbox = gtk_vbox_new (FALSE, 10); + gtk_container_border_width (GTK_CONTAINER (filesel), 10); + gtk_container_add (GTK_CONTAINER (filesel), filesel->main_vbox); + gtk_widget_show (filesel->main_vbox); + + /* The horizontal box containing the directory and file listboxes */ + list_hbox = gtk_hbox_new (TRUE, 5); + gtk_box_pack_start (GTK_BOX (filesel->main_vbox), list_hbox, TRUE, TRUE, 0); + gtk_widget_show (list_hbox); + + + /* The directories listbox */ + dir_vbox = gtk_vbox_new (FALSE, 2); + gtk_box_pack_start (GTK_BOX (list_hbox), dir_vbox, TRUE, TRUE, 0); + gtk_widget_show (dir_vbox); + + label = gtk_label_new ("Directories"); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (dir_vbox), label, FALSE, FALSE, 0); + gtk_widget_show (label); + + listbox = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (listbox), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_ALWAYS); + gtk_box_pack_start (GTK_BOX (dir_vbox), listbox, TRUE, TRUE, 0); + gtk_widget_set_usize (listbox, DIR_LIST_WIDTH, DIR_LIST_HEIGHT); + gtk_widget_show (listbox); + + filesel->dir_list = gtk_list_new (); + gtk_list_set_selection_mode (GTK_LIST (filesel->dir_list), GTK_SELECTION_BROWSE); + gtk_signal_connect (GTK_OBJECT (filesel->dir_list), "button_press_event", + (GtkSignalFunc) gtk_file_selection_dir_button, filesel); + key = gtk_signal_connect (GTK_OBJECT (filesel->dir_list), + "selection_changed", + (GtkSignalFunc) gtk_file_selection_dir_list_changed, + filesel); + gtk_object_set_data (GTK_OBJECT (filesel->dir_list), list_changed_key, (gpointer) key); + gtk_container_add (GTK_CONTAINER (listbox), filesel->dir_list); + gtk_widget_show (filesel->dir_list); + + + /* The files listbox */ + file_vbox = gtk_vbox_new (FALSE, 2); + gtk_box_pack_start (GTK_BOX (list_hbox), file_vbox, TRUE, TRUE, 0); + gtk_widget_show (file_vbox); + + label = gtk_label_new ("Files"); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (file_vbox), label, FALSE, FALSE, 0); + gtk_widget_show (label); + + listbox = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (listbox), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_ALWAYS); + gtk_box_pack_start (GTK_BOX (file_vbox), listbox, TRUE, TRUE, 0); + gtk_widget_set_usize (listbox, FILE_LIST_WIDTH, FILE_LIST_HEIGHT); + gtk_widget_show (listbox); + + filesel->file_list = gtk_list_new (); + gtk_list_set_selection_mode (GTK_LIST (filesel->file_list), GTK_SELECTION_BROWSE); + gtk_signal_connect (GTK_OBJECT (filesel->file_list), "button_press_event", + (GtkSignalFunc) gtk_file_selection_file_button, filesel); + key = gtk_signal_connect (GTK_OBJECT (filesel->file_list), + "selection_changed", + (GtkSignalFunc) gtk_file_selection_file_list_changed, + filesel); + gtk_object_set_data (GTK_OBJECT (filesel->file_list), list_changed_key, (gpointer) key); + gtk_container_add (GTK_CONTAINER (listbox), filesel->file_list); + gtk_widget_show (filesel->file_list); + + + /* The action area */ + action_area = gtk_hbox_new (TRUE, 10); + gtk_box_pack_end (GTK_BOX (filesel->main_vbox), action_area, FALSE, FALSE, 0); + gtk_widget_show (action_area); + + /* The OK button */ + filesel->ok_button = gtk_button_new_with_label ("OK"); + GTK_WIDGET_SET_FLAGS (filesel->ok_button, GTK_CAN_DEFAULT); + gtk_box_pack_start (GTK_BOX (action_area), filesel->ok_button, TRUE, TRUE, 0); + gtk_widget_grab_default (filesel->ok_button); + gtk_widget_show (filesel->ok_button); + + /* The Cancel button */ + filesel->cancel_button = gtk_button_new_with_label ("Cancel"); + GTK_WIDGET_SET_FLAGS (filesel->cancel_button, GTK_CAN_DEFAULT); + gtk_box_pack_start (GTK_BOX (action_area), filesel->cancel_button, TRUE, TRUE, 0); + gtk_widget_show (filesel->cancel_button); + + /* The Help button */ + filesel->help_button = gtk_button_new_with_label ("Help"); + GTK_WIDGET_SET_FLAGS (filesel->help_button, GTK_CAN_DEFAULT); + gtk_box_pack_start (GTK_BOX (action_area), filesel->help_button, TRUE, TRUE, 0); + gtk_widget_show (filesel->help_button); + + + /* The selection entry widget */ + entry_vbox = gtk_vbox_new (FALSE, 2); + gtk_box_pack_end (GTK_BOX (filesel->main_vbox), entry_vbox, FALSE, FALSE, 0); + gtk_widget_show (entry_vbox); + + filesel->selection_text = label = gtk_label_new (""); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (entry_vbox), label, FALSE, FALSE, 0); + gtk_widget_show (label); + + filesel->selection_entry = gtk_entry_new (); + gtk_signal_connect (GTK_OBJECT (filesel->selection_entry), "key_press_event", + (GtkSignalFunc) gtk_file_selection_key_press, filesel); + gtk_signal_connect_object (GTK_OBJECT (filesel->selection_entry), "focus_in_event", + (GtkSignalFunc) gtk_widget_grab_default, + GTK_OBJECT (filesel->ok_button)); + gtk_signal_connect_object (GTK_OBJECT (filesel->selection_entry), "activate", + (GtkSignalFunc) gtk_button_clicked, + GTK_OBJECT (filesel->ok_button)); + gtk_box_pack_start (GTK_BOX (entry_vbox), filesel->selection_entry, TRUE, TRUE, 0); + gtk_widget_show (filesel->selection_entry); + + if (!cmpl_state_okay (filesel->cmpl_state)) + { + gchar err_buf[256]; + + sprintf (err_buf, "Directory unreadable: %s", cmpl_strerror (cmpl_errno)); + + gtk_label_set (GTK_LABEL (filesel->selection_text), err_buf); + } + else + { + gtk_file_selection_populate (filesel, "", FALSE); + } + + gtk_widget_grab_focus (filesel->selection_entry); +} + +GtkWidget* +gtk_file_selection_new (const gchar *title) +{ + GtkFileSelection *filesel; + + filesel = gtk_type_new (gtk_file_selection_get_type ()); + gtk_window_set_title (GTK_WINDOW (filesel), title); + + return GTK_WIDGET (filesel); +} + +void +gtk_file_selection_set_filename (GtkFileSelection *filesel, + const gchar *filename) +{ + char buf[MAXPATHLEN]; + const char *name, *last_slash; + + g_return_if_fail (filesel != NULL); + g_return_if_fail (GTK_IS_FILE_SELECTION (filesel)); + g_return_if_fail (filename != NULL); + + last_slash = strrchr (filename, '/'); + + if (!last_slash) + { + buf[0] = 0; + name = filename; + } + else + { + gint len = MIN (MAXPATHLEN - 1, last_slash - filename + 1); + + strncpy (buf, filename, len); + buf[len] = 0; + + name = last_slash + 1; + } + + gtk_file_selection_populate (filesel, buf, FALSE); + + if (filesel->selection_entry) + gtk_entry_set_text (GTK_ENTRY (filesel->selection_entry), name); +} + +gchar* +gtk_file_selection_get_filename (GtkFileSelection *filesel) +{ + static char nothing[2] = ""; + char *text; + char *filename; + + g_return_val_if_fail (filesel != NULL, nothing); + g_return_val_if_fail (GTK_IS_FILE_SELECTION (filesel), nothing); + + text = gtk_entry_get_text (GTK_ENTRY (filesel->selection_entry)); + if (text) + { + filename = cmpl_completion_fullname (text, filesel->cmpl_state); + return filename; + } + + return nothing; +} + +static void +gtk_file_selection_destroy (GtkObject *object) +{ + GtkFileSelection *filesel; + + g_return_if_fail (object != NULL); + g_return_if_fail (GTK_IS_FILE_SELECTION (object)); + + filesel = GTK_FILE_SELECTION (object); + + cmpl_free_state (filesel->cmpl_state); + + if (GTK_OBJECT_CLASS (parent_class)->destroy) + (* GTK_OBJECT_CLASS (parent_class)->destroy) (object); +} + +static gint +gtk_file_selection_key_press (GtkWidget *widget, + GdkEventKey *event, + gpointer user_data) +{ + GtkFileSelection *fs; + char *text; + + g_return_val_if_fail (widget != NULL, FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + if (event->keyval == GDK_Tab) + { + gtk_signal_emit_stop_by_name (GTK_OBJECT (widget), "key_press_event"); + + fs = GTK_FILE_SELECTION (user_data); + text = gtk_entry_get_text (GTK_ENTRY (fs->selection_entry)); + gtk_file_selection_populate (fs, text, TRUE); + + return TRUE; + } + + return FALSE; +} + +static gint +gtk_file_selection_file_button (GtkWidget *widget, + GdkEventButton *event, + gpointer user_data) +{ + GtkFileSelection *fs; + GtkWidget *event_widget; + gchar *filename; + gboolean handled; + + g_return_val_if_fail (widget != NULL, FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + fs = GTK_FILE_SELECTION (user_data); + g_return_val_if_fail (fs != NULL, FALSE); + g_return_val_if_fail (GTK_IS_FILE_SELECTION (fs), FALSE); + + event_widget = gtk_get_event_widget ((GdkEvent*) event); + handled = FALSE; + if (GTK_IS_LIST_ITEM (event_widget)) + { + switch (event->type) + { + case GDK_BUTTON_PRESS: + filename = gtk_object_get_user_data (GTK_OBJECT (event_widget)); + gtk_widget_grab_focus (fs->selection_entry); + gtk_entry_set_text (GTK_ENTRY (fs->selection_entry), filename); + handled = TRUE; + break; + + case GDK_2BUTTON_PRESS: + gtk_button_clicked (GTK_BUTTON (fs->ok_button)); + handled = TRUE; + break; + + default: + break; + } + } + + return handled; +} + +static void +gtk_file_selection_file_list_changed (GtkList *list, + gpointer func_data) +{ + GtkFileSelection *fs; + + g_return_if_fail (func_data != NULL); + g_return_if_fail (GTK_IS_FILE_SELECTION (func_data)); + + fs = GTK_FILE_SELECTION (func_data); + + /* only act on an appropriate selection + */ + if (list->selection && list->selection->data) + { + GtkListItem *item; + + item = list->selection->data; + + if (GTK_IS_LIST_ITEM (item)) + { + gchar *filename; + + filename = gtk_object_get_user_data (GTK_OBJECT (item)); + gtk_entry_set_text (GTK_ENTRY (fs->selection_entry), filename); + } + } +} + +static gint +gtk_file_selection_dir_button (GtkWidget *widget, + GdkEventButton *event, + gpointer user_data) +{ + GtkFileSelection *fs; + GtkWidget *event_widget; + gchar *filename; + gboolean handled; + + g_return_val_if_fail (widget != NULL, FALSE); + g_return_val_if_fail (GTK_IS_LIST (widget), FALSE); + g_return_val_if_fail (event != NULL, FALSE); + + fs = GTK_FILE_SELECTION (user_data); + g_return_val_if_fail (fs != NULL, FALSE); + g_return_val_if_fail (GTK_IS_FILE_SELECTION (fs), FALSE); + + event_widget = gtk_get_event_widget ((GdkEvent*) event); + handled = FALSE; + if (GTK_IS_LIST_ITEM (event_widget)) + { + gint key; + + filename = gtk_object_get_user_data (GTK_OBJECT (event_widget)); + + key = (gint) gtk_object_get_data (GTK_OBJECT (widget), list_changed_key); + + switch (event->type) + { + case GDK_BUTTON_PRESS: + + gtk_signal_handler_block (GTK_OBJECT (widget), key); + gtk_widget_activate (GTK_WIDGET (event_widget)); + gtk_signal_handler_unblock (GTK_OBJECT (widget), key); + + gtk_widget_grab_focus (fs->selection_entry); + gtk_entry_set_text (GTK_ENTRY (fs->selection_entry), filename); + handled = TRUE; + break; + + case GDK_2BUTTON_PRESS: + gtk_file_selection_populate (fs, filename, FALSE); + handled = TRUE; + break; + + default: + break; + } + } + + return handled; +} + +static void +gtk_file_selection_dir_list_changed (GtkList *list, + gpointer func_data) +{ + GtkFileSelection *fs; + + g_return_if_fail (func_data != NULL); + g_return_if_fail (GTK_IS_FILE_SELECTION (func_data)); + + fs = GTK_FILE_SELECTION (func_data); + + /* only act on an appropriate selection + */ + if (list->selection && list->selection->data) + { + GtkListItem *item; + + item = list->selection->data; + + if (GTK_IS_LIST_ITEM (item)) + { + gchar *filename; + + filename = gtk_object_get_user_data (GTK_OBJECT (item)); + + if (filename) + gtk_file_selection_populate (fs, filename, FALSE); + } + } +} + +static void +gtk_file_selection_populate (GtkFileSelection *fs, + gchar *rel_path, + gint try_complete) +{ + CompletionState *cmpl_state; + PossibleCompletion* poss; + GList *dir_list = NULL; + GList *file_list = NULL; + GtkWidget *label; + gchar* filename; + gchar* rem_path = rel_path; + gchar* sel_text; + gint did_recurse = FALSE; + gint possible_count = 0; + gint selection_index = -1; + gint dir_changed_key; + + g_return_if_fail (fs != NULL); + g_return_if_fail (GTK_IS_FILE_SELECTION (fs)); + + dir_changed_key = (gint) gtk_object_get_data (GTK_OBJECT (fs->dir_list), list_changed_key); + gtk_signal_handler_block (GTK_OBJECT (fs->dir_list), dir_changed_key); + + cmpl_state = (CompletionState*) fs->cmpl_state; + poss = cmpl_completion_matches (rel_path, &rem_path, cmpl_state); + + if (!cmpl_state_okay (cmpl_state)) + { + /* Something went wrong. */ + gtk_file_selection_abort (fs); + gtk_signal_handler_unblock (GTK_OBJECT (fs->dir_list), dir_changed_key); + return; + } + + g_assert (cmpl_state->reference_dir); + + /* Set the dir_list and file_list to be GLists of strdup'd + * filenames, including ./ and ../ */ + dir_list = g_list_prepend (dir_list, g_strdup("./")); + dir_list = g_list_prepend (dir_list, g_strdup("../")); + + while (poss) + { + if (cmpl_is_a_completion (poss)) + { + possible_count += 1; + + filename = g_strdup (cmpl_this_completion (poss)); + + if (cmpl_is_directory (poss)) + { + if (strcmp (filename, "./") != 0 && + strcmp (filename, "../") != 0) + dir_list = g_list_prepend (dir_list, filename); + } + else + file_list = g_list_prepend (file_list, filename); + } + + poss = cmpl_next_completion (cmpl_state); + } + + /* File lists are set. */ + + g_assert (cmpl_state->reference_dir); + + if (try_complete) + { + /* User is trying to complete filenames, so advance the user's input + * string to the updated_text, which is the common leading substring + * of all possible completions, and if its a directory attempt + * attempt completions in it. */ + + if (cmpl_updated_text (cmpl_state)[0]) + { + if (cmpl_updated_dir (cmpl_state)) + { + gchar* dir_name = g_strdup (cmpl_updated_text (cmpl_state)); + + did_recurse = TRUE; + + gtk_file_selection_populate (fs, dir_name, TRUE); + + g_free (dir_name); + } + else + { + if (fs->selection_entry) + gtk_entry_set_text (GTK_ENTRY (fs->selection_entry), + cmpl_updated_text (cmpl_state)); + } + } + else + { + selection_index = cmpl_last_valid_char (cmpl_state) - + (strlen (rel_path) - strlen (rem_path)); + if (fs->selection_entry) + gtk_entry_set_text (GTK_ENTRY (fs->selection_entry), rem_path); + } + } + else + { + if (fs->selection_entry) + gtk_entry_set_text (GTK_ENTRY (fs->selection_entry), ""); + } + + if (!did_recurse) + { + GList *file_label_list = NULL; + GList *dir_label_list = NULL; + + /* This reverses the lists. */ + while (file_list) + { + label = gtk_list_item_new_with_label (file_list->data); + gtk_object_set_user_data (GTK_OBJECT (label), file_list->data); + gtk_widget_show (label); + + file_label_list = g_list_prepend (file_label_list, label); + file_list = file_list->next; + } + + while (dir_list) + { + label = gtk_list_item_new_with_label (dir_list->data); + gtk_object_set_user_data (GTK_OBJECT (label), dir_list->data); + gtk_widget_show (label); + + dir_label_list = g_list_prepend (dir_label_list, label); + dir_list = dir_list->next; + } + + gtk_container_disable_resize (GTK_CONTAINER (fs)); + + if (fs->selection_entry) + gtk_entry_set_position (GTK_ENTRY (fs->selection_entry), selection_index); + + if (fs->selection_entry) + { + sel_text = g_new (char, strlen (cmpl_reference_position (cmpl_state)) + + sizeof ("Selection: ")); + strcpy (sel_text, "Selection: "); + strcat (sel_text, cmpl_reference_position (cmpl_state)); + + gtk_label_set (GTK_LABEL (fs->selection_text), sel_text); + g_free (sel_text); + } + + if (fs->dir_list) + { + gtk_container_foreach (GTK_CONTAINER (fs->dir_list), + gtk_file_selection_free_filename, NULL); + gtk_list_clear_items (GTK_LIST (fs->dir_list), 0, -1); + + if (dir_label_list) + gtk_list_append_items (GTK_LIST (fs->dir_list), dir_label_list); + } + if (fs->file_list) + { + gtk_container_foreach (GTK_CONTAINER (fs->file_list), + gtk_file_selection_free_filename, NULL); + gtk_list_clear_items (GTK_LIST (fs->file_list), 0, -1); + + if (file_label_list) + gtk_list_append_items (GTK_LIST (fs->file_list), file_label_list); + } + + gtk_container_enable_resize (GTK_CONTAINER (fs)); + } + else + { + GList *dir_list0 = dir_list; + GList *file_list0 = file_list; + + while (dir_list) + { + GList *tmp = dir_list; + dir_list = dir_list->next; + + if (tmp) + g_free (tmp->data); + } + + while (file_list) + { + GList *tmp = file_list; + file_list = file_list->next; + + if (tmp) + g_free (tmp->data); + } + + g_list_free (dir_list0); + g_list_free (file_list0); + } + + gtk_signal_handler_unblock (GTK_OBJECT (fs->dir_list), dir_changed_key); +} + +static void +gtk_file_selection_abort (GtkFileSelection *fs) +{ + gchar err_buf[256]; + + sprintf (err_buf, "Directory unreadable: %s", cmpl_strerror (cmpl_errno)); + + /* BEEP gdk_beep(); */ + + if (fs->selection_entry) + gtk_label_set (GTK_LABEL (fs->selection_text), err_buf); +} + +static void +gtk_file_selection_free_filename (GtkWidget *widget, + gpointer client_data) +{ + g_return_if_fail (widget != NULL); + + g_free (gtk_object_get_user_data (GTK_OBJECT (widget))); + gtk_object_set_user_data (GTK_OBJECT (widget), NULL); +} + + + +/**********************************************************************/ +/* External Interface */ +/**********************************************************************/ + +/* The four completion state selectors + */ +static gchar* +cmpl_updated_text (CompletionState* cmpl_state) +{ + return cmpl_state->updated_text; +} + +static gint +cmpl_updated_dir (CompletionState* cmpl_state) +{ + return cmpl_state->re_complete; +} + +static gchar* +cmpl_reference_position (CompletionState* cmpl_state) +{ + return cmpl_state->reference_dir->fullname; +} + +static gint +cmpl_last_valid_char (CompletionState* cmpl_state) +{ + return cmpl_state->last_valid_char; +} + +static gchar* +cmpl_completion_fullname (gchar* text, CompletionState* cmpl_state) +{ + if (text[0] == '/') + { + strcpy (cmpl_state->updated_text, text); + } + else if (text[0] == '~') + { + CompletionDir* dir; + char* slash; + + dir = open_user_dir (text, cmpl_state); + + if (!dir) + { + /* spencer says just return ~something, so + * for now just do it. */ + strcpy (cmpl_state->updated_text, text); + } + else + { + + strcpy (cmpl_state->updated_text, dir->fullname); + + slash = strchr (text, '/'); + + if (slash) + strcat (cmpl_state->updated_text, slash); + } + } + else + { + strcpy (cmpl_state->updated_text, cmpl_state->reference_dir->fullname); + strcat (cmpl_state->updated_text, "/"); + strcat (cmpl_state->updated_text, text); + } + + return cmpl_state->updated_text; +} + +/* The three completion selectors + */ +static gchar* +cmpl_this_completion (PossibleCompletion* pc) +{ + return pc->text; +} + +static gint +cmpl_is_directory (PossibleCompletion* pc) +{ + return pc->is_directory; +} + +static gint +cmpl_is_a_completion (PossibleCompletion* pc) +{ + return pc->is_a_completion; +} + +/**********************************************************************/ +/* Construction, deletion */ +/**********************************************************************/ + +static CompletionState* +cmpl_init_state (void) +{ + gchar getcwd_buf[2*MAXPATHLEN]; + CompletionState *new_state; + + new_state = g_new (CompletionState, 1); + + if (!getcwd (getcwd_buf, MAXPATHLEN)) + { + cmpl_errno = errno; + return NULL; + } + + new_state->reference_dir = NULL; + new_state->completion_dir = NULL; + new_state->active_completion_dir = NULL; + + if ((new_state->user_home_dir = getenv("HOME")) != NULL) + { + /* if this fails, get_pwdb will fill it in. */ + new_state->user_home_dir = g_strdup(new_state->user_home_dir); + } + + new_state->directory_storage = NULL; + new_state->directory_sent_storage = NULL; + new_state->last_valid_char = 0; + new_state->updated_text = g_new (gchar, MAXPATHLEN); + new_state->updated_text_alloc = MAXPATHLEN; + new_state->the_completion.text = g_new (gchar, MAXPATHLEN); + new_state->the_completion.text_alloc = MAXPATHLEN; + new_state->user_dir_name_buffer = NULL; + new_state->user_directories = NULL; + + new_state->reference_dir = open_dir (getcwd_buf, new_state); + + if (!new_state->reference_dir) + return NULL; + + return new_state; +} + +static void +cmpl_free_dir_list(GList* dp0) +{ + GList *dp = dp0; + + while (dp) { + free_dir (dp->data); + dp = dp->next; + } + + g_list_free(dp0); +} + +static void +cmpl_free_dir_sent_list(GList* dp0) +{ + GList *dp = dp0; + + while (dp) { + free_dir_sent (dp->data); + dp = dp->next; + } + + g_list_free(dp0); +} + +static void +cmpl_free_state (CompletionState* cmpl_state) +{ + cmpl_free_dir_list(cmpl_state->directory_storage); + cmpl_free_dir_sent_list(cmpl_state->directory_sent_storage); + + if (cmpl_state->user_dir_name_buffer) + g_free (cmpl_state->user_dir_name_buffer); + if (cmpl_state->user_directories) + g_free (cmpl_state->user_directories); + if (cmpl_state->the_completion.text) + g_free (cmpl_state->the_completion.text); + if (cmpl_state->updated_text) + g_free (cmpl_state->updated_text); + + g_free (cmpl_state); +} + +static void +free_dir(CompletionDir* dir) +{ + g_free(dir->fullname); + g_free(dir); +} + +static void +free_dir_sent(CompletionDirSent* sent) +{ + g_free(sent->name_buffer); + g_free(sent->entries); + g_free(sent); +} + +static void +prune_memory_usage(CompletionState *cmpl_state) +{ + GList* cdsl = cmpl_state->directory_sent_storage; + GList* cdl = cmpl_state->directory_storage; + GList* cdl0 = cdl; + gint len = 0; + + for(; cdsl && len < CMPL_DIRECTORY_CACHE_SIZE; len += 1) + cdsl = cdsl->next; + + if (cdsl) { + cmpl_free_dir_sent_list(cdsl->next); + cdsl->next = NULL; + } + + while (cdl) { + if (cdl->data == cmpl_state->reference_dir) + cmpl_state->directory_storage = g_list_prepend(NULL, cdl->data); + else + free_dir (cdl->data); + cdl = cdl->next; + } + + g_list_free(cdl0); +} + +/**********************************************************************/ +/* The main entrances. */ +/**********************************************************************/ + +static PossibleCompletion* +cmpl_completion_matches (gchar* text_to_complete, + gchar** remaining_text, + CompletionState* cmpl_state) +{ + gchar* first_slash; + PossibleCompletion *poss; + + prune_memory_usage(cmpl_state); + + g_assert(text_to_complete); + + cmpl_state->user_completion_index = -1; + cmpl_state->last_completion_text = text_to_complete; + cmpl_state->the_completion.text[0] = 0; + cmpl_state->last_valid_char = 0; + cmpl_state->updated_text_len = -1; + cmpl_state->updated_text[0] = 0; + cmpl_state->re_complete = FALSE; + + first_slash = strchr(text_to_complete, '/'); + + if(text_to_complete[0] == '~' && !first_slash) + { + /* Text starts with ~ and there is no slash, show all the + * home directory completions. + */ + poss = attempt_homedir_completion(text_to_complete, cmpl_state); + + update_cmpl(poss, cmpl_state); + + return poss; + } + + cmpl_state->reference_dir = + open_ref_dir(text_to_complete, remaining_text, cmpl_state); + + if(!cmpl_state->reference_dir) + return NULL; + + cmpl_state->completion_dir = + find_completion_dir(*remaining_text, remaining_text, cmpl_state); + + cmpl_state->last_valid_char = *remaining_text - text_to_complete; + + if(!cmpl_state->completion_dir) + return NULL; + + cmpl_state->completion_dir->cmpl_index = -1; + cmpl_state->completion_dir->cmpl_parent = NULL; + cmpl_state->completion_dir->cmpl_text = *remaining_text; + + cmpl_state->active_completion_dir = cmpl_state->completion_dir; + + cmpl_state->reference_dir = cmpl_state->completion_dir; + + poss = attempt_file_completion(cmpl_state); + + update_cmpl(poss, cmpl_state); + + return poss; +} + +static PossibleCompletion* +cmpl_next_completion (CompletionState* cmpl_state) +{ + PossibleCompletion* poss = NULL; + + cmpl_state->the_completion.text[0] = 0; + + if(cmpl_state->user_completion_index >= 0) + poss = attempt_homedir_completion(cmpl_state->last_completion_text, cmpl_state); + else + poss = attempt_file_completion(cmpl_state); + + update_cmpl(poss, cmpl_state); + + return poss; +} + +/**********************************************************************/ +/* Directory Operations */ +/**********************************************************************/ + +/* Open the directory where completion will begin from, if possible. */ +static CompletionDir* +open_ref_dir(gchar* text_to_complete, + gchar** remaining_text, + CompletionState* cmpl_state) +{ + gchar* first_slash; + CompletionDir *new_dir; + + first_slash = strchr(text_to_complete, '/'); + + if (text_to_complete[0] == '/' || !cmpl_state->reference_dir) + { + new_dir = open_dir("/", cmpl_state); + + if(new_dir) + *remaining_text = text_to_complete + 1; + } + else if (text_to_complete[0] == '~') + { + new_dir = open_user_dir(text_to_complete, cmpl_state); + + if(new_dir) + { + if(first_slash) + *remaining_text = first_slash + 1; + else + *remaining_text = text_to_complete + strlen(text_to_complete); + } + else + { + return NULL; + } + } + else + { + *remaining_text = text_to_complete; + + new_dir = open_dir(cmpl_state->reference_dir->fullname, cmpl_state); + } + + if(new_dir) + { + new_dir->cmpl_index = -1; + new_dir->cmpl_parent = NULL; + } + + return new_dir; +} + +/* open a directory by user name */ +static CompletionDir* +open_user_dir(gchar* text_to_complete, + CompletionState *cmpl_state) +{ + gchar *first_slash; + gint cmp_len; + + g_assert(text_to_complete && text_to_complete[0] == '~'); + + first_slash = strchr(text_to_complete, '/'); + + if (first_slash) + cmp_len = first_slash - text_to_complete - 1; + else + cmp_len = strlen(text_to_complete + 1); + + if(!cmp_len) + { + /* ~/ */ + if (!cmpl_state->user_home_dir && + !get_pwdb(cmpl_state)) + return NULL; + return open_dir(cmpl_state->user_home_dir, cmpl_state); + } + else + { + /* ~user/ */ + char* copy = g_new(char, cmp_len + 1); + struct passwd *pwd; + strncpy(copy, text_to_complete + 1, cmp_len); + copy[cmp_len] = 0; + pwd = getpwnam(copy); + g_free(copy); + if (!pwd) + { + cmpl_errno = errno; + return NULL; + } + + return open_dir(pwd->pw_dir, cmpl_state); + } +} + +/* open a directory relative the the current relative directory */ +static CompletionDir* +open_relative_dir(gchar* dir_name, + CompletionDir* dir, + CompletionState *cmpl_state) +{ + gchar path_buf[2*MAXPATHLEN]; + + if(dir->fullname_len + strlen(dir_name) + 2 >= MAXPATHLEN) + { + cmpl_errno = CMPL_ERRNO_TOO_LONG; + return NULL; + } + + strcpy(path_buf, dir->fullname); + + if(dir->fullname_len > 1) + { + path_buf[dir->fullname_len] = '/'; + strcpy(path_buf + dir->fullname_len + 1, dir_name); + } + else + { + strcpy(path_buf + dir->fullname_len, dir_name); + } + + return open_dir(path_buf, cmpl_state); +} + +/* after the cache lookup fails, really open a new directory */ +static CompletionDirSent* +open_new_dir(gchar* dir_name, struct stat* sbuf) +{ + CompletionDirSent* sent; + DIR* directory; + gchar *buffer_ptr; + struct dirent *dirent_ptr; + gint buffer_size = 0; + gint entry_count = 0; + gint i; + struct stat ent_sbuf; + char path_buf[MAXPATHLEN*2]; + gint path_buf_len; + + sent = g_new(CompletionDirSent, 1); + sent->mtime = sbuf->st_mtime; + sent->inode = sbuf->st_ino; + + path_buf_len = strlen(dir_name); + + if (path_buf_len > MAXPATHLEN) + { + cmpl_errno = CMPL_ERRNO_TOO_LONG; + return NULL; + } + + strcpy(path_buf, dir_name); + + directory = opendir(dir_name); + + if(!directory) + { + cmpl_errno = errno; + return NULL; + } + + while((dirent_ptr = readdir(directory)) != NULL) + { + int entry_len = strlen(dirent_ptr->d_name); + buffer_size += entry_len + 1; + entry_count += 1; + + if(path_buf_len + entry_len + 2 >= MAXPATHLEN) + { + cmpl_errno = CMPL_ERRNO_TOO_LONG; + closedir(directory); + return NULL; + } + } + + sent->name_buffer = g_new(gchar, buffer_size); + sent->entries = g_new(CompletionDirEntry, entry_count); + sent->entry_count = entry_count; + + buffer_ptr = sent->name_buffer; + + rewinddir(directory); + + for(i = 0; i < entry_count; i += 1) + { + dirent_ptr = readdir(directory); + + if(!dirent_ptr) + { + cmpl_errno = errno; + closedir(directory); + return NULL; + } + + strcpy(buffer_ptr, dirent_ptr->d_name); + sent->entries[i].entry_name = buffer_ptr; + buffer_ptr += strlen(dirent_ptr->d_name); + *buffer_ptr = 0; + buffer_ptr += 1; + + path_buf[path_buf_len] = '/'; + strcpy(path_buf + path_buf_len + 1, dirent_ptr->d_name); + + if(stat(path_buf, &ent_sbuf) >= 0 && S_ISDIR(ent_sbuf.st_mode)) + sent->entries[i].is_dir = 1; + else + /* stat may fail, and we don't mind, since it could be a + * dangling symlink. */ + sent->entries[i].is_dir = 0; + } + + qsort(sent->entries, sent->entry_count, sizeof(CompletionDirEntry), compare_cmpl_dir); + + closedir(directory); + + return sent; +} + +/* open a directory by absolute pathname */ +static CompletionDir* +open_dir(gchar* dir_name, CompletionState* cmpl_state) +{ + struct stat sbuf; + CompletionDirSent *sent; + GList* cdsl; + + if(stat(dir_name, &sbuf) < 0) + { + cmpl_errno = errno; + return NULL; + } + + cdsl = cmpl_state->directory_sent_storage; + + while (cdsl) + { + sent = cdsl->data; + + if(sent->inode == sbuf.st_ino && + sent->mtime == sbuf.st_mtime) + return attach_dir(sent, dir_name, cmpl_state); + + cdsl = cdsl->next; + } + + sent = open_new_dir(dir_name, &sbuf); + + if (sent) { + cmpl_state->directory_sent_storage = + g_list_prepend(cmpl_state->directory_sent_storage, sent); + + return attach_dir(sent, dir_name, cmpl_state); + } + + return NULL; +} + +static CompletionDir* +attach_dir(CompletionDirSent* sent, gchar* dir_name, CompletionState *cmpl_state) +{ + CompletionDir* new_dir; + + new_dir = g_new(CompletionDir, 1); + + cmpl_state->directory_storage = + g_list_prepend(cmpl_state->directory_storage, new_dir); + + new_dir->sent = sent; + new_dir->fullname = g_strdup(dir_name); + new_dir->fullname_len = strlen(dir_name); + + return new_dir; +} + +static gint +correct_dir_fullname(CompletionDir* cmpl_dir) +{ + gint length = strlen(cmpl_dir->fullname); + struct stat sbuf; + + if (strcmp(cmpl_dir->fullname + length - 2, "/.") == 0) + cmpl_dir->fullname[length - 2] = 0; + else if (strcmp(cmpl_dir->fullname + length - 3, "/./") == 0) + cmpl_dir->fullname[length - 3] = 0; + else if (strcmp(cmpl_dir->fullname + length - 3, "/..") == 0) + { + if(length == 3) + { + strcpy(cmpl_dir->fullname, "/"); + cmpl_dir->fullname_len = 1; + return TRUE; + } + + if(stat(cmpl_dir->fullname, &sbuf) < 0) + { + cmpl_errno = errno; + return FALSE; + } + + cmpl_dir->fullname[length - 3] = 0; + + if(!correct_parent(cmpl_dir, &sbuf)) + return FALSE; + } + else if (strcmp(cmpl_dir->fullname + length - 4, "/../") == 0) + { + if(length == 4) + { + strcpy(cmpl_dir->fullname, "/"); + cmpl_dir->fullname_len = 1; + return TRUE; + } + + if(stat(cmpl_dir->fullname, &sbuf) < 0) + { + cmpl_errno = errno; + return FALSE; + } + + cmpl_dir->fullname[length - 4] = 0; + + if(!correct_parent(cmpl_dir, &sbuf)) + return FALSE; + } + + cmpl_dir->fullname_len = strlen(cmpl_dir->fullname); + + return TRUE; +} + +static gint +correct_parent(CompletionDir* cmpl_dir, struct stat *sbuf) +{ + struct stat parbuf; + gchar *last_slash; + gchar *new_name; + gchar c = 0; + + last_slash = strrchr(cmpl_dir->fullname, '/'); + + g_assert(last_slash); + + if(last_slash != cmpl_dir->fullname) + last_slash[0] = 0; + else + { + c = last_slash[1]; + last_slash[1] = 0; + } + + if (stat(cmpl_dir->fullname, &parbuf) < 0) + { + cmpl_errno = errno; + return FALSE; + } + + if (parbuf.st_ino == sbuf->st_ino && parbuf.st_dev == sbuf->st_dev) + /* it wasn't a link */ + return TRUE; + + if(c) + last_slash[1] = c; + else + last_slash[0] = '/'; + + /* it was a link, have to figure it out the hard way */ + + new_name = find_parent_dir_fullname(cmpl_dir->fullname); + + if (!new_name) + return FALSE; + + g_free(cmpl_dir->fullname); + + cmpl_dir->fullname = new_name; + + return TRUE; +} + +static gchar* +find_parent_dir_fullname(gchar* dirname) +{ + gchar buffer[MAXPATHLEN]; + gchar buffer2[MAXPATHLEN]; + + if(!getcwd(buffer, MAXPATHLEN)) + { + cmpl_errno = errno; + return NULL; + } + + if(chdir(dirname) != 0 || chdir("..") != 0) + { + cmpl_errno = errno; + return NULL; + } + + if(!getcwd(buffer2, MAXPATHLEN)) + { + chdir(buffer); + cmpl_errno = errno; + + return NULL; + } + + if(chdir(buffer) != 0) + { + cmpl_errno = errno; + return NULL; + } + + return g_strdup(buffer2); +} + +/**********************************************************************/ +/* Completion Operations */ +/**********************************************************************/ + +static PossibleCompletion* +attempt_homedir_completion(gchar* text_to_complete, + CompletionState *cmpl_state) +{ + gint index, length; + + if (!cmpl_state->user_dir_name_buffer && + !get_pwdb(cmpl_state)) + return NULL; + length = strlen(text_to_complete) - 1; + + cmpl_state->user_completion_index += 1; + + while(cmpl_state->user_completion_index < cmpl_state->user_directories_len) + { + index = first_diff_index(text_to_complete + 1, + cmpl_state->user_directories + [cmpl_state->user_completion_index].login); + + switch(index) + { + case PATTERN_MATCH: + break; + default: + if(cmpl_state->last_valid_char < (index + 1)) + cmpl_state->last_valid_char = index + 1; + cmpl_state->user_completion_index += 1; + continue; + } + + cmpl_state->the_completion.is_a_completion = 1; + cmpl_state->the_completion.is_directory = 1; + + append_completion_text("~", cmpl_state); + + append_completion_text(cmpl_state-> + user_directories[cmpl_state->user_completion_index].login, + cmpl_state); + + return append_completion_text("/", cmpl_state); + } + + if(text_to_complete[1] || + cmpl_state->user_completion_index > cmpl_state->user_directories_len) + { + cmpl_state->user_completion_index = -1; + return NULL; + } + else + { + cmpl_state->user_completion_index += 1; + cmpl_state->the_completion.is_a_completion = 1; + cmpl_state->the_completion.is_directory = 1; + + return append_completion_text("~/", cmpl_state); + } +} + +/* returns the index (>= 0) of the first differing character, + * PATTERN_MATCH if the completion matches */ +static gint +first_diff_index(gchar* pat, gchar* text) +{ + gint diff = 0; + + while(*pat && *text && *text == *pat) + { + pat += 1; + text += 1; + diff += 1; + } + + if(*pat) + return diff; + + return PATTERN_MATCH; +} + +static PossibleCompletion* +append_completion_text(gchar* text, CompletionState* cmpl_state) +{ + gint len, i = 1; + + if(!cmpl_state->the_completion.text) + return NULL; + + len = strlen(text) + strlen(cmpl_state->the_completion.text) + 1; + + if(cmpl_state->the_completion.text_alloc > len) + { + strcat(cmpl_state->the_completion.text, text); + return &cmpl_state->the_completion; + } + + while(i < len) { i <<= 1; } + + cmpl_state->the_completion.text_alloc = i; + + cmpl_state->the_completion.text = (gchar*)g_realloc(cmpl_state->the_completion.text, i); + + if(!cmpl_state->the_completion.text) + return NULL; + else + { + strcat(cmpl_state->the_completion.text, text); + return &cmpl_state->the_completion; + } +} + +static CompletionDir* +find_completion_dir(gchar* text_to_complete, + gchar** remaining_text, + CompletionState* cmpl_state) +{ + gchar* first_slash = strchr(text_to_complete, '/'); + CompletionDir* dir = cmpl_state->reference_dir; + *remaining_text = text_to_complete; + + while(first_slash) + { + gint len = first_slash - *remaining_text; + gint found = 0; + gint found_index = -1; + gint i; + gchar* pat_buf = g_new (gchar, len + 1); + + strncpy(pat_buf, *remaining_text, len); + pat_buf[len] = 0; + + for(i = 0; i < dir->sent->entry_count; i += 1) + { + if(dir->sent->entries[i].is_dir && + fnmatch(pat_buf, dir->sent->entries[i].entry_name, + FNMATCH_FLAGS)!= FNM_NOMATCH) + { + if(found) + { + g_free (pat_buf); + return dir; + } + else + { + found = 1; + found_index = i; + } + } + } + + if(found) + { + CompletionDir* next = open_relative_dir(dir->sent->entries[found_index].entry_name, + dir, cmpl_state); + + if(!next) + { + g_free (pat_buf); + return NULL; + } + + next->cmpl_parent = dir; + + dir = next; + + if(!correct_dir_fullname(dir)) + { + g_free(pat_buf); + return NULL; + } + + *remaining_text = first_slash + 1; + first_slash = strchr(*remaining_text, '/'); + } + else + { + g_free (pat_buf); + return NULL; + } + + g_free (pat_buf); + } + + return dir; +} + +static void +update_cmpl(PossibleCompletion* poss, CompletionState* cmpl_state) +{ + gint cmpl_len; + + if(!poss || !cmpl_is_a_completion(poss)) + return; + + cmpl_len = strlen(cmpl_this_completion(poss)); + + if(cmpl_state->updated_text_alloc < cmpl_len + 1) + { + cmpl_state->updated_text = + (gchar*)g_realloc(cmpl_state->updated_text, + cmpl_state->updated_text_alloc); + cmpl_state->updated_text_alloc = 2*cmpl_len; + } + + if(cmpl_state->updated_text_len < 0) + { + strcpy(cmpl_state->updated_text, cmpl_this_completion(poss)); + cmpl_state->updated_text_len = cmpl_len; + cmpl_state->re_complete = cmpl_is_directory(poss); + } + else if(cmpl_state->updated_text_len == 0) + { + cmpl_state->re_complete = FALSE; + } + else + { + gint first_diff = + first_diff_index(cmpl_state->updated_text, + cmpl_this_completion(poss)); + + cmpl_state->re_complete = FALSE; + + if(first_diff == PATTERN_MATCH) + return; + + if(first_diff > cmpl_state->updated_text_len) + strcpy(cmpl_state->updated_text, cmpl_this_completion(poss)); + + cmpl_state->updated_text_len = first_diff; + cmpl_state->updated_text[first_diff] = 0; + } +} + +static PossibleCompletion* +attempt_file_completion(CompletionState *cmpl_state) +{ + gchar *pat_buf, *first_slash; + CompletionDir *dir = cmpl_state->active_completion_dir; + + dir->cmpl_index += 1; + + if(dir->cmpl_index == dir->sent->entry_count) + { + if(dir->cmpl_parent == NULL) + { + cmpl_state->active_completion_dir = NULL; + + return NULL; + } + else + { + cmpl_state->active_completion_dir = dir->cmpl_parent; + + return attempt_file_completion(cmpl_state); + } + } + + g_assert(dir->cmpl_text); + + first_slash = strchr(dir->cmpl_text, '/'); + + if(first_slash) + { + gint len = first_slash - dir->cmpl_text; + + pat_buf = g_new (gchar, len + 1); + strncpy(pat_buf, dir->cmpl_text, len); + pat_buf[len] = 0; + } + else + { + gint len = strlen(dir->cmpl_text); + + pat_buf = g_new (gchar, len + 2); + strcpy(pat_buf, dir->cmpl_text); + strcpy(pat_buf + len, "*"); + } + + if(first_slash) + { + if(dir->sent->entries[dir->cmpl_index].is_dir) + { + if(fnmatch(pat_buf, dir->sent->entries[dir->cmpl_index].entry_name, + FNMATCH_FLAGS) != FNM_NOMATCH) + { + CompletionDir* new_dir; + + new_dir = open_relative_dir(dir->sent->entries[dir->cmpl_index].entry_name, + dir, cmpl_state); + + if(!new_dir) + { + g_free (pat_buf); + return NULL; + } + + new_dir->cmpl_parent = dir; + + new_dir->cmpl_index = -1; + new_dir->cmpl_text = first_slash + 1; + + cmpl_state->active_completion_dir = new_dir; + + g_free (pat_buf); + return attempt_file_completion(cmpl_state); + } + else + { + g_free (pat_buf); + return attempt_file_completion(cmpl_state); + } + } + else + { + g_free (pat_buf); + return attempt_file_completion(cmpl_state); + } + } + else + { + if(dir->cmpl_parent != NULL) + { + append_completion_text(dir->fullname + + strlen(cmpl_state->completion_dir->fullname) + 1, + cmpl_state); + append_completion_text("/", cmpl_state); + } + + append_completion_text(dir->sent->entries[dir->cmpl_index].entry_name, cmpl_state); + + cmpl_state->the_completion.is_a_completion = + (fnmatch(pat_buf, dir->sent->entries[dir->cmpl_index].entry_name, + FNMATCH_FLAGS) != FNM_NOMATCH); + + cmpl_state->the_completion.is_directory = dir->sent->entries[dir->cmpl_index].is_dir; + if(dir->sent->entries[dir->cmpl_index].is_dir) + append_completion_text("/", cmpl_state); + + g_free (pat_buf); + return &cmpl_state->the_completion; + } +} + + +static gint +get_pwdb(CompletionState* cmpl_state) +{ + struct passwd *pwd_ptr; + gchar* buf_ptr, *home_dir = NULL; + gint len = 0, i, count = 0; + + if(cmpl_state->user_dir_name_buffer) + return TRUE; + setpwent (); + + while ((pwd_ptr = getpwent()) != NULL) + { + len += strlen(pwd_ptr->pw_name); + len += strlen(pwd_ptr->pw_dir); + len += 2; + count += 1; + } + + if (!cmpl_state->user_home_dir) + { + /* the loser doesn't have $HOME set */ + setpwent (); + + pwd_ptr = getpwuid(getuid()); + if(!pwd_ptr) + { + cmpl_errno = errno; + goto error; + } + home_dir = pwd_ptr->pw_dir; + + len += strlen(home_dir); + len += 1; + } + + setpwent (); + + cmpl_state->user_dir_name_buffer = g_new(gchar, len); + cmpl_state->user_directories = g_new(CompletionUserDir, count); + cmpl_state->user_directories_len = count; + + buf_ptr = cmpl_state->user_dir_name_buffer; + + if (!cmpl_state->user_home_dir) + { + strcpy(buf_ptr, home_dir); + cmpl_state->user_home_dir = buf_ptr; + buf_ptr += strlen(buf_ptr); + buf_ptr += 1; + } + + for(i = 0; i < count; i += 1) + { + pwd_ptr = getpwent(); + if(!pwd_ptr) + { + cmpl_errno = errno; + goto error; + } + + strcpy(buf_ptr, pwd_ptr->pw_name); + cmpl_state->user_directories[i].login = buf_ptr; + buf_ptr += strlen(buf_ptr); + buf_ptr += 1; + strcpy(buf_ptr, pwd_ptr->pw_dir); + cmpl_state->user_directories[i].homedir = buf_ptr; + buf_ptr += strlen(buf_ptr); + buf_ptr += 1; + } + + qsort(cmpl_state->user_directories, + cmpl_state->user_directories_len, + sizeof(CompletionUserDir), + compare_user_dir); + + endpwent(); + + return TRUE; + +error: + + if(cmpl_state->user_dir_name_buffer) + g_free(cmpl_state->user_dir_name_buffer); + if(cmpl_state->user_directories) + g_free(cmpl_state->user_directories); + + cmpl_state->user_dir_name_buffer = NULL; + cmpl_state->user_directories = NULL; + + return FALSE; +} + +static gint +compare_user_dir(const void* a, const void* b) +{ + return strcmp((((CompletionUserDir*)a))->login, + (((CompletionUserDir*)b))->login); +} + +static gint +compare_cmpl_dir(const void* a, const void* b) +{ + return strcmp((((CompletionDirEntry*)a))->entry_name, + (((CompletionDirEntry*)b))->entry_name); +} + +static gint +cmpl_state_okay(CompletionState* cmpl_state) +{ + return cmpl_state && cmpl_state->reference_dir; +} + +static gchar* +cmpl_strerror(gint err) +{ + if(err == CMPL_ERRNO_TOO_LONG) + return "Name too long"; + else + return g_strerror (err); +} |