summaryrefslogtreecommitdiff
path: root/src/opener.c
blob: 822e332ef912e2ac1e94e0d477e26b8d086609be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/* opener.c — launch external programs with %f expansion. Detached GSubprocess.
 */
#include "opener.h"

struct Opener {
   GPtrArray *p_progs; /* OpenerProg* */
};

static void
_prog_free(gpointer p) {
   OpenerProg *d = (OpenerProg *)p;
   if (d) {
      g_free(d->c_name);
      g_free(d->c_command);
      g_free(d);
   }
}

Opener *
opener_new(void) {
   Opener *o  = g_new0(Opener, 1);
   o->p_progs = g_ptr_array_new_with_free_func(_prog_free);
   return o;
}

void
opener_delete(Opener *o) {
   if (!o)
      return;
   g_ptr_array_unref(o->p_progs);
   g_free(o);
}

void
opener_set_progs(Opener *o, const GPtrArray *p) {
   g_return_if_fail(o);
   g_ptr_array_set_size(o->p_progs, 0);
   if (!p)
      return;
   for (guint i = 0; i < p->len; i++) {
      const OpenerProg *src = g_ptr_array_index((GPtrArray *)p, i);
      OpenerProg       *np  = g_new(OpenerProg, 1);
      np->c_name            = g_strdup(src->c_name);
      np->c_command         = g_strdup(src->c_command);
      g_ptr_array_add(o->p_progs, np);
   }
}

const GPtrArray *
opener_get_progs(Opener *o) {
   return o ? o->p_progs : NULL;
}

/* Replace all occurrences of c_old with c_new in c_str. Caller frees. */
static char *
_str_replace(const char *c_str, const char *c_old, const char *c_new) {
   if (c_str == NULL || c_old == NULL || c_new == NULL)
      return NULL;
   GString    *p_out     = g_string_new(NULL);
   const char *p         = c_str;
   gsize       u_old_len = strlen(c_old);
   while (*p) {
      if (strncmp(p, c_old, u_old_len) == 0) {
         g_string_append(p_out, c_new);
         p += u_old_len;
      } else {
         g_string_append_c(p_out, *p);
         p++;
      }
   }
   return g_string_free(p_out, FALSE);
}

/* Expand %f → the file path inside an already-parsed argv.
 *
 * The command template is parsed with g_shell_parse_argv FIRST, so quotes,
 * escapes, and option flags are resolved correctly; %f is then substituted
 * into the parsed argv elements. This guarantees %f is always exactly one
 * argv value no matter what spaces or shell metacharacters the path contains
 * (substitution happens after shell parsing, never before).
 *
 * Returns a newly-allocated, NULL-terminated argv vector; caller frees with
 * g_strfreev. On a parse error returns NULL and sets p_err — never launches
 * malformed argv. Non-static (with the `_` prefix retained to mark it as
 * non-public) so tests/test_opener.c can verify the argv shape directly via
 * an extern prototype, without polluting the public opener.h. */
char **
_expand_command(const char *c_cmd, GFile *p_file, GError **p_err) {
   g_return_val_if_fail(c_cmd != NULL, NULL);
   g_return_val_if_fail(G_IS_FILE(p_file), NULL);
   char *c_path = g_file_get_path(p_file);
   if (c_path == NULL) {
      g_set_error(p_err, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                  "opener: file has no local path");
      return NULL;
   }
   /* Parse the raw template first: resolves quotes/escapes/options. */
   char **argv = NULL;
   if (!g_shell_parse_argv(c_cmd, NULL, &argv, p_err)) {
      g_free(c_path);
      return NULL;
   }
   /* Substitute %f into each parsed element so the path is always one argv
    * value, regardless of spaces or shell metacharacters it contains. */
   for (guint i = 0; argv[i]; i++) {
      if (g_str_equal(argv[i], "%f")) {
         g_free(argv[i]);
         argv[i] = g_strdup(c_path);
      } else if (strstr(argv[i], "%f")) {
         char *r = _str_replace(argv[i], "%f", c_path);
         if (r) {
            g_free(argv[i]);
            argv[i] = r;
         }
      }
   }
   g_free(c_path);
   return argv;
}

gboolean
opener_launch(Opener *o, GFile *p_file, const OpenerProg *p_prog,
              GError **p_err) {
   (void)o;
   g_return_val_if_fail(G_IS_FILE(p_file), FALSE);
   g_return_val_if_fail(p_prog, FALSE);
   char **argv = _expand_command(p_prog->c_command, p_file, p_err);
   if (argv == NULL)
      return FALSE;
   GSubprocess *p_sub = g_subprocess_newv((const char *const *)argv,
                                          G_SUBPROCESS_FLAGS_NONE, p_err);
   g_strfreev(argv);
   if (p_sub == NULL)
      return FALSE;
   /* Detached: don't wait. Unref immediately (process keeps running). */
   g_object_unref(p_sub);
   return TRUE;
}