/*
cdcd - Command Driven CD player
Copyright (C) 1998-99 Tony Arcieri
Copyright (C) 2001, 2002, 2003 Fabrice Bauzac
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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/*
TODO:
Cleaner completion (command-dependant, full, etc). So that it
approaches perfection.
*/
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <sys/ioctl.h>
#ifdef HAVE_SYS_PTYVAR_H
# include <sys/ptyvar.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif
#include "rlhist.h"
#include "cmdline.h"
#include "config.h"
#include "global.h"
#include "str.h"
#include "cdcd.h"
#include "interface.h"
void
init_cmdline (void)
{
using_history ();
rl_readline_name = PACKAGE;
rl_completer_quote_characters = "\"'`";
rl_basic_word_break_characters = " ";
/* What is this for? Seems to be unused, because it doesn't crash
even if we set it to NULL! -- fb. */
rl_basic_quote_characters = "\"'`";
}
char *
command_matcher (struct Command *c, const char *text, int state)
{
static int list_index, len;
if (!*text)
return NULL;
if (!state)
{
list_index = 0;
len = strlen (text);
}
for (; c[list_index].name; ++list_index)
if (!strncmp (c[list_index].name, text, len))
return strdup (c[list_index++].name);
return NULL;
}
/* Returns the screen width (useful for prettyprintf's word
break). */
int
get_width ()
{
int cols = 0;
if (isatty (0))
{
#ifdef TIOCGWINSZ
struct winsize size;
if (ioctl (0, TIOCGWINSZ, &size) != -1)
cols = size.ws_col;
#else
struct ttysize size;
if (ioctl (input_fd, TIOCGSIZE, &size) != -1)
cols = size.ts_cols;
#endif
}
if (cols == 0)
cols = 80;
return cols;
}
/* Read the string S; if it is in the form `[0-9]*:[0-9]*', set RET
accordingly and return 0; otherwise, don't do anything and return
1. Note: the empty string (e.g. the first number in ":15", the
second in "2:") is treated as 0 (strtol's default behaviour). */
int
read_tv (char *s, struct disc_timeval *ret)
{
char *t;
int min, sec;
min = strtol (s, &t, 10);
if (*t != ':')
return 1;
sec = strtol (t + 1, &t, 10);
if (*t)
return 1;
ret->frames = 0;
ret->minutes = min;
ret->seconds = sec;
return 0;
}
/* Read the string S; if it is of the form "[0-9]*", set RET
accordingly and return 0; otherwise, return 1. */
int
read_int (char *s, int *ret)
{
char *t;
int i;
i = strtol (s, &t, 10);
if (*t)
return 1;
*ret = i;
return 0;
}
/*
Read the string S.
If it is a trackname, then set RET to the track number and return
0.
If it is part of one (and only one) trackname (strcasestr), then
set RET to the track number and return 0.
If several tracknames match, or if none do, return 1.
*/
int
read_trackname (char *s, int *ret,
struct disc_data *data, struct disc_info *disc)
{
int i, j;
/* Search for exact matches */
for (i = 0; i < disc->disc_total_tracks; ++i)
if (!strcasecmp (data->data_track[i].track_name, s))
{
*ret = i;
return 0; /* No check for a second exact match */
}
/* No exact match found, search for inexact ones */
for (i = 0, j = -1; i < disc->disc_total_tracks; ++i)
if (strcasestr (data->data_track[i].track_name, s))
{
if (j >= 0) /* This is the second match we find */
return 1;
j = i; /* This is the first match we find */
}
if (j >= 0)
{ /* We found something */
*ret = j;
return 0;
}
return 1;
}
/* If S is of the form url=SOMETHING (case insensitivity on `url'),
then sets *ret to a newly-allocated string containing SOMETHING and
return 0. Otherwise, return 1. */
int
read_url (char *s, char **ret)
{
char urleq[5];
strncpy (urleq, s, 4);
urleq[4] = 0;
if (strcasecmp (urleq, "url="))
return 1;
*ret = strdup (s + 4);
return 0;
}
/* If S is "CDDB" or "CDI", set *RET to the corresponding type (see
cmdline.h) and return 0; otherwise return 1. */
int
read_type (char *s, int *ret)
{
if (!strcasecmp (s, "CDI"))
{
*ret = TYPE_CDI;
return 0;
}
else if (!strcasecmp (s, "CDDB"))
{
*ret = TYPE_CDDB;
return 0;
}
return 1;
}
int
read_access (char *s, int *ret, char **prox)
{
if (!strcmp (s, "local"))
{
*ret = ACC_LOCAL;
*prox = NULL;
return 0;
}
else if (!strcmp (s, "remote"))
{
*ret = ACC_REMOTE;
*prox = NULL;
return 0;
}
else if (!strncmp (s, "proxy=", 6))
{
*ret = ACC_PROXY;
*prox = strdup (s + 6);
return 0;
}
return 1;
}
/* Converts the time in a struct disc_timeval to the easier
seconds-only representation. */
int
dt2sec (struct disc_timeval *dt)
{
return 60 * dt->minutes + dt->seconds;
}
/* Returns a newly allocated string containing cddb_host information
in the format: `CCCC cccc://wwwwwwww:ppp/ddddddddd' */
/* TODO: unused yet */
char *
cddb_host_prettystr (struct cddb_host *h)
{
char *r;
r = (char *) xmalloc (8 + /* "CDDB " */
5 + /* "cddbp" */
3 + /* "://" */
strlen (h->host_server.server_name) + 1 + /* ":" */
5 + /* IP port number */
1 + /* "/" */
strlen (h->host_addressing) + 1); /* NULL */
sprintf (r, "%-8s%s://%s:%d/%s",
h->host_protocol == CDINDEX_MODE_HTTP ? "CDI" : "CDDB",
h->host_protocol ? "http" : "cddbp",
h->host_server.server_name,
h->host_server.server_port, h->host_addressing);
r = (char *) xrealloc (r, strlen (r) + 1);
return r;
}
char *
cddb_serverlist_prettystr (struct cddb_serverlist *s)
{
int i;
char *ret;
const char *head = "Server list:\n" "------------\n";
const char *empty = "Empty list\n";
ret = (char *) xmalloc (strlen (head) + 1);
strcpy (ret, head);
if (!s->list_len)
{
ret = (char *) xrealloc (ret, strlen (ret) + strlen (empty) + 1);
strcat (ret, empty);
return ret;
}
for (i = 0; i < s->list_len; ++i)
{
char *t;
t = cddb_host_prettystr (s->list_host + i);
ret = (char *) xrealloc (ret, strlen (ret) + 4 /* "%2d: " */
+ strlen (t) + 1 /* "\n" */
+ 1); /* NULL */
sprintf (strchr (ret, 0), "%2d: %s\n", i + 1, t); /* sprintf at the
end of ret */
free (t);
}
return ret;
}
/* Comparison function for qsort(), in order to always have a sorted
command list. */
int
commandcmp (struct Command *c1, struct Command *c2)
{
return strcmp (c1->name, c2->name);
}
/* This is called at the beginning, in cmd_*.c:init_*(). */
void
sort_commands (struct Command *com)
{
int count;
for (count = 0; com[count].name; count++);
qsort (com, count, sizeof (*com),
(int (*)(const void *, const void *)) commandcmp);
}
/* Recursive command finder (in the case of an alias) */
struct Command *
find_command (char *name, struct Command *com)
{
int i;
struct Command *f = NULL;
for (i = 0; com[i].name; ++i)
if (!strcmp (name, com[i].name))
f = com + i;
if (!f)
return NULL;
else if (f->alias)
return find_command (f->alias, com);
else
return f;
}
/* Produce a malloc'ed string nicely representing an array of strings.
ARRAY is a NULL-terminated string array.
For example, with {"1","2","3","4"}: "1, 2, 3, and 4."
*/
/* FIXME: It's not a good idea to be so dependant of the language.
Maybe we'll change this to a more universal representation of lists
("cmd cmd [...] cmd"). */
static char *
array_to_string (const char *array[])
{
char *ret;
int i;
int len, capacity;
ret = (char *) xmalloc (capacity = 50);
ret[len = 0] = 0;
for (i = 0; array[i]; ++i)
{
int comma, and, add;
/* Boolean variables: do we want a comma? Do we want an
"and"? */
comma = and = 0;
if (i == 0) /* the first one */
;
else if (array[i + 1]) /* not the last one */
comma = 1;
else /* the last one */
and = 1;
/* Amount of text to add (and thus to allocate). */
add = (comma ? 2 : 0) + (and ? 6 : 0) + strlen (array[i]);
if (len + add + 1 > capacity)
ret = (char *) xrealloc (ret, capacity = 2 * (len + add + 1));
if (and)
{
strcpy (ret + len, ", and ");
len += 6;
}
if (comma)
{
strcpy (ret + len, ", ");
len += 2;
}
strcpy (ret + len, array[i]);
len += strlen (array[i]);
}
return ret;
}
/* If COMMANDLINE is 0, then print every command, even `help', `!' and
the like, which normally don't make sense when saying "cdcd
--help" from the shell interpreter. */
void
print_all_commands (struct Command *com, int commandline)
{
char *list;
{
int i;
const char **a;
int len, capacity;
a = (const char **) xmalloc ((capacity = 30) * sizeof *a);
a[len = 0] = NULL;
for (i = 0; com[i].name; ++i)
{
if (commandline && !com[i].display_cmdline)
;
else
{
if (len + 1 + 1 > capacity)
a = (const char **) xrealloc (a,
(capacity = 2 *
(len + 1 + 1)) * sizeof *a);
a[len++] = com[i].name;
}
}
a[len] = NULL;
list = array_to_string (a);
xfree (a);
}
{
char *p, *string;
const char *prefix = "Commands: ";
const char *suffix = ".";
const int prefix_len = strlen (prefix);
const int suffix_len = strlen (suffix);
const int list_len = strlen (list);
p = string = (char *) xmalloc (prefix_len + suffix_len + list_len + 1);
strcpy (p, prefix);
p += prefix_len;
strcpy (p, list);
p += list_len;
strcpy (p, suffix);
p += suffix_len;
pprintf (get_width () - 1, "%s", string);
putchar ('\n');
xfree (string);
}
xfree (list);
if (!commandline)
puts ("For more specific help, type `help <command>'.");
}
/* Print the help message corresponding to the command in ARGV,
looking for the help strings in COM. If COMMANDLINE is nonzero,
then assume the help function was called from the commandline
(shell) interface and don't print useless information. */
void
cmdhelp (struct Command *com, char **argv, int commandline)
{
if (!argv[1])
print_all_commands (com, 0);
else
{
struct Command *cmd;
cmd = find_command (argv[1], com);
if (!cmd)
printf ("No help available on `%s'.\n", argv[1]);
else
{
if (!argv[2])
{
if (cmd->usage)
printf ("Usage: %s %s\n", argv[1], cmd->usage);
if (cmd->description)
{
pprintf (get_width () - 1, "%s", cmd->description);
if (!commandline)
putchar ('\n'); /* prettyprintf suppresses all '\n'
as whitespace */
}
}
}
}
}
/* Perhaps the following function isn't useful. */
/* If S begins with '! ' (exclamation, then space), then put a \ in
front of '!' in order to make csh-expansion OK. */
void
protect_first_excl (char **s)
{
if (s[0][0] == '!' && s[0][1] == ' ')
{
char *new;
new = (char *) xmalloc (strlen (*s) + 2);
*new = '\\';
strcpy (new + 1, *s);
xfree (*s);
*s = new;
}
}
/* Universal text input */
void
get_input_text (char *buffer, char *prompt, int len)
{
char *inputbuffer;
inputbuffer = readline (prompt);
if (!inputbuffer)
{
fputs ("exit\n", stdout);
exit (0);
}
{
char *temp;
#if 0
protect_first_excl (&inputbuffer); /* TODO: see if this is useful */
#endif
if (history_expand (inputbuffer, &temp) < 0)
{ /* csh expansions
magic */
fputs (temp, stdout);
inputbuffer[0] = 0;
}
xfree (inputbuffer);
inputbuffer = temp;
}
if (inputbuffer[0]) /* TODO: don't add_history everything. e.g. type
"verbose" RET "on" RET: the "on" shouldn't be
in the history */
add_history (inputbuffer);
strncpy (buffer, inputbuffer, len);
buffer[len - 1] = '\0';
free (inputbuffer);
}
char *
trackname_matcher (const char *text, int state)
{
static int list_index, len;
static enum
{
Q_DOUBLE = '\"',
Q_SIMPLE = '\'',
Q_REV = '`',
Q_NONE = Q_DOUBLE,
}
quote;
static const char *match_string;
struct disc_info disc;
struct disc_data data;
if (!*text)
return NULL;
cdcd_cd_stat (cd_desc, &disc);
lookup_now (cd_desc, &data);
if (!state)
{
list_index = 0;
len = strlen (text);
if (text[0] == '\"' || text[0] == '\'' || text[0] == '`')
{
match_string = text + 1;
quote = text[0];
len--;
}
else
{
match_string = text;
quote = Q_NONE;
}
}
while (list_index < disc.disc_total_tracks)
if (!strncasecmp (data.data_track[list_index++].track_name,
match_string, len))
return quote_armor (data.data_track[list_index - 1].track_name, quote);
return NULL;
}
char **
cdcd_completion (char *text, int start, int end)
{
char **matches = NULL;
if (start > 0)
{
timestamp = 0;
timestamped_discid = 0;
matches = rl_completion_matches (text, trackname_matcher);
timestamp = 0;
timestamped_discid = 0;
}
return matches;
}
/* Warning: ARGV must be terminated with a NULL pointer */
/* We choose this format because it is the one used by
history_tokenize() */
int
execute_command (char **argv, struct Command *com)
{
struct Command *c;
char **unq; /* unquoted arguments */
int r;
unq = unquote_params (argv);
if (!*unq)
{
freev0 (unq);
return 0;
}
c = find_command (unq[0], com);
if (!c)
{
printf ("Unknown command `%s'\n", unq[0]);
return 0;
}
r = c->cmd (unq);
freev0 (unq);
return r;
}
int
cmd_mainloop (rl_compentry_func_t * cmdmatcher,
char *prompt, struct Command *c)
{
char command[256];
rl_compentry_func_t *previous;
previous = rl_completion_entry_function;
rl_completion_entry_function = cmdmatcher;
while (1)
{
int r;
get_input_text (command, prompt, 256);
{
char **p;
p = my_tokenize (command);
r = execute_command (p, c);
freev0 (p);
}
switch (r)
{
case XRET_NULL: /* Continue in this shell. */
break;
case XRET_BACK: /* Go one level higher in the shells tree. */
rl_completion_entry_function = previous;
return XRET_NULL;
case XRET_QUIT: /* Quit. */
rl_completion_entry_function = previous;
return XRET_QUIT;
}
}
}