1 /*
2  *      fm-terminal.c
3  *
4  *      Copyright 2012-2016 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
5  *
6  *      This file is a part of the Libfm library.
7  *
8  *      This library is free software; you can redistribute it and/or
9  *      modify it under the terms of the GNU Lesser General Public
10  *      License as published by the Free Software Foundation; either
11  *      version 2.1 of the License, or (at your option) any later version.
12  *
13  *      This library is distributed in the hope that it will be useful,
14  *      but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  *      Lesser General Public License for more details.
17  *
18  *      You should have received a copy of the GNU Lesser General Public
19  *      License along with this library; if not, write to the Free Software
20  *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
21  */
22 
23 /**
24  * SECTION:fm-terminal
25  * @short_description: Terminals representation for libfm.
26  * @title: FmTerminal
27  *
28  * @include: libfm/fm.h
29  *
30  * The FmTerminal object represents description how applications which
31  * require start in terminal should be started.
32  */
33 
34 #ifdef HAVE_CONFIG_H
35 #include <config.h>
36 #endif
37 
38 #include <glib.h>
39 #include <glib/gi18n-lib.h>
40 #include <gio/gdesktopappinfo.h>
41 #include <string.h>
42 #include <unistd.h>
43 #if !GLIB_CHECK_VERSION(2, 28, 0) && !HAVE_DECL_ENVIRON
44 extern char **environ;
45 #endif
46 
47 #include "fm-terminal.h"
48 #include "fm-config.h"
49 
50 struct _FmTerminalClass
51 {
52     GObjectClass parent;
53 };
54 
55 static void fm_terminal_finalize(GObject *object);
56 
57 G_DEFINE_TYPE(FmTerminal, fm_terminal, G_TYPE_OBJECT);
58 
fm_terminal_class_init(FmTerminalClass * klass)59 static void fm_terminal_class_init(FmTerminalClass *klass)
60 {
61     GObjectClass *g_object_class;
62 
63     g_object_class = G_OBJECT_CLASS(klass);
64     g_object_class->finalize = fm_terminal_finalize;
65 }
66 
fm_terminal_finalize(GObject * object)67 static void fm_terminal_finalize(GObject *object)
68 {
69     FmTerminal* self;
70     g_return_if_fail(object != NULL);
71     g_return_if_fail(FM_IS_TERMINAL(object));
72 
73     self = (FmTerminal*)object;
74     g_free(self->program);
75     g_free(self->open_arg);
76     g_free(self->noclose_arg);
77     g_free(self->launch);
78     g_free(self->desktop_id);
79     g_free(self->custom_args);
80 
81     G_OBJECT_CLASS(fm_terminal_parent_class)->finalize(object);
82 }
83 
fm_terminal_init(FmTerminal * self)84 static void fm_terminal_init(FmTerminal *self)
85 {
86 }
87 
fm_terminal_new(void)88 static FmTerminal* fm_terminal_new(void)
89 {
90     return (FmTerminal*)g_object_new(FM_TERMINAL_TYPE, NULL);
91 }
92 
93 
94 static GSList *terminals = NULL;
95 static FmTerminal *default_terminal = NULL;
96 G_LOCK_DEFINE_STATIC(terminal);
97 
on_terminal_changed(FmConfig * cfg,gpointer unused)98 static void on_terminal_changed(FmConfig *cfg, gpointer unused)
99 {
100     FmTerminal *term = NULL;
101     gsize n;
102     GSList *l;
103     gchar *name, *basename;
104 
105     if(cfg->terminal == NULL)
106         goto _end;
107 
108     for(n = 0; cfg->terminal[n] && cfg->terminal[n] != ' '; n++);
109     name = g_strndup(cfg->terminal, n);
110     basename = strrchr(name, '/');
111     if(basename)
112         basename++;
113     else
114         basename = name;
115     /* g_debug("terminal in FmConfig: %s, args=%s", name, &cfg->terminal[n]); */
116     for(l = terminals; l; l = l->next)
117         if(strcmp(basename, ((FmTerminal*)l->data)->program) == 0)
118             break;
119     /* don't change existing object to be thread-safe */
120     term = fm_terminal_new();
121     if(l)
122     {
123         if(name[0] != '/') /* not full path; call by basename */
124         {
125             term->program = g_strdup(basename);
126             g_free(name);
127         }
128         else /* call by full path */
129             term->program = name;
130         term->open_arg = g_strdup(((FmTerminal*)l->data)->open_arg);
131         term->noclose_arg = g_strdup(((FmTerminal*)l->data)->noclose_arg);
132         term->launch = g_strdup(((FmTerminal*)l->data)->launch);
133         term->desktop_id = g_strdup(((FmTerminal*)l->data)->desktop_id);
134     }
135     else /* unknown terminal */
136     {
137         if (strcmp(basename, "x-terminal-emulator") == 0)
138             g_message("x-terminal-emulator has very limited support, consider choosing another terminal");
139         else
140             g_warning("terminal %s isn't known, consider report it to LibFM developers",
141                       basename);
142         term->program = name;
143         term->open_arg = g_strdup("-e"); /* assume it is default */
144     }
145     if(cfg->terminal[n] == ' ' && cfg->terminal[n+1])
146     {
147         term->custom_args = g_strdup(&cfg->terminal[n+1]);
148         /* support for old style terminal line alike 'xterm -e %s' */
149         name = strchr(term->custom_args, '%');
150         if(name)
151         {
152             /* skip end spaces */
153             while(name > term->custom_args && name[-1] == ' ')
154                 name--;
155             /* drop '-e' or '-x' */
156             if(name > term->custom_args + 1 && name[-2] == '-')
157             {
158                 name -= 2;
159                 /* skip end spaces */
160                 while(name > term->custom_args && name[-1] == ' ')
161                     name--;
162             }
163             if(name > term->custom_args)
164                 *name = '\0'; /* cut the line */
165             else
166             {
167                 g_free(term->custom_args);
168                 term->custom_args = NULL;
169             }
170         }
171     }
172 _end:
173     G_LOCK(terminal);
174     if(default_terminal)
175         g_object_unref(default_terminal);
176     default_terminal = term;
177     G_UNLOCK(terminal);
178 }
179 
180 /* init terminal list from config */
_fm_terminal_init(void)181 void _fm_terminal_init(void)
182 {
183     GKeyFile *kf;
184     gsize i, n;
185     gchar **programs;
186     FmTerminal *term;
187 
188     /* read system terminals file */
189     kf = g_key_file_new();
190     if(g_key_file_load_from_file(kf, PACKAGE_DATA_DIR "/terminals.list", 0, NULL))
191     {
192         programs = g_key_file_get_groups(kf, &n);
193         if(programs)
194         {
195             for(i = 0; i < n; ++i)
196             {
197                 /* g_debug("found terminal configuration: %s", programs[i]); */
198                 term = fm_terminal_new();
199                 term->program = programs[i];
200                 term->open_arg = g_key_file_get_string(kf, programs[i],
201                                                        "open_arg", NULL);
202                 term->noclose_arg = g_key_file_get_string(kf, programs[i],
203                                                           "noclose_arg", NULL);
204                 term->launch = g_key_file_get_string(kf, programs[i],
205                                                      "launch", NULL);
206                 term->desktop_id = g_key_file_get_string(kf, programs[i],
207                                                          "desktop_id", NULL);
208                 terminals = g_slist_append(terminals, term);
209             }
210             g_free(programs); /* strings in the vector are stolen by objects */
211         }
212     }
213     g_key_file_free(kf);
214     /* TODO: read user terminals file? */
215     /* read from config */
216     on_terminal_changed(fm_config, NULL);
217     /* monitor the config */
218     g_signal_connect(fm_config, "changed::terminal",
219                      G_CALLBACK(on_terminal_changed), NULL);
220 }
221 
222 /* free all resources */
_fm_terminal_finalize(void)223 void _fm_terminal_finalize(void)
224 {
225     /* cancel monitor of config */
226     g_signal_handlers_disconnect_by_func(fm_config, on_terminal_changed, NULL);
227     /* free the data */
228     g_slist_foreach(terminals, (GFunc)g_object_unref, NULL);
229     g_slist_free(terminals);
230     terminals = NULL;
231     if(default_terminal)
232         g_object_unref(default_terminal);
233     default_terminal = NULL;
234 }
235 
236 /**
237  * fm_terminal_dup_default
238  * @error: (allow-none): location of error to set
239  *
240  * Retrieves description of terminal which is defined in libfm config.
241  * Returned data should be freed with g_object_unref() after usage.
242  *
243  * Returns: (transfer full): terminal descriptor or %NULL if no terminal is set.
244  *
245  * Since: 1.2.0
246  */
fm_terminal_dup_default(GError ** error)247 FmTerminal* fm_terminal_dup_default(GError **error)
248 {
249     FmTerminal *term = NULL;
250     G_LOCK(terminal);
251     if(default_terminal)
252         term = g_object_ref(default_terminal);
253     G_UNLOCK(terminal);
254     if(!term)
255         g_set_error_literal(error, G_SHELL_ERROR, G_SHELL_ERROR_EMPTY_STRING,
256                             _("No terminal emulator is set in libfm config"));
257     return term;
258 }
259 
child_setup(gpointer user_data)260 static void child_setup(gpointer user_data)
261 {
262     /* Move child to grandparent group so it will not die with parent */
263     setpgid(0, (pid_t)(gsize)user_data);
264 }
265 
266 /**
267  * fm_terminal_launch
268  * @dir: (allow-none): a directory to launch
269  * @error: (allow-none): location of error to set
270  *
271  * Spawns a terminal window in requested @dir. If @dir is %NULL then it
272  * will be spawned in current working directory.
273  *
274  * Returns: %TRUE if spawn was succesful.
275  *
276  * Since: 1.2.0
277  */
fm_terminal_launch(const gchar * dir,GError ** error)278 gboolean fm_terminal_launch(const gchar *dir, GError **error)
279 {
280     FmTerminal *term;
281     GDesktopAppInfo *appinfo = NULL;
282     const gchar *cmd;
283     gchar *_cmd = NULL;
284     gchar **argv;
285     gchar **envp;
286     gint argc;
287     gboolean ret;
288 
289     term = fm_terminal_dup_default(error);
290     if(!term)
291         return FALSE;
292     if(term->desktop_id)
293         appinfo = g_desktop_app_info_new(term->desktop_id);
294     if(appinfo)
295         /* FIXME: is it possible to have some %U there? */
296         cmd = g_app_info_get_commandline(G_APP_INFO(appinfo));
297     else if(term->launch)
298         cmd = _cmd = g_strdup_printf("%s %s", term->program, term->launch);
299     else
300         cmd = term->program;
301     if (term->custom_args)
302     {
303         cmd = g_strdup_printf("%s %s", cmd, term->custom_args);
304         g_free(_cmd);
305         _cmd = (char *)cmd;
306     }
307     if(!g_shell_parse_argv(cmd, &argc, &argv, error))
308         argv = NULL;
309     g_free(_cmd);
310     if(appinfo)
311         g_object_unref(appinfo);
312     g_object_unref(term);
313     if(!argv) /* parsing failed */
314         return FALSE;
315 #if GLIB_CHECK_VERSION(2, 28, 0)
316     envp = g_get_environ();
317 #else
318     envp = g_strdupv(environ);
319 #endif
320     if (dir)
321 #if GLIB_CHECK_VERSION(2, 32, 0)
322         envp = g_environ_setenv(envp, "PWD", dir, TRUE);
323 #else
324     {
325         char **env = envp;
326 
327         if (env) while (*env != NULL)
328         {
329             if (strncmp(*env, "PWD=", 4) == 0)
330                 break;
331             env++;
332         }
333         if (env == NULL || *env == NULL)
334         {
335             gint length;
336 
337             length = envp ? g_strv_length(envp) : 0;
338             envp = g_renew(gchar *, envp, length + 2);
339             env = &envp[length];
340             env[1] = NULL;
341         }
342         else
343             g_free(*env);
344         *env = g_strdup_printf ("PWD=%s", dir);
345     }
346 #endif
347     ret = g_spawn_async(dir, argv, envp, G_SPAWN_SEARCH_PATH,
348                         child_setup, (gpointer)(gsize)getpgid(getppid()), NULL, error);
349     g_strfreev(argv);
350     g_strfreev(envp);
351     return ret;
352 }
353