Rework the "Run" dialog to load all available commands with multi-threading to speed...
[lxde/lxpanel.git] / src / gtk-run.c
1 /*
2 * gtk-run.c: Little application launcher
3 * Copyright (C) 2006 Hong Jen Yee (PCMan) pcman.tw(AT)gmail.com
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 */
19
20 #include <gtk/gtk.h>
21 #include <glib/gi18n.h>
22 #include <string.h>
23 #include <unistd.h>
24
25 #include "misc.h"
26 #include <menu-cache.h>
27
28 static GtkWidget* win = NULL; /* the run dialog */
29 static GSList* app_list = NULL; /* all known apps in menu cache */
30
31 typedef struct _ThreadData
32 {
33 gboolean cancel; /* is the loading cancelled */
34 GSList* files; /* all executable files found */
35 GtkEntry* entry;
36 }ThreadData;
37
38 static ThreadData* thread_data = NULL; /* thread data used to load availble programs in PATH */
39
40 static MenuCacheApp* match_app_by_exec(const char* exec)
41 {
42 GSList* l;
43 MenuCacheApp* ret = NULL;
44 char* exec_path = g_find_program_in_path(exec);
45 const char* pexec;
46 int path_len, exec_len, len;
47
48 if( ! exec_path )
49 return NULL;
50
51 path_len = strlen(exec_path);
52 exec_len = strlen(exec);
53
54 for( l = app_list; l; l = l->next )
55 {
56 MenuCacheApp* app = MENU_CACHE_APP(l->data);
57 const char* app_exec = menu_cache_app_get_exec(app);
58 #if 0 /* This is useless and incorrect. */
59 /* Dirty hacks to skip sudo programs. This can be a little bit buggy */
60 if( g_str_has_prefix(app_exec, "gksu") )
61 {
62 app_exec += 4;
63 if( app_exec[0] == '\0' ) /* "gksu" itself */
64 app_exec -= 4;
65 else if( app_exec[0] == ' ' ) /* "gksu something..." */
66 ++app_exec;
67 else if( g_str_has_prefix(app_exec, "do ") ) /* "gksudo something" */
68 app_exec += 3;
69 }
70 else if( g_str_has_prefix(app_exec, "kdesu ") ) /* kdesu */
71 app_exec += 6;
72 #endif
73
74 if( g_path_is_absolute(app_exec) )
75 {
76 pexec = exec_path;
77 len = path_len;
78 }
79 else
80 {
81 pexec = exec;
82 len = exec_len;
83 }
84
85 if( strncmp(app_exec, pexec, len) == 0 )
86 {
87 /* exact match has the highest priority */
88 if( app_exec[len] == '\0' )
89 {
90 ret = app;
91 break;
92 }
93 /* those matches the pattern: exe_name %F|%f|%U|%u have higher priority */
94 if( app_exec[len] == ' ' )
95 {
96 if( app_exec[len + 1] == '%' )
97 {
98 if( strchr( "FfUu", app_exec[len + 2] ) )
99 {
100 ret = app;
101 break;
102 }
103 }
104 ret = app;
105 }
106 }
107 }
108
109 /* if this is a symlink */
110 if( ! ret && g_file_test(exec_path, G_FILE_TEST_IS_SYMLINK) )
111 {
112 char target[512]; /* FIXME: is this enough? */
113 len = readlink( exec_path, target, sizeof(target) - 1);
114 if( len > 0 )
115 {
116 target[len] = '\0';
117 ret = match_app_by_exec(target);
118 if( ! ret )
119 {
120 /* FIXME: Actually, target could be relative paths.
121 * So, actually path resolution is needed here. */
122 char* basename = g_path_get_basename(target);
123 char* locate = g_find_program_in_path(basename);
124 if( locate && strcmp(locate, target) == 0 )
125 {
126 ret = match_app_by_exec(basename);
127 g_free(locate);
128 }
129 g_free(basename);
130 }
131 }
132 }
133
134 g_free(exec_path);
135 return ret;
136 }
137
138 static void setup_auto_complete_with_data(ThreadData* data)
139 {
140 GtkListStore* store;
141 GSList *l;
142 GtkEntryCompletion* comp = gtk_entry_completion_new();
143 gtk_entry_completion_set_minimum_key_length( comp, 2 );
144 gtk_entry_completion_set_inline_completion( comp, TRUE );
145 #if GTK_CHECK_VERSION( 2, 8, 0 )
146 gtk_entry_completion_set_popup_set_width( comp, TRUE );
147 gtk_entry_completion_set_popup_single_match( comp, FALSE );
148 #endif
149 store = gtk_list_store_new( 1, G_TYPE_STRING );
150
151 for( l = data->files; l; l = l->next )
152 {
153 const char *name = (const char*)l->data;
154 GtkTreeIter it;
155 gtk_list_store_append( store, &it );
156 gtk_list_store_set( store, &it, 0, name, -1 );
157 }
158
159 gtk_entry_completion_set_model( comp, (GtkTreeModel*)store );
160 g_object_unref( store );
161 gtk_entry_completion_set_text_column( comp, 0 );
162 gtk_entry_set_completion( (GtkEntry*)data->entry, comp );
163 g_object_unref( G_OBJECT(comp) );
164 }
165
166 void thread_data_free(ThreadData* data)
167 {
168 g_slist_foreach(data->files, (GFunc)g_free, NULL);
169 g_slist_free(data->files);
170 g_slice_free(ThreadData, data);
171 }
172
173 static gboolean on_thread_finished(ThreadData* data)
174 {
175 /* don't setup entry completion if the thread is already cancelled. */
176 if( !data->cancel )
177 setup_auto_complete_with_data(thread_data);
178 thread_data_free(data);
179 thread_data = NULL; /* global thread_data pointer */
180 return FALSE;
181 }
182
183 static gpointer thread_func(ThreadData* data)
184 {
185 GSList *list = NULL, *l;
186 gchar **dirname;
187 gchar **dirnames = g_strsplit( g_getenv("PATH"), ":", 0 );
188
189 for( dirname = dirnames; !thread_data->cancel && *dirname; ++dirname )
190 {
191 GDir *dir = g_dir_open( *dirname, 0, NULL );
192 const char *name;
193 if( ! dir )
194 continue;
195 while( !thread_data->cancel && (name = g_dir_read_name(dir)) )
196 {
197 char* filename = g_build_filename( *dirname, name, NULL );
198 if( g_file_test( filename, G_FILE_TEST_IS_EXECUTABLE ) )
199 {
200 if(thread_data->cancel)
201 break;
202 if( !g_slist_find_custom( list, name, (GCompareFunc)strcmp ) )
203 list = g_slist_prepend( list, g_strdup( name ) );
204 }
205 g_free( filename );
206 }
207 g_dir_close( dir );
208 }
209 g_strfreev( dirnames );
210
211 data->files = list;
212 /* install an idle handler to free associated data */
213 g_idle_add((GSourceFunc)on_thread_finished, data);
214
215 return NULL;
216 }
217
218 static void setup_auto_complete( GtkEntry* entry )
219 {
220 gboolean cache_is_available = FALSE;
221 /* FIXME: consider saving the list of commands as on-disk cache. */
222 if( cache_is_available )
223 {
224 /* load cached program list */
225 }
226 else
227 {
228 /* load in another working thread */
229 thread_data = g_slice_new0(ThreadData); /* the data will be freed in idle handler later. */
230 thread_data->entry = entry;
231 g_thread_create((GThreadFunc)thread_func, thread_data, FALSE, NULL);
232 }
233 }
234
235 static void on_response( GtkDialog* dlg, gint response, gpointer user_data )
236 {
237 GtkEntry* entry = (GtkEntry*)user_data;
238 if( G_LIKELY(response == GTK_RESPONSE_OK) )
239 {
240 GError* err = NULL;
241 if( !g_spawn_command_line_async( gtk_entry_get_text(entry), &err ) )
242 {
243 show_error( (GtkWindow*)dlg, err->message );
244 g_error_free( err );
245 g_signal_stop_emission_by_name( dlg, "response" );
246 return;
247 }
248 }
249
250 /* cancel running thread if needed */
251 if( thread_data ) /* the thread is still running */
252 thread_data->cancel = TRUE; /* cancel the thread */
253
254 gtk_widget_destroy( (GtkWidget*)dlg );
255 win = NULL;
256
257 /* free app list */
258 g_slist_foreach(app_list, (GFunc)menu_cache_item_unref, NULL);
259 g_slist_free(app_list);
260 app_list = NULL;
261 }
262
263 static void on_entry_changed( GtkEntry* entry, GtkImage* img )
264 {
265 const char* str = gtk_entry_get_text(entry);
266 MenuCacheApp* app = NULL;
267 if( str && *str )
268 app = match_app_by_exec(str);
269
270 if( app )
271 {
272 int w, h;
273 GdkPixbuf* pix;
274 gtk_icon_size_lookup(GTK_ICON_SIZE_DIALOG, &w, &h);
275 pix = lxpanel_load_icon(menu_cache_item_get_icon(MENU_CACHE_ITEM(app)), w, h, TRUE);
276 gtk_image_set_from_pixbuf(img, pix);
277 g_object_unref(pix);
278 }
279 else
280 {
281 gtk_image_set_from_stock(img, GTK_STOCK_EXECUTE, GTK_ICON_SIZE_DIALOG);
282 }
283 }
284
285 void gtk_run()
286 {
287 GtkWidget *entry, *hbox, *img;
288 MenuCache* menu_cache;
289
290 if( win )
291 {
292 gtk_window_present(GTK_WINDOW(win));
293 return;
294 }
295
296 win = gtk_dialog_new_with_buttons( _("Run"),
297 NULL,
298 GTK_DIALOG_NO_SEPARATOR,
299 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
300 GTK_STOCK_OK, GTK_RESPONSE_OK,
301 NULL );
302 gtk_dialog_set_alternative_button_order((GtkDialog*)win,
303 GTK_RESPONSE_OK, GTK_RESPONSE_CANCEL, -1);
304 gtk_dialog_set_default_response( (GtkDialog*)win, GTK_RESPONSE_OK );
305 entry = gtk_entry_new();
306
307 gtk_entry_set_activates_default( (GtkEntry*)entry, TRUE );
308 gtk_box_pack_start( (GtkBox*)((GtkDialog*)win)->vbox,
309 gtk_label_new(_("Enter the command you want to execute:")),
310 FALSE, FALSE, 8 );
311 hbox = gtk_hbox_new( FALSE, 2 );
312 img = gtk_image_new_from_stock( GTK_STOCK_EXECUTE, GTK_ICON_SIZE_DIALOG );
313 gtk_box_pack_start( (GtkBox*)hbox, img,
314 FALSE, FALSE, 4 );
315 gtk_box_pack_start( (GtkBox*)hbox, entry, TRUE, TRUE, 4 );
316 gtk_box_pack_start( (GtkBox*)((GtkDialog*)win)->vbox,
317 hbox, FALSE, FALSE, 8 );
318 g_signal_connect( win, "response", G_CALLBACK(on_response), entry );
319 gtk_window_set_position( (GtkWindow*)win, GTK_WIN_POS_CENTER );
320 gtk_window_set_default_size( (GtkWindow*)win, 360, -1 );
321 gtk_widget_show_all( win );
322
323 setup_auto_complete( (GtkEntry*)entry );
324 gtk_widget_show( win );
325
326 g_signal_connect(entry ,"changed", G_CALLBACK(on_entry_changed), img);
327
328 /* get all apps */
329 menu_cache = menu_cache_lookup(g_getenv("XDG_MENU_PREFIX") ? "applications.menu" : "lxde-applications.menu" );
330 if( menu_cache )
331 {
332 app_list = (GSList*)menu_cache_list_all_apps(menu_cache);
333 menu_cache_unref(menu_cache);
334 }
335 }
336