summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlos Garnacho <carlosg@gnome.org>2021-08-12 15:30:16 +0200
committerCarlos Garnacho <carlosg@gnome.org>2023-05-14 00:30:57 +0200
commit688b1600a65e77aad273800f705281e9ed25e648 (patch)
tree5049ba0bd8163cf0bd092f9d07972332b481fd1c
parent8eeaa1595d9e7c9d280e48292cacc2ff477c6ca7 (diff)
downloadtracker-wip/carlosg/sparql-shell.tar.gz
cli: Add "shell" subcommandwip/carlosg/sparql-shell
As the command says, this creates a "shell" connecting to a given SPARQL endpoint (by default, in-memory DB), so that multiple queries can be executed on it. This shell is rather basic, allowing to either quit (via Ctrl-Q or 3 Ctrl-C presses), or execute the given SPARQL query (via Ctrl-X). When displaying a cursor content, output is redirected to a pager (by default "less") for display purposes, returning back to the SPARQL shell after quitting. It also has some rudimentary history support, allowing the use of PgUp and PgDown to navigate through the prior command list. This list only holds up for the running session though and is not saved in disk. The editor handles multiline and text that is larger than the available screen size, and handles the minimal set of keys that is expected for navigation. When dealing with errors, the full error message is displayed, and the editor cursor is moved to the location of the syntax error (if any, and available).
-rw-r--r--docs/manpages/meson.build1
-rw-r--r--docs/manpages/tracker3-shell.1.txt49
-rw-r--r--src/tracker/meson.build1
-rw-r--r--src/tracker/tracker-main.c2
-rw-r--r--src/tracker/tracker-shell.c1125
-rw-r--r--src/tracker/tracker-shell.h28
6 files changed, 1206 insertions, 0 deletions
diff --git a/docs/manpages/meson.build b/docs/manpages/meson.build
index babc3a699..6a940b8c8 100644
--- a/docs/manpages/meson.build
+++ b/docs/manpages/meson.build
@@ -2,6 +2,7 @@ manpages = [
['tracker3-endpoint', 1, true],
['tracker3-export', 1, true],
['tracker3-import', 1, true],
+ ['tracker3-shell', 1, true],
['tracker3-sparql', 1, true],
['tracker3-sql', 1, true],
['tracker-xdg-portal-3', 1, false],
diff --git a/docs/manpages/tracker3-shell.1.txt b/docs/manpages/tracker3-shell.1.txt
new file mode 100644
index 000000000..2b81c0a1d
--- /dev/null
+++ b/docs/manpages/tracker3-shell.1.txt
@@ -0,0 +1,49 @@
+tracker3-shell(1)
+=================
+
+== NAME
+
+tracker3-shell - Open a testing session to SPARQL endpoints.
+
+== SYNOPSIS
+
+....
+tracker3 shell
+tracker3 shell -d <directory>
+tracker3 shell -b <busname>
+tracker3 shell -r <httpservice>
+....
+
+== DESCRIPTION
+
+This command creates a shell to issue SPARQL commands on a consistent
+connection. If called with no arguments, an in-memory database using
+the Nepomuk ontology will be used. If provided a specific database
+location, this SPARQL endpoint will be used for queries.
+
+== OPTIONS
+
+*-d, --database=<__directory__>*::
+ Open a database _directory_.
+*-b, --dbus-service=<__busname__>*::
+ Connect to a SPARQL endpoint at a D-Bus name.
+*-r, --remote-service=<__httpservice__>*::
+ Connect to a SPARQL endpoint at a HTTP server.
+
+== EXAMPLES
+
+Connect to tracker-miner-fs-3 bus name::
++
+----
+$ tracker3 shell -b org.freedesktop.Tracker3.Miner.Files
+----
+
+Connect to Wikidata SPARQL service::
++
+----
+$ tracker3 shell -r https://query.wikidata.org/sparql
+----
+
+== SEE ALSO
+
+*tracker3-sparql*(1).
diff --git a/src/tracker/meson.build b/src/tracker/meson.build
index caaa4b3a3..3f64f0180 100644
--- a/src/tracker/meson.build
+++ b/src/tracker/meson.build
@@ -3,6 +3,7 @@ modules = [
'export',
'help',
'import',
+ 'shell',
'sparql',
'sql',
]
diff --git a/src/tracker/tracker-main.c b/src/tracker/tracker-main.c
index 138346637..12f078c0f 100644
--- a/src/tracker/tracker-main.c
+++ b/src/tracker/tracker-main.c
@@ -32,6 +32,7 @@
#include "tracker-export.h"
#include "tracker-help.h"
#include "tracker-import.h"
+#include "tracker-shell.h"
#include "tracker-sparql.h"
#include "tracker-sql.h"
@@ -90,6 +91,7 @@ static struct cmd_struct commands[] = {
{ "endpoint", tracker_endpoint, NEED_NOTHING, N_("Create a SPARQL endpoint") },
{ "export", tracker_export, NEED_WORK_TREE, N_("Export data from a Tracker database") },
{ "import", tracker_import, NEED_WORK_TREE, N_("Import data into a Tracker database") },
+ { "shell", tracker_shell, NEED_NOTHING, N_("Interactive SPARQL shell") },
{ "sparql", tracker_sparql, NEED_WORK_TREE, N_("Query and update the index using SPARQL or search, list and tree the ontology") },
{ "sql", tracker_sql, NEED_WORK_TREE, N_("Query the database at the lowest level using SQL") },
};
diff --git a/src/tracker/tracker-shell.c b/src/tracker/tracker-shell.c
new file mode 100644
index 000000000..d93fd00b2
--- /dev/null
+++ b/src/tracker/tracker-shell.c
@@ -0,0 +1,1125 @@
+/*
+ * Copyright (C) 2021, Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ *
+ * Author: Carlos Garnacho <carlosg@gnome.org>
+ */
+
+/* This code is pretty much untestable */
+//LCOV_EXCL_START
+
+#include "config.h"
+
+#include <sys/param.h>
+#include <stdlib.h>
+#include <time.h>
+#include <locale.h>
+
+#include <termios.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+
+#include <libtracker-sparql/tracker-sparql.h>
+#include <libtracker-common/tracker-common.h>
+
+#include "tracker-sparql.h"
+#include "tracker-color.h"
+
+enum {
+ DIRECTION_UP,
+ DIRECTION_DOWN,
+ DIRECTION_LEFT,
+ DIRECTION_RIGHT,
+};
+
+typedef struct
+{
+ GPtrArray *lines; /* Array of GString */
+ guint line, col;
+} EditorState;
+
+static struct termios original_termios = { 0 };
+static int ctrl_c_counter = 0;
+static EditorState *staging = NULL; /* The last new buffer */
+static EditorState *state = NULL;
+static GList *history = NULL;
+
+#define CTRL_KEYCOMBO(c) ((c) & 0x1F)
+#define KEY_ENTER 13
+#define KEY_BACKSPACE 127
+#define ESCAPE_SEQUENCE 27
+
+#define LINE_PADDING 3 /* Prompt, plus possible ellipsis (or spaces) on both sides */
+#define ROW_PADDING 1
+
+#define COL_MAX_LEN 100
+
+static gchar *database_path;
+static gchar *dbus_service;
+static gchar *remote_service;
+static const gchar *subtitle;
+
+static GOptionEntry entries[] = {
+ { "database", 'd', 0, G_OPTION_ARG_FILENAME, &database_path,
+ N_("Location of the database"),
+ N_("FILE")
+ },
+ { "dbus-service", 'b', 0, G_OPTION_ARG_STRING, &dbus_service,
+ N_("Connects to a DBus service"),
+ N_("DBus service name")
+ },
+ { "remote-service", 'r', 0, G_OPTION_ARG_STRING, &remote_service,
+ N_("Connects to a remote service"),
+ N_("Remote service URI")
+ },
+ { NULL }
+};
+
+#if 0
+static inline gchar
+read_char (void)
+{
+ gchar c;
+
+ if (read (STDIN_FILENO, &c, 1) != 1)
+ return 0;
+
+ return c;
+}
+#endif
+
+static inline gunichar
+read_char (void)
+{
+#define MAX_UTF8_CHAR_LEN 7
+ gchar c[MAX_UTF8_CHAR_LEN] = { 0, };
+ gunichar ch;
+ gint i = 0;
+
+ for (i = 0; i < MAX_UTF8_CHAR_LEN; i++) {
+ if (read (STDIN_FILENO, &c[i], 1) != 1)
+ return 0;
+
+ ch = g_utf8_get_char_validated ((gchar*) &c, -1);
+ if (ch == (gunichar) -1)
+ return 0;
+ else if (ch == (gunichar) -2)
+ continue;
+ else
+ return ch;
+ }
+
+ return 0;
+#undef MAX_UTF8_CHAR_LEN
+}
+
+static GString *
+string_array_add_line (GPtrArray *array,
+ gint idx)
+{
+ GString *str;
+
+ str = g_string_new ("");
+ g_ptr_array_insert (array, idx, str);
+
+ return str;
+}
+
+static EditorState *
+editor_state_new (void)
+{
+ EditorState *state = g_new0 (EditorState, 1);
+
+ state->lines = g_ptr_array_new ();
+ string_array_add_line (state->lines, -1);
+
+ return state;
+}
+
+static void
+editor_state_free (EditorState *state)
+{
+ guint i;
+
+ for (i = 0; i < state->lines->len; i++)
+ g_string_free (g_ptr_array_index (state->lines, i), TRUE);
+
+ g_ptr_array_unref (state->lines);
+ g_free (state);
+}
+
+static gchar *
+editor_state_to_string (EditorState *state)
+{
+ GString *str = g_string_new (NULL);
+ guint i;
+
+ for (i = 0; i < state->lines->len; i++) {
+ GString *line = g_ptr_array_index (state->lines, i);
+
+ g_string_append (str, line->str);
+ g_string_append_c (str, '\n');
+ }
+
+ return g_string_free (str, FALSE);
+}
+
+static glong
+char_to_offset (GString *str,
+ gint n_chars)
+{
+ const gchar *pos;
+
+ pos = g_utf8_offset_to_pointer (str->str, n_chars);
+
+ return pos - str->str;
+}
+
+static void
+editor_state_insert_char (EditorState *state,
+ gunichar ch)
+{
+ GString *str;
+ glong offset;
+
+ str = g_ptr_array_index (state->lines, state->line);
+ g_assert (str != NULL);
+ g_assert (state->col <= g_utf8_strlen (str->str, str->len));
+
+ offset = char_to_offset (str, state->col);
+ g_string_insert_unichar (str, offset, ch);
+ state->col++;
+}
+
+static void
+editor_state_handle_enter (EditorState *state)
+{
+ GString *str, *new;
+ glong offset;
+
+ str = g_ptr_array_index (state->lines, state->line);
+ g_assert (str != NULL);
+ g_assert (state->col <= g_utf8_strlen (str->str, str->len));
+
+ if (state->col == str->len) {
+ string_array_add_line (state->lines, state->line + 1);
+ } else {
+ new = string_array_add_line (state->lines, state->line + 1);
+ offset = char_to_offset (str, state->col);
+
+ g_string_append (new, &str->str[offset]);
+ g_string_erase (str, offset, -1);
+ }
+
+ state->line++;
+ state->col = 0;
+}
+
+static void
+editor_state_handle_delete (EditorState *state)
+{
+ GString *str, *prev;
+
+ str = g_ptr_array_index (state->lines, state->line);
+ g_assert (str != NULL);
+ g_assert (state->col <= g_utf8_strlen (str->str, str->len));
+
+ if (state->col == 0) {
+ if (state->line > 0) {
+ /* Merge with previous line */
+ prev = g_ptr_array_index (state->lines, state->line - 1);
+ g_ptr_array_remove_index (state->lines, state->line);
+ state->col = g_utf8_strlen (prev->str, prev->len);
+ state->line--;
+
+ g_string_append (prev, str->str);
+ g_string_free (str, TRUE);
+ }
+ } else {
+ glong prev, cur;
+
+ prev = char_to_offset (str, state->col - 1);
+ cur = char_to_offset (str, state->col);
+ g_string_erase (str, prev, cur - prev);
+ state->col--;
+ }
+}
+
+static void
+editor_state_handle_delete_forward (EditorState *state)
+{
+ GString *str, *next;
+
+ str = g_ptr_array_index (state->lines, state->line);
+ g_assert (str != NULL);
+ g_assert (state->col <= g_utf8_strlen (str->str, str->len));
+
+ if (state->col == g_utf8_strlen (str->str, str->len)) {
+ if (state->line < state->lines->len - 1) {
+ /* Merge with following line */
+ next = g_ptr_array_index (state->lines, state->line + 1);
+ g_ptr_array_remove_index (state->lines, state->line + 1);
+
+ g_string_append (str, next->str);
+ g_string_free (next, TRUE);
+ }
+ } else {
+ glong cur, next;
+
+ cur = char_to_offset (str, state->col);
+ next = char_to_offset (str, state->col + 1);
+ g_string_erase (str, cur, next - cur);
+ }
+}
+
+static void
+editor_state_handle_move (EditorState *state,
+ int direction)
+{
+ GString *str;
+
+ switch (direction) {
+ case DIRECTION_LEFT:
+ if (state->col == 0) {
+ if (state->line > 0) {
+ state->line--;
+ str = g_ptr_array_index (state->lines, state->line);
+ state->col = g_utf8_strlen (str->str, str->len);
+ }
+ } else {
+ state->col--;
+ }
+ break;
+ case DIRECTION_RIGHT:
+ str = g_ptr_array_index (state->lines, state->line);
+ if (state->col >= g_utf8_strlen (str->str, str->len)) {
+ if (state->line < state->lines->len - 1) {
+ state->line++;
+ state->col = 0;
+ }
+ } else {
+ state->col++;
+ }
+ break;
+ case DIRECTION_UP:
+ if (state->line > 0) {
+ state->line--;
+ str = g_ptr_array_index (state->lines, state->line);
+ state->col = MIN (state->col, g_utf8_strlen (str->str, str->len));
+ }
+ break;
+ case DIRECTION_DOWN:
+ if (state->line < state->lines->len - 1) {
+ state->line++;
+ str = g_ptr_array_index (state->lines, state->line);
+ state->col = MIN (state->col, g_utf8_strlen (str->str, str->len));
+ }
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static gboolean
+find_next_word_position (EditorState *state,
+ int direction,
+ guint *line,
+ guint *col)
+{
+ gint inc = 0, cur_line, cur_col, next_line, next_col;
+ GString *str;
+ gboolean found_nonspace = FALSE;
+ glong offset;
+
+ if (direction == DIRECTION_LEFT)
+ inc = -1;
+ else if (direction == DIRECTION_RIGHT)
+ inc = 1;
+ else
+ g_assert_not_reached ();
+
+ cur_line = next_line = state->line;
+ cur_col = next_col = state->col;
+
+ do {
+ str = g_ptr_array_index (state->lines, cur_line);
+
+ if (cur_col + inc < 0) {
+ if (cur_line == 0)
+ break;
+ next_line = cur_line - 1;
+ str = g_ptr_array_index (state->lines, next_line);
+ next_col = g_utf8_strlen (str->str, str->len);
+ } else if (cur_col + inc > (int) g_utf8_strlen (str->str, str->len)) {
+ if (cur_line == (int) state->lines->len - 1)
+ break;
+ next_line = cur_line + 1;
+ str = g_ptr_array_index (state->lines, next_line);
+ next_col = 0;
+ } else {
+ next_line = cur_line;
+ next_col = cur_col + inc;
+ }
+
+ offset = char_to_offset (str, next_col);
+
+ if (!g_unichar_isspace (g_utf8_get_char (&str->str[offset])) || str->len == 0)
+ found_nonspace = TRUE;
+ else if (found_nonspace)
+ break;
+
+ cur_line = next_line;
+ cur_col = next_col;
+ } while (next_line >= 0 && next_line < (int) state->lines->len);
+
+ if (next_line != (int) state->line ||
+ next_col != (int) state->col) {
+ if (inc > 0) {
+ *line = next_line;
+ *col = next_col;
+ } else {
+ *line = cur_line;
+ *col = cur_col;
+ }
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+editor_state_handle_delete_word (EditorState *state)
+{
+ GString *str, *prev;
+ guint line, col, i;
+
+ if (!find_next_word_position (state, DIRECTION_LEFT, &line, &col))
+ return;
+
+ str = g_ptr_array_index (state->lines, state->line);
+
+ if (line == state->line) {
+ glong old, new;
+
+ g_assert (col < state->col);
+ old = char_to_offset (str, state->col);
+ new = char_to_offset (str, col);
+ g_string_erase (str, new, old - new);
+ state->col = col;
+ } else {
+ glong offset;
+ gint len;
+
+ g_assert (line < state->line);
+
+ prev = g_ptr_array_index (state->lines, line);
+
+ /* Delete end of previous line and beginning of last */
+ g_string_erase (prev, col, -1);
+ offset = char_to_offset (str, state->col);
+ g_string_erase (str, 0, offset);
+
+ if (str->len == 0)
+ len = state->line - line;
+ else
+ len = state->line - line - 1;
+
+ /* Delete intermediate lines */
+ for (i = line + 1; i < state->line; i++) {
+ g_string_free (g_ptr_array_index (state->lines, i),
+ TRUE);
+ }
+
+ if (len > 0)
+ g_ptr_array_remove_range (state->lines, line + 1, len);
+
+ state->line = line;
+ state->col = col;
+ }
+}
+
+static void
+editor_state_handle_move_word (EditorState *state,
+ int direction)
+{
+ guint line, col;
+
+ if (direction == DIRECTION_LEFT || direction == DIRECTION_RIGHT) {
+ if (find_next_word_position (state, direction, &line, &col)) {
+ state->line = line;
+ state->col = col;
+ }
+ } else {
+ g_assert_not_reached ();
+ }
+}
+
+static void
+editor_state_handle_home (EditorState *state)
+{
+ state->col = 0;
+}
+
+static void
+editor_state_handle_end (EditorState *state)
+{
+ GString *str;
+
+ str = g_ptr_array_index (state->lines, state->line);
+ state->col = g_utf8_strlen (str->str, str->len);
+}
+
+static void
+print_line (EditorState *state,
+ guint line,
+ guint first_col,
+ guint cols)
+{
+ GString *str;
+
+ str = g_ptr_array_index (state->lines, line);
+
+ g_print ("»");
+
+ if (first_col != 0)
+ g_print ("…");
+ else
+ g_print (" ");
+
+ if (str->len - first_col < cols - LINE_PADDING) {
+ glong offset;
+
+ offset = char_to_offset (str, first_col);
+ g_print ("%s", &str->str[offset]);
+ } else {
+ gchar *truncated;
+
+ truncated = g_strndup (&str->str[first_col], cols - LINE_PADDING);
+ g_print ("%s", truncated);
+ g_free (truncated);
+ }
+
+ if (first_col + cols < str->len)
+ g_print ("…");
+}
+
+static guint
+calculate_dimensions (guint len,
+ guint available_size,
+ guint pos)
+{
+ gint first_elem;
+
+ if (len <= available_size ||
+ pos < available_size / 2)
+ first_elem = 0;
+ else if (pos > len - (available_size / 2))
+ first_elem = len - available_size;
+ else
+ first_elem = pos - (available_size / 2) - (available_size % 2);
+
+ first_elem = MAX (0, first_elem);
+
+ return first_elem;
+}
+
+static void
+get_viewport (EditorState *state,
+ guint rows,
+ guint cols,
+ guint *first_line,
+ guint *first_column)
+{
+ GString *str;
+
+ str = g_ptr_array_index (state->lines, state->line);
+ *first_line = calculate_dimensions (state->lines->len, rows - ROW_PADDING, state->line);
+ *first_column = calculate_dimensions (str->len, cols - LINE_PADDING, state->col);
+}
+
+static void
+editor_state_print (EditorState *state)
+{
+ guint rows, cols, i, first_col, first_line;
+
+ /* Hide cursor */
+ g_print ("\x1b[?25l");
+
+ /* Move to first line/col */
+ g_print ("\x1b[%d;%dH", 1, 1);
+
+ tracker_term_dimensions (&cols, &rows);
+ get_viewport (state, rows, cols, &first_line, &first_col);
+
+ for (i = 0; i < rows; i++) {
+ guint line = i + first_line;
+
+ /* Clear line */
+ g_print ("\x1b[K");
+
+ if (i == rows - 1) {
+ g_print ("Ctrl-Q to quit. Ctrl-X to execute SPARQL. PgUp/PgDown to navigate history");
+ break;
+ }
+
+ if (line < state->lines->len)
+ print_line (state, line, first_col, cols);
+
+ g_print ("\r\n");
+ }
+
+ /* Set cursor in position */
+ g_print ("\x1b[%d;%dH",
+ state->line + 1 - first_line,
+ state->col + 3 - first_col);
+
+ /* Show cursor again */
+ g_print ("\x1b[?25h");
+}
+
+static void
+editor_state_find_offset (EditorState *state,
+ guint64 offset)
+{
+ guint i;
+
+ for (i = 0; i < state->lines->len; i++) {
+ GString *str = g_ptr_array_index (state->lines, i);
+
+ if (offset < str->len) {
+ state->line = i;
+ state->col = offset;
+ }
+
+ offset -= str->len;
+ }
+}
+
+static void
+editor_history_move_up (void)
+{
+ GList *item;
+
+ item = g_list_find (history, state);
+ if (!item) {
+ staging = state;
+ state = history->data;
+ } else if (item->next) {
+ state = item->next->data;
+ }
+}
+
+static void
+editor_history_move_down (void)
+{
+ GList *item;
+
+ item = g_list_find (history, state);
+ if (!item)
+ return;
+
+ if (item->prev) {
+ state = item->prev->data;
+ } else if (item) {
+ state = staging;
+ staging = NULL;
+ }
+}
+
+static void
+editor_state_handle_key (EditorState *state,
+ gunichar key)
+{
+ if (key == CTRL_KEYCOMBO ('h') || key == KEY_BACKSPACE) {
+ editor_state_handle_delete (state);
+ } else if (key == CTRL_KEYCOMBO ('w')) {
+ editor_state_handle_delete_word (state);
+ } else if (key == KEY_ENTER) {
+ editor_state_handle_enter (state);
+ } else if (key == ESCAPE_SEQUENCE) {
+ gunichar ch;
+
+ switch ((ch = read_char ())) {
+ case '[':
+ switch ((ch = read_char ())) {
+ case 'A':
+ /* Up arrow */
+ editor_state_handle_move (state, DIRECTION_UP);
+ break;
+ case 'B':
+ /* Down arrow */
+ editor_state_handle_move (state, DIRECTION_DOWN);
+ break;
+ case 'C':
+ /* Right arrow */
+ editor_state_handle_move (state, DIRECTION_RIGHT);
+ break;
+ case 'D':
+ /* Left arrow */
+ editor_state_handle_move (state, DIRECTION_LEFT);
+ break;
+ case '1':
+ if (read_char () == ';' &&
+ read_char () == '5') {
+ switch ((ch = read_char ())) {
+ case 'C':
+ /* Ctrl + Right */
+ editor_state_handle_move_word (state, DIRECTION_RIGHT);
+ break;
+ case 'D':
+ /* Ctrl + Left */
+ editor_state_handle_move_word (state, DIRECTION_LEFT);
+ break;
+ default:
+ g_debug ("Escape sequence: '[1;5%c'", ch);
+ break;
+ }
+ }
+ break;
+ case '3':
+ if (read_char () == '~')
+ editor_state_handle_delete_forward (state);
+ break;
+ case '5':
+ if (read_char () == '~')
+ editor_history_move_up ();
+ break;
+ case '6':
+ if (read_char () == '~')
+ editor_history_move_down ();
+ break;
+ case 'H':
+ editor_state_handle_home (state);
+ break;
+ case 'F':
+ editor_state_handle_end (state);
+ break;
+ default:
+ g_debug ("Escape sequence: '[%c'", ch);
+ read_char ();
+ break;
+ }
+ break;
+ default:
+ g_debug ("Escape sequence: '%c'", ch);
+ break;
+ }
+ } else if (!g_ascii_iscntrl (key)) {
+ /* Visible characters */
+ editor_state_insert_char (state, key);
+ }
+}
+
+static void
+disable_raw_mode (void)
+{
+ tcsetattr (STDIN_FILENO, TCSAFLUSH, &original_termios);
+}
+
+static void
+enable_raw_mode (void)
+{
+ struct termios termios;
+
+ tcgetattr (STDIN_FILENO, &original_termios);
+ termios = original_termios;
+ termios.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
+ termios.c_iflag &= ~(ICRNL | IXON | INPCK | ISTRIP);
+ termios.c_oflag &= ~(OPOST);
+ termios.c_cflag |= CS8;
+ tcsetattr (STDIN_FILENO, TCSAFLUSH, &termios);
+}
+
+static void
+pad_string (GString *str,
+ guint len,
+ const gchar *ch)
+{
+ guint i;
+
+ for (i = 0; i < len; i++)
+ g_string_append (str, ch);
+}
+
+typedef struct {
+ gchar *prefix;
+ const gchar *shorthand;
+} ShorthandLookup;
+
+static void
+find_shorthand (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ ShorthandLookup *data = user_data;
+
+ if (g_strcmp0 ((const gchar *) value, data->prefix))
+ data->shorthand = key;
+}
+
+static gchar *
+get_uri_shorthand (TrackerNamespaceManager *namespaces,
+ const gchar *uri)
+{
+ ShorthandLookup data;
+ const gchar *loc;
+
+ loc = strstr (uri, "#");
+ if (!loc)
+ return NULL;
+
+ loc++;
+ data.prefix = g_strndup (uri, loc - uri);
+ tracker_namespace_manager_foreach (namespaces, find_shorthand, &data);
+ g_free (data.prefix);
+
+ if (!data.shorthand)
+ return NULL;
+
+ return g_strdup_printf ("%s:%s", data.shorthand, loc);
+}
+
+static gchar *
+limit_string_length (const gchar *str)
+{
+ if (g_utf8_strlen (str, -1) < COL_MAX_LEN)
+ return g_strdup (str);
+
+ return g_utf8_substring (str, 0, COL_MAX_LEN);
+}
+
+static gchar *
+format_column_output (TrackerSparqlCursor *cursor,
+ gint col)
+{
+ TrackerSparqlConnection *conn;
+ TrackerNamespaceManager *namespaces;
+ const gchar *col_str;
+ gchar *shortened = NULL, *limited;
+
+ col_str = tracker_sparql_cursor_get_string (cursor, col, NULL);
+ if (!col_str)
+ return NULL;
+
+ conn = tracker_sparql_cursor_get_connection (cursor);
+ namespaces = tracker_sparql_connection_get_namespace_manager (conn);
+
+ if (namespaces)
+ shortened = get_uri_shorthand (namespaces, col_str);
+
+ limited = limit_string_length (shortened ? shortened : col_str);
+ g_free (shortened);
+
+ return limited;
+}
+
+static void
+print_row (GStrv values,
+ gsize *lengths,
+ gint n_columns,
+ gchar *sep)
+{
+ GString *str;
+ gint i;
+
+ str = g_string_new (sep);
+
+ for (i = 0; i < n_columns; i++) {
+ g_string_append (str, values[i]);
+ pad_string (str, lengths[i] - g_utf8_strlen (values[i], -1), " ");
+ g_string_append (str, sep);
+ }
+
+ g_print ("%s\r\n", str->str);
+ g_string_free (str, TRUE);
+}
+
+static void
+print_decoration (gsize *lengths,
+ gint n_columns,
+ gchar *start,
+ gchar *mid,
+ gchar *end,
+ gchar *padding)
+{
+ GString *str;
+ gint i;
+
+ str = g_string_new (NULL);
+
+ for (i = 0; i < n_columns; i++) {
+ if (i == 0)
+ g_string_append (str, start);
+ else
+ g_string_append (str, mid);
+
+ pad_string (str, lengths[i], padding);
+ }
+
+ g_string_append (str, end);
+ g_print ("%s\r\n", str->str);
+ g_string_free (str, TRUE);
+}
+
+static void
+print_cursor (TrackerSparqlCursor *cursor)
+{
+ GPtrArray *results;
+ GStrv column_names = NULL, row = NULL;
+ gsize *lengths = NULL;
+ guint i, n_columns = 0;
+
+ results = g_ptr_array_new_with_free_func ((GDestroyNotify) g_strfreev);
+
+ while (tracker_sparql_cursor_next (cursor, NULL, NULL)) {
+ GStrv row = NULL;
+
+ n_columns = (guint) tracker_sparql_cursor_get_n_columns (cursor);
+
+ if (!column_names) {
+ column_names = g_new0 (gchar*, n_columns + 1);
+ lengths = g_new0 (size_t, n_columns + 1);
+
+ for (i = 0; i < n_columns; i++) {
+ column_names[i] = g_strdup (tracker_sparql_cursor_get_variable_name (cursor, i));
+ lengths[i] = MAX (lengths[i], (gsize) g_utf8_strlen (column_names[i], -1));
+ }
+ }
+
+ row = g_new0 (gchar*, n_columns + 1);
+
+ for (i = 0; i < n_columns; i++) {
+ row[i] = format_column_output (cursor, i);
+ lengths[i] = MAX (lengths[i], (gsize) g_utf8_strlen (row[i], -1));
+ }
+
+ g_ptr_array_add (results, row);
+ }
+
+ print_decoration (lengths, n_columns, "┌", "┬", "┐", "─");
+ print_row (column_names, lengths, n_columns, "│");
+
+ for (i = 0; i < results->len; i++) {
+ if (i == 0)
+ print_decoration (lengths, n_columns, "├", "┼", "┤", "─");
+
+ row = g_ptr_array_index (results, i);
+ print_row (row, lengths, n_columns, "│");
+ }
+
+ print_decoration (lengths, n_columns, "└", "┴", "┘", "─");
+
+ g_ptr_array_unref (results);
+}
+
+static TrackerSparqlConnection *
+create_connection (GError **error)
+{
+ if (database_path && !dbus_service && !remote_service) {
+ GFile *file;
+
+ file = g_file_new_for_commandline_arg (database_path);
+ subtitle = g_file_peek_path (file);
+ return tracker_sparql_connection_new (TRACKER_SPARQL_CONNECTION_FLAGS_NONE,
+ file, NULL, NULL, error);
+ } else if (dbus_service && !database_path && !remote_service) {
+ GDBusConnection *dbus_conn;
+
+ dbus_conn = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error);
+ if (!dbus_conn)
+ return NULL;
+
+ subtitle = dbus_service;
+ return tracker_sparql_connection_bus_new (dbus_service, NULL, dbus_conn, error);
+ } else if (remote_service && !database_path && !dbus_service) {
+ subtitle = remote_service;
+ return tracker_sparql_connection_remote_new (remote_service);
+ } else {
+ TrackerSparqlConnection *conn;
+ GFile *ontology;
+
+ ontology = tracker_sparql_get_ontology_nepomuk ();
+ conn = tracker_sparql_connection_new (TRACKER_SPARQL_CONNECTION_FLAGS_NONE,
+ NULL,
+ ontology,
+ NULL,
+ NULL);
+ g_object_unref (ontology);
+
+ return conn;
+ }
+}
+
+int
+tracker_shell (int argc,
+ char *argv[])
+{
+ TrackerSparqlConnection *conn;
+ GOptionContext *context;
+ GError *error = NULL;
+ gunichar c;
+
+ context = g_option_context_new (NULL);
+ g_option_context_add_main_entries (context, entries, NULL);
+
+ argv[0] = "tracker shell";
+
+ if (!g_option_context_parse (context, &argc, (char***) &argv, &error)) {
+ g_printerr ("%s, %s\n", _("Unrecognized options"), error->message);
+ g_error_free (error);
+ g_option_context_free (context);
+ return EXIT_FAILURE;
+ }
+
+ g_option_context_free (context);
+
+
+ /* Check we are not redirected */
+ if (!tracker_term_is_tty ()) {
+ g_printerr ("Output must be a TTY");
+ return EXIT_FAILURE;
+ }
+
+ conn = create_connection (&error);
+ if (!conn) {
+ g_printerr ("%s: %s\n",
+ _("Could not establish a connection to Tracker"),
+ error ? error->message : _("No error given"));
+ g_clear_error (&error);
+ return EXIT_FAILURE;
+ }
+
+ /* Change terminal title */
+ g_print ("\033]0;SPARQL Shell%s%s\007",
+ subtitle ? ": " : "",
+ subtitle ? subtitle : "");
+
+ g_assert (conn != NULL);
+
+ enable_raw_mode ();
+ atexit (disable_raw_mode);
+
+ state = editor_state_new ();
+ editor_state_print (state);
+
+ while (TRUE) {
+ c = read_char ();
+
+ if (c == CTRL_KEYCOMBO ('q')) {
+ break;
+ } else if (c == CTRL_KEYCOMBO ('c')) {
+ ctrl_c_counter++;
+ if (ctrl_c_counter == 3)
+ break;
+
+ continue;
+ }
+
+ ctrl_c_counter = 0;
+
+ if (c == CTRL_KEYCOMBO ('x')) {
+ TrackerSparqlCursor *cursor;
+ gchar *sparql;
+ GError *error = NULL;
+
+ /* Execute the SPARQL query */
+
+ /* Erase screen */
+ g_print ("\x1b[2J\r");
+
+ /* Move to first line/col */
+ g_print ("\x1b[%d;%dH", 1, 1);
+ g_print ("\r");
+
+ sparql = editor_state_to_string (state);
+ cursor = tracker_sparql_connection_query (conn, sparql,
+ NULL, &error);
+ g_free (sparql);
+
+ if (cursor) {
+ /* Temporarily disable raw mode to redirect cursor to pager */
+ disable_raw_mode ();
+ tracker_term_pipe_to_pager (FALSE);
+ print_cursor (cursor);
+ tracker_term_pager_close ();
+ enable_raw_mode ();
+
+ g_object_unref (cursor);
+
+ /* Move current state last in history */
+ history = g_list_remove (history, state);
+ history = g_list_prepend (history, state);
+
+ g_clear_pointer (&staging, editor_state_free);
+ state = editor_state_new ();
+ editor_state_print (state);
+ } else {
+ gchar *error_str;
+ gchar **error_lines;
+
+ /* Look for our own syntax errors, and update editor
+ * cursor based on the error location.
+ */
+ if (g_str_has_prefix (error->message, "Parser error at byte")) {
+ guint64 offset;
+
+ if (sscanf (error->message,
+ "Parser error at byte %" G_GUINT64_FORMAT,
+ &offset) == 1) {
+ editor_state_find_offset (state, offset);
+ }
+ }
+
+ error_lines = g_strsplit (error->message, "\n", -1);
+ error_str = g_strjoinv ("\r\n", error_lines);
+ g_print ("%s\r\n", error_str);
+ g_error_free (error);
+ g_free (error_str);
+ g_strfreev (error_lines);
+
+ /* Temporarily hide the cursor while showing the error */
+ g_print ("\x1b[?25l");
+ }
+
+ continue;
+ }
+
+ editor_state_handle_key (state, c);
+ editor_state_print (state);
+ }
+
+ if (!g_list_find (history, state))
+ editor_state_free (state);
+
+ g_clear_pointer (&staging, editor_state_free);
+ g_list_free_full (history, (GDestroyNotify) editor_state_free);
+
+ tracker_sparql_connection_close (conn);
+
+ /* Erase screen */
+ g_print ("\x1b[2J\r");
+
+ /* Move to first line/col */
+ g_print ("\x1b[%d;%dH", 1, 1);
+ g_print ("\r");
+
+ return EXIT_SUCCESS;
+}
+
+//LCOV_EXCL_STOP
diff --git a/src/tracker/tracker-shell.h b/src/tracker/tracker-shell.h
new file mode 100644
index 000000000..d86481856
--- /dev/null
+++ b/src/tracker/tracker-shell.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021, Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ *
+ * Author: Carlos Garnacho <carlosg@gnome.org>
+ */
+
+#ifndef __TRACKER_SHELL_H__
+#define __TRACKER_SHELL_H__
+
+int tracker_shell (int argc,
+ const char **argv);
+
+#endif /* __TRACKER_SHELL_H__ */