mirror of
https://github.com/flatpak/flatpak.git
synced 2026-03-17 06:29:21 -04:00
This commit implements a "typo helper" for the install command, so that if you don't get the app ID exactly correct you're prompted with similarly named apps available in the remote that you can choose from. Essentially this allows you to do "flatpak install flathub devhelp" instead of "flatpak install flathub org.gnome.Devhelp". The choice is only made for you in two cases: 1. If it's an exact match and there's only one match, it is used as before this commit. 2. If the -y/--assume-yes option was used and there's only one match, it is used. Presumably scripts would always specify the full app ID, so this should only affect users on the command line who choose to use that option. In the future we may want to use the groundwork laid in this commit to add similar functionality to other commands, like perhaps remote-info and run. This is a partial fix for https://github.com/flatpak/flatpak/issues/1258 Closes: #2113 Approved by: matthiasclasen
533 lines
16 KiB
C
533 lines
16 KiB
C
/*
|
|
* Copyright © 2018 Red Hat, Inc
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Authors:
|
|
* Alexander Larsson <alexl@redhat.com>
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "flatpak-complete.h"
|
|
#include "flatpak-utils-private.h"
|
|
|
|
/* Uncomment to get debug traces in /tmp/flatpak-completion-debug.txt (nice
|
|
* to not have it interfere with stdout/stderr)
|
|
*/
|
|
#if 0
|
|
void
|
|
flatpak_completion_debug (const gchar *format, ...)
|
|
{
|
|
va_list var_args;
|
|
gchar *s;
|
|
static FILE *f = NULL;
|
|
|
|
va_start (var_args, format);
|
|
s = g_strdup_vprintf (format, var_args);
|
|
if (f == NULL)
|
|
f = fopen ("/tmp/flatpak-completion-debug.txt", "a+");
|
|
fprintf (f, "%s\n", s);
|
|
fflush (f);
|
|
g_free (s);
|
|
}
|
|
#else
|
|
void
|
|
flatpak_completion_debug (const gchar *format, ...)
|
|
{
|
|
}
|
|
#endif
|
|
|
|
static gboolean
|
|
is_word_separator (char c)
|
|
{
|
|
return g_ascii_isspace (c);
|
|
}
|
|
|
|
void
|
|
flatpak_complete_file (FlatpakCompletion *completion,
|
|
const char *file_type)
|
|
{
|
|
flatpak_completion_debug ("completing FILE");
|
|
g_print ("%s\n", file_type);
|
|
}
|
|
|
|
void
|
|
flatpak_complete_dir (FlatpakCompletion *completion)
|
|
{
|
|
flatpak_completion_debug ("completing DIR");
|
|
g_print ("%s\n", "__FLATPAK_DIR");
|
|
}
|
|
|
|
void
|
|
flatpak_complete_word (FlatpakCompletion *completion,
|
|
char *format, ...)
|
|
{
|
|
va_list args;
|
|
const char *rest;
|
|
const char *shell_cur;
|
|
const char *shell_cur_end;
|
|
g_autofree char *string = NULL;
|
|
|
|
g_return_if_fail (format != NULL);
|
|
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
|
va_start (args, format);
|
|
string = g_strdup_vprintf (format, args);
|
|
va_end (args);
|
|
#pragma GCC diagnostic pop
|
|
|
|
if (!g_str_has_prefix (string, completion->cur))
|
|
return;
|
|
|
|
shell_cur = completion->shell_cur ? completion->shell_cur : "";
|
|
|
|
rest = string + strlen (completion->cur);
|
|
|
|
shell_cur_end = shell_cur + strlen (shell_cur);
|
|
while (shell_cur_end > shell_cur &&
|
|
rest > string &&
|
|
shell_cur_end[-1] == rest[-1] &&
|
|
/* I'm not sure exactly what bash is doing here with =, but this seems to work... */
|
|
shell_cur_end[-1] != '=')
|
|
{
|
|
rest--;
|
|
shell_cur_end--;
|
|
}
|
|
|
|
flatpak_completion_debug ("completing word: '%s' (%s)", string, rest);
|
|
|
|
g_print ("%s\n", rest);
|
|
}
|
|
|
|
void
|
|
flatpak_complete_ref (FlatpakCompletion *completion,
|
|
OstreeRepo *repo)
|
|
{
|
|
g_autoptr(GHashTable) refs = NULL;
|
|
flatpak_completion_debug ("completing REF");
|
|
|
|
if (ostree_repo_list_refs (repo,
|
|
NULL,
|
|
&refs, NULL, NULL))
|
|
{
|
|
GHashTableIter hashiter;
|
|
gpointer hashkey, hashvalue;
|
|
|
|
g_hash_table_iter_init (&hashiter, refs);
|
|
while ((g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue)))
|
|
{
|
|
const char *ref = (const char *) hashkey;
|
|
if (!(g_str_has_prefix (ref, "runtime/") ||
|
|
g_str_has_prefix (ref, "app/")))
|
|
continue;
|
|
flatpak_complete_word (completion, "%s", ref);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int
|
|
find_current_element (const char *str)
|
|
{
|
|
int count = 0;
|
|
|
|
if (g_str_has_prefix (str, "app/"))
|
|
str += strlen ("app/");
|
|
else if (g_str_has_prefix (str, "runtime/"))
|
|
str += strlen ("runtime/");
|
|
|
|
while (str != NULL && count <= 3)
|
|
{
|
|
str = strchr (str, '/');
|
|
count++;
|
|
if (str != NULL)
|
|
str = str + 1;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
void
|
|
flatpak_complete_partial_ref (FlatpakCompletion *completion,
|
|
FlatpakKinds kinds,
|
|
const char *only_arch,
|
|
FlatpakDir *dir,
|
|
const char *remote)
|
|
{
|
|
FlatpakKinds matched_kinds;
|
|
const char *pref;
|
|
g_autofree char *id = NULL;
|
|
g_autofree char *arch = NULL;
|
|
g_autofree char *branch = NULL;
|
|
|
|
g_auto(GStrv) refs = NULL;
|
|
int element;
|
|
const char *cur_parts[4] = { NULL };
|
|
g_autoptr(GError) error = NULL;
|
|
int i;
|
|
|
|
pref = completion->cur;
|
|
element = find_current_element (pref);
|
|
|
|
flatpak_split_partial_ref_arg_novalidate (pref, kinds,
|
|
NULL, NULL,
|
|
&matched_kinds, &id, &arch, &branch);
|
|
|
|
cur_parts[1] = id;
|
|
cur_parts[2] = arch ? arch : "";
|
|
cur_parts[3] = branch ? branch : "";
|
|
|
|
if (remote)
|
|
{
|
|
refs = flatpak_dir_find_remote_refs (dir, completion->argv[1],
|
|
(element > 1) ? id : NULL,
|
|
(element > 3) ? branch : NULL,
|
|
NULL, /* default branch */
|
|
(element > 2) ? arch : only_arch,
|
|
NULL, /* default arch */
|
|
matched_kinds,
|
|
FIND_MATCHING_REFS_FLAGS_NONE,
|
|
NULL, &error);
|
|
}
|
|
else
|
|
{
|
|
refs = flatpak_dir_find_installed_refs (dir,
|
|
(element > 1) ? id : NULL,
|
|
(element > 3) ? branch : NULL,
|
|
(element > 2) ? arch : only_arch,
|
|
matched_kinds, &error);
|
|
}
|
|
if (refs == NULL)
|
|
flatpak_completion_debug ("find refs error: %s", error->message);
|
|
for (i = 0; refs != NULL && refs[i] != NULL; i++)
|
|
{
|
|
int j;
|
|
g_autoptr(GString) comp = NULL;
|
|
g_auto(GStrv) parts = flatpak_decompose_ref (refs[i], NULL);
|
|
if (parts == NULL)
|
|
continue;
|
|
|
|
if (!g_str_has_prefix (parts[element], cur_parts[element]))
|
|
continue;
|
|
|
|
if (flatpak_id_has_subref_suffix (parts[element]))
|
|
{
|
|
char *last_dot = strrchr (parts[element], '.');
|
|
|
|
if (last_dot == NULL)
|
|
continue; /* Shouldn't really happen */
|
|
|
|
/* Only complete to subrefs is fully matching real part.
|
|
* For example, only match org.foo.Bar.Sources for
|
|
* "org.foo.Bar", "org.foo.Bar." or "org.foo.Bar.S", but
|
|
* not for "org.foo" or other shorter prefixes.
|
|
*/
|
|
if (strncmp (parts[element], cur_parts[element], last_dot - parts[element] - 1) != 0)
|
|
continue;
|
|
}
|
|
|
|
comp = g_string_new (pref);
|
|
g_string_append (comp, parts[element] + strlen (cur_parts[element]));
|
|
|
|
/* Only complete on the last part if the user explicitly adds a / */
|
|
if (element >= 2)
|
|
{
|
|
for (j = element + 1; j < 4; j++)
|
|
{
|
|
g_string_append (comp, "/");
|
|
g_string_append (comp, parts[j]);
|
|
}
|
|
}
|
|
|
|
flatpak_complete_word (completion, "%s", comp->str);
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
switch_already_in_line (FlatpakCompletion *completion,
|
|
GOptionEntry *entry)
|
|
{
|
|
guint i = 0;
|
|
guint line_part_len = 0;
|
|
|
|
for (; i < completion->original_argc; ++i)
|
|
{
|
|
line_part_len = strlen (completion->original_argv[i]);
|
|
if (line_part_len > 2 &&
|
|
g_strcmp0 (&completion->original_argv[i][2], entry->long_name) == 0)
|
|
return TRUE;
|
|
|
|
if (line_part_len == 2 &&
|
|
completion->original_argv[i][1] == entry->short_name)
|
|
return TRUE;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
should_filter_out_option_from_completion (FlatpakCompletion *completion,
|
|
GOptionEntry *entry)
|
|
{
|
|
switch (entry->arg)
|
|
{
|
|
case G_OPTION_ARG_NONE:
|
|
case G_OPTION_ARG_STRING:
|
|
case G_OPTION_ARG_INT:
|
|
case G_OPTION_ARG_FILENAME:
|
|
case G_OPTION_ARG_DOUBLE:
|
|
case G_OPTION_ARG_INT64:
|
|
return switch_already_in_line (completion, entry);
|
|
|
|
default:
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
void
|
|
flatpak_complete_options (FlatpakCompletion *completion,
|
|
GOptionEntry *entries)
|
|
{
|
|
GOptionEntry *e = entries;
|
|
int i;
|
|
|
|
while (e->long_name != NULL)
|
|
{
|
|
if (e->arg_description)
|
|
{
|
|
g_autofree char *prefix = g_strdup_printf ("--%s=", e->long_name);
|
|
|
|
if (g_str_has_prefix (completion->cur, prefix))
|
|
{
|
|
if (strcmp (e->arg_description, "ARCH") == 0)
|
|
{
|
|
const char *arches[] = {"i386", "x86_64", "aarch64", "arm"};
|
|
for (i = 0; i < G_N_ELEMENTS (arches); i++)
|
|
flatpak_complete_word (completion, "%s%s ", prefix, arches[i]);
|
|
}
|
|
else if (strcmp (e->arg_description, "SHARE") == 0)
|
|
{
|
|
for (i = 0; flatpak_context_shares[i] != NULL; i++)
|
|
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_shares[i]);
|
|
}
|
|
else if (strcmp (e->arg_description, "DEVICE") == 0)
|
|
{
|
|
for (i = 0; flatpak_context_devices[i] != NULL; i++)
|
|
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_devices[i]);
|
|
}
|
|
else if (strcmp (e->arg_description, "FEATURE") == 0)
|
|
{
|
|
for (i = 0; flatpak_context_features[i] != NULL; i++)
|
|
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_features[i]);
|
|
}
|
|
else if (strcmp (e->arg_description, "SOCKET") == 0)
|
|
{
|
|
for (i = 0; flatpak_context_sockets[i] != NULL; i++)
|
|
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_sockets[i]);
|
|
}
|
|
else if (strcmp (e->arg_description, "FILE") == 0)
|
|
{
|
|
flatpak_complete_file (completion, "__FLATPAK_FILE");
|
|
}
|
|
else
|
|
flatpak_complete_word (completion, "%s", prefix);
|
|
}
|
|
else
|
|
flatpak_complete_word (completion, "%s", prefix);
|
|
}
|
|
else
|
|
{
|
|
/* If this is just a switch, then don't add it multiple
|
|
* times */
|
|
if (!should_filter_out_option_from_completion (completion, e))
|
|
{
|
|
flatpak_complete_word (completion, "--%s ", e->long_name);
|
|
}
|
|
else
|
|
{
|
|
flatpak_completion_debug ("switch --%s is already in line %s", e->long_name, completion->line);
|
|
}
|
|
}
|
|
|
|
/* We may end up checking switch_already_in_line twice, but this is
|
|
* for simplicity's sake - the alternative solution would be to
|
|
* continue the loop early and have to increment e. */
|
|
if (e->short_name != 0)
|
|
{
|
|
/* This is a switch, we may not want to add it */
|
|
if (!e->arg_description)
|
|
{
|
|
if (!should_filter_out_option_from_completion (completion, e))
|
|
{
|
|
flatpak_complete_word (completion, "-%c ", e->short_name);
|
|
}
|
|
else
|
|
{
|
|
flatpak_completion_debug ("switch -%c is already in line %s", e->short_name, completion->line);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
flatpak_complete_word (completion, "-%c ", e->short_name);
|
|
}
|
|
}
|
|
e++;
|
|
}
|
|
}
|
|
|
|
void
|
|
flatpak_complete_context (FlatpakCompletion *completion)
|
|
{
|
|
flatpak_complete_options (completion, flatpak_context_get_option_entries ());
|
|
}
|
|
|
|
static gchar *
|
|
pick_word_at (const char *s,
|
|
int cursor,
|
|
int *out_word_begins_at)
|
|
{
|
|
int begin, end;
|
|
|
|
if (s[0] == '\0')
|
|
{
|
|
if (out_word_begins_at != NULL)
|
|
*out_word_begins_at = -1;
|
|
return NULL;
|
|
}
|
|
|
|
if (is_word_separator (s[cursor]) && ((cursor > 0 && is_word_separator (s[cursor - 1])) || cursor == 0))
|
|
{
|
|
if (out_word_begins_at != NULL)
|
|
*out_word_begins_at = cursor;
|
|
return g_strdup ("");
|
|
}
|
|
|
|
while (!is_word_separator (s[cursor - 1]) && cursor > 0)
|
|
cursor--;
|
|
begin = cursor;
|
|
|
|
end = begin;
|
|
while (!is_word_separator (s[end]) && s[end] != '\0')
|
|
end++;
|
|
|
|
if (out_word_begins_at != NULL)
|
|
*out_word_begins_at = begin;
|
|
|
|
return g_strndup (s + begin, end - begin);
|
|
}
|
|
|
|
static gboolean
|
|
parse_completion_line_to_argv (const char *initial_completion_line,
|
|
FlatpakCompletion *completion)
|
|
{
|
|
gboolean parse_result = g_shell_parse_argv (initial_completion_line,
|
|
&completion->original_argc,
|
|
&completion->original_argv,
|
|
NULL);
|
|
|
|
/* Make a shallow copy of argv, which will be our "working set" */
|
|
completion->argc = completion->original_argc;
|
|
completion->argv = g_memdup (completion->original_argv,
|
|
sizeof (gchar *) * (completion->original_argc + 1));
|
|
|
|
return parse_result;
|
|
}
|
|
|
|
FlatpakCompletion *
|
|
flatpak_completion_new (const char *arg_line,
|
|
const char *arg_point,
|
|
const char *arg_cur)
|
|
{
|
|
FlatpakCompletion *completion;
|
|
g_autofree char *initial_completion_line = NULL;
|
|
int _point;
|
|
char *endp;
|
|
int cur_begin;
|
|
int i;
|
|
|
|
_point = strtol (arg_point, &endp, 10);
|
|
if (endp == arg_point || *endp != '\0')
|
|
return NULL;
|
|
|
|
completion = g_new0 (FlatpakCompletion, 1);
|
|
completion->line = g_strdup (arg_line);
|
|
completion->shell_cur = g_strdup (arg_cur);
|
|
completion->point = _point;
|
|
|
|
flatpak_completion_debug ("========================================");
|
|
flatpak_completion_debug ("completion_point=%d", completion->point);
|
|
flatpak_completion_debug ("completion_shell_cur='%s'", completion->shell_cur);
|
|
flatpak_completion_debug ("----");
|
|
flatpak_completion_debug (" 0123456789012345678901234567890123456789012345678901234567890123456789");
|
|
flatpak_completion_debug ("'%s'", completion->line);
|
|
flatpak_completion_debug (" %*s^", completion->point, "");
|
|
|
|
/* compute cur and prev */
|
|
completion->prev = NULL;
|
|
completion->cur = pick_word_at (completion->line, completion->point, &cur_begin);
|
|
if (cur_begin > 0)
|
|
{
|
|
gint prev_end;
|
|
for (prev_end = cur_begin - 1; prev_end >= 0; prev_end--)
|
|
{
|
|
if (!is_word_separator (completion->line[prev_end]))
|
|
{
|
|
completion->prev = pick_word_at (completion->line, prev_end, NULL);
|
|
break;
|
|
}
|
|
}
|
|
|
|
initial_completion_line = g_strndup (completion->line, cur_begin);
|
|
}
|
|
else
|
|
initial_completion_line = g_strdup ("");
|
|
|
|
flatpak_completion_debug ("'%s'", initial_completion_line);
|
|
flatpak_completion_debug ("----");
|
|
|
|
flatpak_completion_debug (" cur='%s'", completion->cur);
|
|
flatpak_completion_debug ("prev='%s'", completion->prev);
|
|
|
|
if (!parse_completion_line_to_argv (initial_completion_line,
|
|
completion))
|
|
{
|
|
/* it's very possible the command line can't be parsed (for
|
|
* example, missing quotes etc) - in that case, we just
|
|
* don't autocomplete at all
|
|
*/
|
|
flatpak_completion_free (completion);
|
|
return NULL;
|
|
}
|
|
|
|
flatpak_completion_debug ("completion_argv %i:", completion->original_argc);
|
|
for (i = 0; i < completion->original_argc; i++)
|
|
flatpak_completion_debug (completion->original_argv[i]);
|
|
|
|
flatpak_completion_debug ("----");
|
|
|
|
return completion;
|
|
}
|
|
|
|
void
|
|
flatpak_completion_free (FlatpakCompletion *completion)
|
|
{
|
|
g_free (completion->cur);
|
|
g_free (completion->prev);
|
|
g_free (completion->line);
|
|
g_free (completion->argv);
|
|
g_strfreev (completion->original_argv);
|
|
g_free (completion);
|
|
}
|