mirror of
https://github.com/rkd77/elinks.git
synced 2024-11-04 08:17:17 -05:00
1385 lines
36 KiB
C
1385 lines
36 KiB
C
/* Config file manipulation */
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h> /* OS/2 needs this after sys/types.h */
|
|
#ifdef HAVE_FCNTL_H
|
|
#include <fcntl.h> /* OS/2 needs this after sys/types.h */
|
|
#endif
|
|
#ifdef HAVE_UNISTD_H
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "elinks.h"
|
|
|
|
#include "config/conf.h"
|
|
#include "config/dialogs.h"
|
|
#include "config/domain.h"
|
|
#include "config/home.h"
|
|
#include "config/kbdbind.h"
|
|
#include "config/options.h"
|
|
#include "config/opttypes.h"
|
|
#include "intl/libintl.h"
|
|
#include "osdep/osdep.h"
|
|
#include "terminal/terminal.h"
|
|
#include "util/error.h"
|
|
#include "util/memory.h"
|
|
#include "util/qs_parse/qs_parse.h"
|
|
#include "util/secsave.h"
|
|
#include "util/string.h"
|
|
|
|
|
|
/* Config file has only very simple grammar:
|
|
*
|
|
* /set *option *= *value/
|
|
* /bind *keymap *keystroke *= *action/
|
|
* /include *file/
|
|
* /#.*$/
|
|
*
|
|
* Where option consists from any number of categories separated by dots and
|
|
* name of the option itself. Both category and option name consists from
|
|
* [a-zA-Z0-9_-*] - using uppercase letters is not recommended, though. '*' is
|
|
* reserved and is used only as escape character in place of '.' originally in
|
|
* option name.
|
|
*
|
|
* Value can consist from:
|
|
* - number (it will be converted to int/long)
|
|
* - enum (like on, off; true, fake, last_url; etc ;) - in planning state yet
|
|
* - string - "blah blah" (keymap, keystroke and action and file looks like that too)
|
|
*
|
|
* "set" command is parsed first, and then type-specific function is called,
|
|
* with option as one parameter and value as a second. Usually it just assigns
|
|
* value to an option, but sometimes you may want to first create the option
|
|
* ;). Then this will come handy. */
|
|
|
|
struct conf_parsing_pos {
|
|
/** Points to the next character to be parsed from the
|
|
* configuration file. */
|
|
char *look;
|
|
|
|
/** The line number corresponding to #look. This is
|
|
* shown in error messages. */
|
|
int line;
|
|
};
|
|
|
|
struct conf_parsing_state {
|
|
/** This part may be copied to a local variable as a bookmark
|
|
* and restored later. So it must not contain any pointers
|
|
* that would have to be freed in that situation. */
|
|
struct conf_parsing_pos pos;
|
|
|
|
/** When ELinks is rewriting the configuration file, @c mirrored
|
|
* indicates the end of the part that has already been copied
|
|
* to the mirror string. Otherwise, @c mirrored is not used.
|
|
*
|
|
* @invariant @c mirrored @<= @c pos.look */
|
|
char *mirrored;
|
|
|
|
/** File name for error messages. If NULL then do not display
|
|
* error messages. */
|
|
const char *filename;
|
|
};
|
|
|
|
/** Tell the user about an error in the configuration file.
|
|
* @return @a err, for convenience. */
|
|
static enum parse_error
|
|
show_parse_error(const struct conf_parsing_state *state, enum parse_error err)
|
|
{
|
|
static const char error_msg[][40] = {
|
|
"no error", /* ERROR_NONE */
|
|
"unknown command", /* ERROR_COMMAND */
|
|
"parse error", /* ERROR_PARSE */
|
|
"unknown option", /* ERROR_OPTION */
|
|
"bad value", /* ERROR_VALUE */
|
|
"no memory left", /* ERROR_NOMEM */
|
|
};
|
|
|
|
if (state->filename) {
|
|
fprintf(stderr, "%s:%d: %s\n",
|
|
state->filename, state->pos.line, error_msg[err]);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/** Skip comments and whitespace. */
|
|
static void
|
|
skip_white(struct conf_parsing_pos *pos)
|
|
{
|
|
char *start = pos->look;
|
|
|
|
while (*start) {
|
|
while (isspace((unsigned char)*start)) {
|
|
if (*start == '\n') {
|
|
pos->line++;
|
|
}
|
|
start++;
|
|
}
|
|
|
|
if (*start == '#') {
|
|
start += strcspn(start, "\n");
|
|
} else {
|
|
pos->look = start;
|
|
return;
|
|
}
|
|
}
|
|
|
|
pos->look = start;
|
|
}
|
|
|
|
/** Skip a quoted string.
|
|
* This function allows "mismatching quotes' because str_rd() does so. */
|
|
static void
|
|
skip_quoted(struct conf_parsing_pos *pos)
|
|
{
|
|
assert(isquote(*pos->look));
|
|
if_assert_failed return;
|
|
pos->look++;
|
|
|
|
for (;;) {
|
|
if (!*pos->look)
|
|
return;
|
|
if (isquote(*pos->look)) {
|
|
pos->look++;
|
|
return;
|
|
}
|
|
if (*pos->look == '\\' && pos->look[1])
|
|
pos->look++;
|
|
if (*pos->look == '\n')
|
|
pos->line++;
|
|
pos->look++;
|
|
}
|
|
}
|
|
|
|
/** Skip the value of an option.
|
|
*
|
|
* This job is normally done by the reader function that corresponds
|
|
* to the type of the option. However, if ELinks does not recognize
|
|
* the name of the option, it cannot look up the type and has to use
|
|
* this function instead. */
|
|
static void
|
|
skip_option_value(struct conf_parsing_pos *pos)
|
|
{
|
|
if (isquote(*pos->look)) {
|
|
/* Looks like OPT_STRING, OPT_CODEPAGE, OPT_LANGUAGE,
|
|
* or OPT_COLOR. */
|
|
skip_quoted(pos);
|
|
} else {
|
|
/* Looks like OPT_BOOL, OPT_INT, or OPT_LONG. */
|
|
while (isasciialnum(*pos->look) || *pos->look == '.'
|
|
|| *pos->look == '+' || *pos->look == '-')
|
|
pos->look++;
|
|
}
|
|
}
|
|
|
|
/** Skip to the next newline or comment that is not part of a quoted
|
|
* string. When ELinks hits a parse error in the configuration file,
|
|
* it calls this in order to find the place where is should resume
|
|
* parsing. This is intended to prevent ELinks from treating words
|
|
* in strings as commands. */
|
|
static void
|
|
skip_to_unquoted_newline_or_comment(struct conf_parsing_pos *pos)
|
|
{
|
|
while (*pos->look && *pos->look != '#' && *pos->look != '\n') {
|
|
if (isquote(*pos->look))
|
|
skip_quoted(pos);
|
|
else
|
|
pos->look++;
|
|
}
|
|
}
|
|
|
|
/* Parse a command. Returns error code. */
|
|
/* If dynamic string credentials are supplied, we will mirror the command at
|
|
* the end of the string; however, we won't load the option value to the tree,
|
|
* and we will even write option value from the tree to the output string.
|
|
* We will only possibly set or clear OPT_MUST_SAVE flag in the option. */
|
|
|
|
static enum parse_error
|
|
parse_set_common(struct option *opt_tree, struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf, int want_domain)
|
|
{
|
|
const char *domain_orig = NULL;
|
|
size_t domain_len = 0;
|
|
char *domain_copy = NULL;
|
|
const char *optname_orig;
|
|
size_t optname_len;
|
|
char *optname_copy;
|
|
|
|
skip_white(&state->pos);
|
|
if (!*state->pos.look) return show_parse_error(state, ERROR_PARSE);
|
|
|
|
if (want_domain) {
|
|
domain_orig = state->pos.look;
|
|
while (isident(*state->pos.look) || *state->pos.look == '*'
|
|
|| *state->pos.look == '.' || *state->pos.look == '+')
|
|
state->pos.look++;
|
|
domain_len = state->pos.look - domain_orig;
|
|
|
|
skip_white(&state->pos);
|
|
}
|
|
|
|
/* Option name */
|
|
optname_orig = state->pos.look;
|
|
while (is_option_name_char(*state->pos.look)
|
|
|| *state->pos.look == '.')
|
|
state->pos.look++;
|
|
optname_len = state->pos.look - optname_orig;
|
|
|
|
skip_white(&state->pos);
|
|
|
|
/* Equal sign */
|
|
if (*state->pos.look != '=')
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
state->pos.look++; /* '=' */
|
|
skip_white(&state->pos);
|
|
if (!*state->pos.look)
|
|
return show_parse_error(state, ERROR_VALUE);
|
|
|
|
optname_copy = memacpy(optname_orig, optname_len);
|
|
if (!optname_copy) return show_parse_error(state, ERROR_NOMEM);
|
|
if (want_domain) {
|
|
domain_copy = memacpy(domain_orig, domain_len);
|
|
if (!domain_copy) {
|
|
mem_free(optname_copy);
|
|
return show_parse_error(state, ERROR_NOMEM);
|
|
}
|
|
}
|
|
|
|
/* Option value */
|
|
{
|
|
struct option *opt;
|
|
char *val;
|
|
const struct conf_parsing_pos pos_before_value = state->pos;
|
|
|
|
if (want_domain && *domain_copy) {
|
|
struct option *domain_tree;
|
|
|
|
domain_tree = get_domain_tree(domain_copy);
|
|
if (!domain_tree) {
|
|
mem_free(domain_copy);
|
|
mem_free(optname_copy);
|
|
skip_option_value(&state->pos);
|
|
return show_parse_error(state, ERROR_NOMEM);
|
|
}
|
|
|
|
if (mirror) {
|
|
opt = get_opt_rec_real(domain_tree,
|
|
optname_copy);
|
|
} else {
|
|
opt = get_opt_rec(opt_tree, optname_copy);
|
|
if (opt) {
|
|
opt = get_option_shadow(opt, opt_tree,
|
|
domain_tree);
|
|
if (!opt) {
|
|
mem_free(domain_copy);
|
|
mem_free(optname_copy);
|
|
skip_option_value(&state->pos);
|
|
return show_parse_error(state,
|
|
ERROR_NOMEM);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
opt = mirror
|
|
? get_opt_rec_real(opt_tree, optname_copy)
|
|
: get_opt_rec(opt_tree, optname_copy);
|
|
}
|
|
if (want_domain)
|
|
mem_free(domain_copy);
|
|
domain_copy = NULL;
|
|
mem_free(optname_copy);
|
|
optname_copy = NULL;
|
|
|
|
if (!opt || (opt->flags & OPT_HIDDEN)) {
|
|
show_parse_error(state, ERROR_OPTION);
|
|
skip_option_value(&state->pos);
|
|
return ERROR_OPTION;
|
|
/* TODO: Distinguish between two scenarios:
|
|
* - A newer version of ELinks has saved an
|
|
* option that this version does not recognize.
|
|
* The option must be preserved. (This works.)
|
|
* - The user has added an option, saved
|
|
* elinks.conf, restarted ELinks, deleted the
|
|
* option, and is now saving elinks.conf again.
|
|
* The option should be rewritten to "unset".
|
|
* (This does not work yet.)
|
|
* In both cases, ELinks has no struct option
|
|
* for that name. Possible fixes:
|
|
* - If the tree has OPT_AUTOCREATE, then
|
|
* assume the user had created that option,
|
|
* and rewrite it to "unset". Otherwise,
|
|
* keep it.
|
|
* - When the user deletes an option, just mark
|
|
* it with OPT_DELETED, and keep it in memory
|
|
* as long as OPT_TOUCHED is set. */
|
|
}
|
|
|
|
if (!option_types[opt->type].read2) {
|
|
show_parse_error(state, ERROR_VALUE);
|
|
skip_option_value(&state->pos);
|
|
return ERROR_VALUE;
|
|
}
|
|
|
|
val = option_types[opt->type].read2(opt, &state->pos.look,
|
|
&state->pos.line);
|
|
if (!val) {
|
|
/* The reader function failed. Jump back to
|
|
* the beginning of the value and skip it with
|
|
* the generic code. For the error message,
|
|
* use the line number at the beginning of the
|
|
* value, because the ending position is not
|
|
* interesting if there is an unclosed quote. */
|
|
state->pos = pos_before_value;
|
|
show_parse_error(state, ERROR_VALUE);
|
|
skip_option_value(&state->pos);
|
|
return ERROR_VALUE;
|
|
}
|
|
|
|
if (!mirror) {
|
|
/* loading a configuration file */
|
|
if (!option_types[opt->type].set
|
|
|| !option_types[opt->type].set(opt, val)) {
|
|
mem_free(val);
|
|
return show_parse_error(state, ERROR_VALUE);
|
|
}
|
|
} else if (is_system_conf) {
|
|
/* scanning a file that will not be rewritten */
|
|
struct option *flagsite = indirect_option(opt);
|
|
|
|
if (!(flagsite->flags & OPT_DELETED)
|
|
&& option_types[opt->type].equals
|
|
&& option_types[opt->type].equals(opt, val))
|
|
flagsite->flags &= ~OPT_MUST_SAVE;
|
|
else
|
|
flagsite->flags |= OPT_MUST_SAVE;
|
|
} else {
|
|
/* rewriting a configuration file */
|
|
struct option *flagsite = indirect_option(opt);
|
|
|
|
if (flagsite->flags & OPT_DELETED) {
|
|
/* Replace the "set" command with an
|
|
* "unset" command. */
|
|
add_to_string(mirror, "unset ");
|
|
add_bytes_to_string(mirror, optname_orig,
|
|
optname_len);
|
|
state->mirrored = state->pos.look;
|
|
} else if (option_types[opt->type].write2) {
|
|
add_bytes_to_string(mirror, state->mirrored,
|
|
pos_before_value.look
|
|
- state->mirrored);
|
|
option_types[opt->type].write2(opt, mirror);
|
|
state->mirrored = state->pos.look;
|
|
}
|
|
/* Remember that the option need not be
|
|
* written to the end of the file. */
|
|
flagsite->flags &= ~OPT_MUST_SAVE;
|
|
}
|
|
mem_free(val);
|
|
}
|
|
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static enum parse_error
|
|
parse_set_domain(struct option *opt_tree, struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf)
|
|
{
|
|
return parse_set_common(opt_tree, state, mirror, is_system_conf, 1);
|
|
}
|
|
|
|
static enum parse_error
|
|
parse_set(struct option *opt_tree, struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf)
|
|
{
|
|
return parse_set_common(opt_tree, state, mirror, is_system_conf, 0);
|
|
}
|
|
|
|
|
|
static enum parse_error
|
|
parse_unset(struct option *opt_tree, struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf)
|
|
{
|
|
const char *optname_orig;
|
|
size_t optname_len;
|
|
char *optname_copy;
|
|
|
|
skip_white(&state->pos);
|
|
if (!*state->pos.look) return show_parse_error(state, ERROR_PARSE);
|
|
|
|
/* Option name */
|
|
optname_orig = state->pos.look;
|
|
while (is_option_name_char(*state->pos.look)
|
|
|| *state->pos.look == '.')
|
|
state->pos.look++;
|
|
optname_len = state->pos.look - optname_orig;
|
|
|
|
optname_copy = memacpy(optname_orig, optname_len);
|
|
if (!optname_copy) return show_parse_error(state, ERROR_NOMEM);
|
|
|
|
{
|
|
struct option *opt;
|
|
|
|
opt = get_opt_rec_real(opt_tree, optname_copy);
|
|
mem_free(optname_copy);
|
|
optname_copy = NULL;
|
|
|
|
if (!opt || (opt->flags & OPT_HIDDEN)) {
|
|
/* The user wanted to delete the option, and
|
|
* it has already been deleted; this is not an
|
|
* error. This might happen if a version of
|
|
* ELinks has a built-in URL rewriting rule,
|
|
* the user disables it, and a later version
|
|
* no longer has it. */
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
if (!mirror) {
|
|
/* loading a configuration file */
|
|
if (opt->flags & OPT_ALLOC) delete_option(opt);
|
|
else mark_option_as_deleted(opt);
|
|
} else if (is_system_conf) {
|
|
/* scanning a file that will not be rewritten */
|
|
struct option *flagsite = indirect_option(opt);
|
|
|
|
if (flagsite->flags & OPT_DELETED)
|
|
flagsite->flags &= ~OPT_MUST_SAVE;
|
|
else
|
|
flagsite->flags |= OPT_MUST_SAVE;
|
|
} else {
|
|
/* rewriting a configuration file */
|
|
struct option *flagsite = indirect_option(opt);
|
|
|
|
if (flagsite->flags & OPT_DELETED) {
|
|
/* The "unset" command is already in the file,
|
|
* and unlike with "set", there is no value
|
|
* to be updated. */
|
|
} else if (option_types[opt->type].write2) {
|
|
/* Replace the "unset" command with a
|
|
* "set" command. */
|
|
add_to_string(mirror, "set ");
|
|
add_bytes_to_string(mirror, optname_orig,
|
|
optname_len);
|
|
add_to_string(mirror, " = ");
|
|
option_types[opt->type].write2(opt, mirror);
|
|
state->mirrored = state->pos.look;
|
|
}
|
|
/* Remember that the option need not be
|
|
* written to the end of the file. */
|
|
flagsite->flags &= ~OPT_MUST_SAVE;
|
|
}
|
|
}
|
|
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
static enum parse_error
|
|
parse_bind(struct option *opt_tree, struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf)
|
|
{
|
|
char *keymap, *keystroke, *action;
|
|
enum parse_error err = ERROR_NONE;
|
|
struct conf_parsing_pos before_error;
|
|
|
|
skip_white(&state->pos);
|
|
if (!*state->pos.look) return show_parse_error(state, ERROR_PARSE);
|
|
|
|
/* Keymap */
|
|
before_error = state->pos;
|
|
keymap = option_types[OPT_STRING].read2(NULL, &state->pos.look,
|
|
&state->pos.line);
|
|
skip_white(&state->pos);
|
|
if (!keymap || !*state->pos.look) {
|
|
state->pos = before_error;
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
}
|
|
|
|
/* Keystroke */
|
|
before_error = state->pos;
|
|
keystroke = option_types[OPT_STRING].read2(NULL, &state->pos.look,
|
|
&state->pos.line);
|
|
skip_white(&state->pos);
|
|
if (!keystroke || !*state->pos.look) {
|
|
mem_free(keymap); mem_free_if(keystroke);
|
|
state->pos = before_error;
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
}
|
|
|
|
/* Equal sign */
|
|
skip_white(&state->pos);
|
|
if (*state->pos.look != '=') {
|
|
mem_free(keymap); mem_free(keystroke);
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
}
|
|
state->pos.look++; /* '=' */
|
|
|
|
skip_white(&state->pos);
|
|
if (!*state->pos.look) {
|
|
mem_free(keymap); mem_free(keystroke);
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
}
|
|
|
|
/* Action */
|
|
before_error = state->pos;
|
|
action = option_types[OPT_STRING].read2(NULL, &state->pos.look,
|
|
&state->pos.line);
|
|
if (!action) {
|
|
mem_free(keymap); mem_free(keystroke);
|
|
state->pos = before_error;
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
}
|
|
|
|
if (!mirror) {
|
|
/* loading a configuration file */
|
|
/* We don't bother to bind() if -default-keys. */
|
|
if (!get_cmd_opt_bool("default-keys")
|
|
&& bind_do(keymap, keystroke, action, is_system_conf)) {
|
|
/* bind_do() tried but failed. */
|
|
err = show_parse_error(state, ERROR_VALUE);
|
|
} else {
|
|
err = ERROR_NONE;
|
|
}
|
|
} else if (is_system_conf) {
|
|
/* scanning a file that will not be rewritten */
|
|
/* TODO */
|
|
} else {
|
|
/* rewriting a configuration file */
|
|
/* Mirror what we already have. If the keystroke has
|
|
* been unbound, then act_str is simply "none" and
|
|
* this does not require special handling. */
|
|
char *act_str = bind_act(keymap, keystroke);
|
|
|
|
if (act_str) {
|
|
add_bytes_to_string(mirror, state->mirrored,
|
|
before_error.look - state->mirrored);
|
|
add_to_string(mirror, act_str);
|
|
mem_free(act_str);
|
|
state->mirrored = state->pos.look;
|
|
} else {
|
|
err = show_parse_error(state, ERROR_VALUE);
|
|
}
|
|
}
|
|
mem_free(keymap); mem_free(keystroke); mem_free(action);
|
|
return err;
|
|
}
|
|
|
|
static int load_config_file(const char *, const char *, struct option *,
|
|
struct string *, int);
|
|
|
|
static enum parse_error
|
|
parse_include(struct option *opt_tree, struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf)
|
|
{
|
|
char *fname;
|
|
struct string dumbstring;
|
|
struct conf_parsing_pos before_error;
|
|
|
|
if (!init_string(&dumbstring))
|
|
return show_parse_error(state, ERROR_NOMEM);
|
|
|
|
skip_white(&state->pos);
|
|
if (!*state->pos.look) {
|
|
done_string(&dumbstring);
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
}
|
|
|
|
/* File name */
|
|
before_error = state->pos;
|
|
fname = option_types[OPT_STRING].read2(NULL, &state->pos.look,
|
|
&state->pos.line);
|
|
if (!fname) {
|
|
done_string(&dumbstring);
|
|
state->pos = before_error;
|
|
return show_parse_error(state, ERROR_PARSE);
|
|
}
|
|
|
|
/* We want load_config_file() to watermark stuff, but not to load
|
|
* anything, polluting our beloved options tree - thus, we will feed it
|
|
* with some dummy string which we will destroy later; still better
|
|
* than cloning whole options tree or polluting interface with another
|
|
* rarely-used option ;). */
|
|
/* XXX: We should try CONFDIR/<file> when proceeding
|
|
* CONFDIR/<otherfile> ;). --pasky */
|
|
if (load_config_file(fname[0] == '/' ? (char *) ""
|
|
: elinks_home,
|
|
fname, opt_tree,
|
|
mirror ? &dumbstring : NULL, 1)) {
|
|
done_string(&dumbstring);
|
|
mem_free(fname);
|
|
return show_parse_error(state, ERROR_VALUE);
|
|
}
|
|
|
|
done_string(&dumbstring);
|
|
mem_free(fname);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
|
|
struct parse_handler {
|
|
const char *command;
|
|
enum parse_error (*handler)(struct option *opt_tree,
|
|
struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf);
|
|
};
|
|
|
|
static const struct parse_handler parse_handlers[] = {
|
|
{ "set_domain", parse_set_domain },
|
|
{ "set", parse_set },
|
|
{ "unset", parse_unset },
|
|
{ "bind", parse_bind },
|
|
{ "include", parse_include },
|
|
{ NULL, NULL }
|
|
};
|
|
|
|
|
|
static enum parse_error
|
|
parse_config_command(struct option *options, struct conf_parsing_state *state,
|
|
struct string *mirror, int is_system_conf)
|
|
{
|
|
const struct parse_handler *handler;
|
|
|
|
/* If we're mirroring, then everything up to this point must
|
|
* have already been mirrored. */
|
|
assert(mirror == NULL || state->mirrored == state->pos.look);
|
|
if_assert_failed return show_parse_error(state, ERROR_PARSE);
|
|
|
|
for (handler = parse_handlers; handler->command;
|
|
handler++) {
|
|
int cmdlen = strlen(handler->command);
|
|
|
|
if (!strncmp(state->pos.look, handler->command, cmdlen)
|
|
&& isspace((unsigned char)state->pos.look[cmdlen])) {
|
|
enum parse_error err;
|
|
|
|
state->pos.look += cmdlen;
|
|
err = handler->handler(options, state, mirror,
|
|
is_system_conf);
|
|
if (mirror) {
|
|
/* Mirror any characters that the handler
|
|
* consumed but did not already mirror. */
|
|
add_bytes_to_string(mirror, state->mirrored,
|
|
state->pos.look - state->mirrored);
|
|
state->mirrored = state->pos.look;
|
|
}
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return show_parse_error(state, ERROR_COMMAND);
|
|
}
|
|
|
|
enum parse_error
|
|
parse_config_exmode_command(char *cmd)
|
|
{
|
|
struct conf_parsing_state state = {{ 0 }};
|
|
|
|
state.pos.look = cmd;
|
|
state.pos.line = 0;
|
|
state.mirrored = NULL; /* not read because mirror is NULL too */
|
|
state.filename = NULL; /* prevent error messages */
|
|
|
|
return parse_config_command(config_options, &state, NULL, 0);
|
|
}
|
|
|
|
void
|
|
parse_config_file(struct option *options, const char *name,
|
|
char *file, struct string *mirror,
|
|
int is_system_conf)
|
|
{
|
|
struct conf_parsing_state state = {{ 0 }};
|
|
int error_occurred = 0;
|
|
|
|
state.pos.look = file;
|
|
state.pos.line = 1;
|
|
state.mirrored = file;
|
|
if (!mirror && get_cmd_opt_int("verbose") >= VERBOSE_WARNINGS)
|
|
state.filename = name;
|
|
|
|
while (state.pos.look && *state.pos.look) {
|
|
enum parse_error err = ERROR_NONE;
|
|
|
|
/* Skip all possible comments and whitespace. */
|
|
skip_white(&state.pos);
|
|
|
|
/* Mirror what we already have */
|
|
if (mirror) {
|
|
add_bytes_to_string(mirror, state.mirrored,
|
|
state.pos.look - state.mirrored);
|
|
state.mirrored = state.pos.look;
|
|
}
|
|
|
|
/* Second chance to escape from the hell. */
|
|
if (!*state.pos.look) break;
|
|
|
|
err = parse_config_command(options, &state, mirror,
|
|
is_system_conf);
|
|
switch (err) {
|
|
case ERROR_NONE:
|
|
break;
|
|
|
|
case ERROR_COMMAND:
|
|
case ERROR_PARSE:
|
|
/* Jump over this crap we can't understand. */
|
|
skip_to_unquoted_newline_or_comment(&state.pos);
|
|
|
|
/* Mirror what we already have */
|
|
if (mirror) {
|
|
add_bytes_to_string(mirror, state.mirrored,
|
|
state.pos.look - state.mirrored);
|
|
state.mirrored = state.pos.look;
|
|
}
|
|
|
|
/* fall through */
|
|
default:
|
|
error_occurred = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!error_occurred || !state.filename) return;
|
|
|
|
/* If an error occurred make sure that the user is notified and is able
|
|
* to see it. First sound the bell. Then, if the text viewer is going to
|
|
* be started, sleep for a while so the message will be visible before
|
|
* ELinks starts drawing to on the screen and overwriting any error
|
|
* messages. This should not be necessary for terminals supporting the
|
|
* alternate screen mode but better to be safe. (debian bug 305017) */
|
|
|
|
fputc('\a', stderr);
|
|
|
|
if (get_cmd_opt_bool("dump")
|
|
|| get_cmd_opt_bool("source"))
|
|
return;
|
|
|
|
sleep(1);
|
|
}
|
|
|
|
|
|
|
|
static char *
|
|
read_config_file(char *name)
|
|
{
|
|
#define FILE_BUF 1024
|
|
char cfg_buffer[FILE_BUF];
|
|
struct string string;
|
|
int fd;
|
|
ssize_t r;
|
|
|
|
fd = open(name, O_RDONLY | O_NOCTTY);
|
|
if (fd < 0) return NULL;
|
|
set_bin(fd);
|
|
|
|
if (!init_string(&string)) return NULL;
|
|
|
|
while ((r = safe_read(fd, cfg_buffer, FILE_BUF)) > 0) {
|
|
int i;
|
|
|
|
/* Clear problems ;). */
|
|
for (i = 0; i < r; i++)
|
|
if (!cfg_buffer[i])
|
|
cfg_buffer[i] = ' ';
|
|
|
|
add_bytes_to_string(&string, cfg_buffer, r);
|
|
}
|
|
|
|
if (r < 0) done_string(&string);
|
|
close(fd);
|
|
|
|
return string.source;
|
|
#undef FILE_BUF
|
|
}
|
|
|
|
/* Return 0 on success. */
|
|
static int
|
|
load_config_file(const char *prefix, const char *name,
|
|
struct option *options, struct string *mirror,
|
|
int is_system_conf)
|
|
{
|
|
char *config_str, *config_file;
|
|
|
|
config_file = straconcat(prefix, STRING_DIR_SEP, name,
|
|
(char *) NULL);
|
|
if (!config_file) return 1;
|
|
|
|
config_str = read_config_file(config_file);
|
|
if (!config_str) {
|
|
mem_free(config_file);
|
|
config_file = straconcat(prefix, STRING_DIR_SEP, ".", name,
|
|
(char *) NULL);
|
|
if (!config_file) return 2;
|
|
|
|
config_str = read_config_file(config_file);
|
|
if (!config_str) {
|
|
mem_free(config_file);
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
parse_config_file(options, config_file, config_str, mirror,
|
|
is_system_conf);
|
|
|
|
mem_free(config_str);
|
|
mem_free(config_file);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
load_config_from(char *file, struct option *tree)
|
|
{
|
|
load_config_file(CONFDIR, file, tree, NULL, 1);
|
|
load_config_file(empty_string_or_(elinks_home), file, tree, NULL, 0);
|
|
}
|
|
|
|
void
|
|
load_config(void)
|
|
{
|
|
load_config_from(get_cmd_opt_str("config-file"),
|
|
config_options);
|
|
}
|
|
|
|
|
|
static int indentation = 2;
|
|
/* 0 -> none, 1 -> only option full name+type, 2 -> only desc, 3 -> both */
|
|
static int comments = 3;
|
|
|
|
static inline const char *
|
|
conf_i18n(const char *s, int i18n)
|
|
{
|
|
if (i18n) return gettext(s);
|
|
return s;
|
|
}
|
|
|
|
static void
|
|
add_indent_to_string(struct string *string, int depth)
|
|
{
|
|
if (!depth) return;
|
|
|
|
add_xchar_to_string(string, ' ', depth * indentation);
|
|
}
|
|
|
|
struct string *
|
|
wrap_option_desc(struct string *out, const char *src,
|
|
const struct string *indent, int maxwidth)
|
|
{
|
|
const char *last_space = NULL;
|
|
const char *uncopied = src;
|
|
int width = 0;
|
|
|
|
/* TODO: multibyte or fullwidth characters */
|
|
for (; *src; src++, width++) {
|
|
if (*src == '\n') {
|
|
last_space = src;
|
|
goto split;
|
|
}
|
|
|
|
if (*src == ' ') last_space = src;
|
|
|
|
if (width >= maxwidth && last_space) {
|
|
split:
|
|
if (!add_string_to_string(out, indent))
|
|
return NULL;
|
|
if (!add_bytes_to_string(out, uncopied,
|
|
last_space - uncopied))
|
|
return NULL;
|
|
if (!add_char_to_string(out, '\n'))
|
|
return NULL;
|
|
uncopied = last_space + 1;
|
|
width = src - uncopied;
|
|
last_space = NULL;
|
|
}
|
|
}
|
|
if (*uncopied) {
|
|
if (!add_string_to_string(out, indent))
|
|
return NULL;
|
|
if (!add_to_string(out, uncopied))
|
|
return NULL;
|
|
if (!add_char_to_string(out, '\n'))
|
|
return NULL;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static void
|
|
output_option_desc_as_comment(struct string *out, const struct option *option,
|
|
int i18n, int depth)
|
|
{
|
|
const char *desc_i18n = conf_i18n(option->desc, i18n);
|
|
struct string indent;
|
|
|
|
if (!init_string(&indent)) return;
|
|
|
|
add_indent_to_string(&indent, depth);
|
|
if (!add_to_string(&indent, "# ")) goto out_of_memory;
|
|
if (!wrap_option_desc(out, desc_i18n, &indent, 80 - indent.length))
|
|
goto out_of_memory;
|
|
|
|
out_of_memory:
|
|
done_string(&indent);
|
|
}
|
|
|
|
static char *smart_config_output_fn_domain;
|
|
|
|
static void
|
|
smart_config_output_fn(struct string *string, struct option *option,
|
|
char *path, int depth, int do_print_comment,
|
|
int action, int i18n)
|
|
{
|
|
if (option->type == OPT_ALIAS)
|
|
return;
|
|
|
|
switch (action) {
|
|
case 0:
|
|
if (!(comments & 1)) break;
|
|
|
|
add_indent_to_string(string, depth);
|
|
|
|
add_to_string(string, "## ");
|
|
if (path) {
|
|
add_to_string(string, path);
|
|
add_char_to_string(string, '.');
|
|
}
|
|
add_to_string(string, option->name);
|
|
add_char_to_string(string, ' ');
|
|
add_to_string(string, option_types[option->type].help_str);
|
|
add_char_to_string(string, '\n');
|
|
break;
|
|
|
|
case 1:
|
|
if (!(comments & 2)) break;
|
|
|
|
if (!option->desc || !do_print_comment)
|
|
break;
|
|
|
|
output_option_desc_as_comment(string, option,
|
|
i18n, depth);
|
|
break;
|
|
|
|
case 2:
|
|
|
|
add_indent_to_string(string, depth);
|
|
if (option->flags & OPT_DELETED) {
|
|
add_to_string(string, "un");
|
|
}
|
|
if (smart_config_output_fn_domain) {
|
|
add_to_string(string, "set_domain ");
|
|
add_to_string(string, smart_config_output_fn_domain);
|
|
add_char_to_string(string, ' ');
|
|
} else {
|
|
add_to_string(string, "set ");
|
|
}
|
|
if (path) {
|
|
add_to_string(string, path);
|
|
add_char_to_string(string, '.');
|
|
}
|
|
add_to_string(string, option->name);
|
|
if (!(option->flags & OPT_DELETED)) {
|
|
add_to_string(string, " = ");
|
|
/* OPT_ALIAS won't ever. OPT_TREE won't reach action 2.
|
|
* OPT_SPECIAL makes no sense in the configuration
|
|
* context. */
|
|
assert(option_types[option->type].write2);
|
|
option_types[option->type].write2(option, string);
|
|
}
|
|
add_char_to_string(string, '\n');
|
|
if (do_print_comment) add_char_to_string(string, '\n');
|
|
break;
|
|
|
|
case 3:
|
|
if (do_print_comment < 2)
|
|
add_char_to_string(string, '\n');
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
smart_config_output_fn_html(struct string *string, struct option *option,
|
|
char *path, int depth, int do_print_comment,
|
|
int action, int i18n)
|
|
{
|
|
static unsigned int counter;
|
|
int is_str = 0;
|
|
|
|
if (option->type == OPT_ALIAS) {
|
|
return;
|
|
}
|
|
|
|
if (option->flags & OPT_DELETED) {
|
|
return;
|
|
}
|
|
|
|
if (smart_config_output_fn_domain) {
|
|
return;
|
|
}
|
|
|
|
switch (action) {
|
|
case 0:
|
|
case 1:
|
|
break;
|
|
|
|
case 2:
|
|
counter++;
|
|
add_to_string(string, "<tr><td>");
|
|
|
|
if (path) {
|
|
add_to_string(string, path);
|
|
add_char_to_string(string, '.');
|
|
}
|
|
add_to_string(string, option->name);
|
|
add_to_string(string, "</td><td>");
|
|
|
|
add_format_to_string(string, "<form name=\"el%d\" action=\"about:config\" method=\"GET\">", counter);
|
|
add_to_string(string, "<input type=\"hidden\" name=\"option\" value=\"");
|
|
|
|
if (path) {
|
|
add_to_string(string, path);
|
|
add_char_to_string(string, '.');
|
|
}
|
|
add_to_string(string, option->name);
|
|
add_to_string(string, "\"/><input type=\"text\" name=\"val\" value=\"");
|
|
|
|
assert(option_types[option->type].write2);
|
|
{
|
|
struct string tmp;
|
|
|
|
if (init_string(&tmp)) {
|
|
option_types[option->type].write2(option, &tmp);
|
|
|
|
if (tmp.length >= 2 && tmp.source[0] == '"' && tmp.source[tmp.length - 1] == '"') {
|
|
add_bytes_to_string(string, tmp.source + 1, tmp.length - 2);
|
|
is_str = 1;
|
|
} else {
|
|
add_string_to_string(string, &tmp);
|
|
}
|
|
done_string(&tmp);
|
|
}
|
|
}
|
|
add_to_string(string, "\"/><input type=\"submit\" name=\"set\" value=\"Set\"/>");
|
|
|
|
if (is_str) {
|
|
add_to_string(string, "<input type=\"hidden\" name=\"str\" value=\"1\"/>");
|
|
}
|
|
add_to_string(string, "<input type=\"submit\" name=\"save\" value=\"Save\"/></form></td></tr>\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
add_cfg_header_to_string(struct string *string, const char *text)
|
|
{
|
|
int n = strlen(text) + 2;
|
|
|
|
int_bounds(&n, 10, 80);
|
|
|
|
add_to_string(string, "\n\n\n");
|
|
add_xchar_to_string(string, '#', n);
|
|
add_to_string(string, "\n# ");
|
|
add_to_string(string, text);
|
|
add_to_string(string, "#\n\n");
|
|
}
|
|
|
|
char *
|
|
create_about_config_string(void)
|
|
{
|
|
struct option *options = config_options;
|
|
struct string config;
|
|
/* Don't write headers if nothing will be added anyway. */
|
|
struct string tmpstring;
|
|
int origlen;
|
|
|
|
if (!init_string(&config)) return NULL;
|
|
|
|
{
|
|
int set_all = 1;
|
|
struct domain_tree *domain;
|
|
|
|
prepare_mustsave_flags(options->value.tree, set_all);
|
|
foreach (domain, domain_trees) {
|
|
prepare_mustsave_flags(domain->tree->value.tree,
|
|
set_all);
|
|
}
|
|
}
|
|
|
|
/* Scaring. */
|
|
if (!init_string(&tmpstring)) goto get_me_out;
|
|
|
|
add_to_string(&tmpstring, "<html><body><table border=\"1\"><tr><th>Option name</th><th>Value</th></tr>\n");
|
|
|
|
origlen = tmpstring.length;
|
|
smart_config_string(&tmpstring, 2, 0, options->value.tree, NULL, 0,
|
|
smart_config_output_fn_html);
|
|
|
|
{
|
|
struct domain_tree *domain;
|
|
|
|
foreach (domain, domain_trees) {
|
|
smart_config_output_fn_domain = domain->name;
|
|
smart_config_string(&tmpstring, 2, 0,
|
|
domain->tree->value.tree,
|
|
NULL, 0,
|
|
smart_config_output_fn_html);
|
|
}
|
|
|
|
smart_config_output_fn_domain = NULL;
|
|
}
|
|
|
|
add_to_string(&tmpstring, "</table></body></html>");
|
|
|
|
if (tmpstring.length > origlen)
|
|
add_string_to_string(&config, &tmpstring);
|
|
done_string(&tmpstring);
|
|
|
|
get_me_out:
|
|
return config.source;
|
|
}
|
|
|
|
char *
|
|
create_config_string(const char *prefix, const char *name)
|
|
{
|
|
struct option *options = config_options;
|
|
struct string config;
|
|
/* Don't write headers if nothing will be added anyway. */
|
|
struct string tmpstring;
|
|
int origlen;
|
|
int savestyle = get_opt_int("config.saving_style", NULL);
|
|
int i18n = get_opt_bool("config.i18n", NULL);
|
|
|
|
if (!init_string(&config)) return NULL;
|
|
|
|
{
|
|
int set_all = (savestyle == 1 || savestyle == 2);
|
|
struct domain_tree *domain;
|
|
|
|
prepare_mustsave_flags(options->value.tree, set_all);
|
|
foreach (domain, domain_trees) {
|
|
prepare_mustsave_flags(domain->tree->value.tree,
|
|
set_all);
|
|
}
|
|
}
|
|
|
|
/* Scaring. */
|
|
if (savestyle == 2
|
|
|| load_config_file(prefix, name, options, &config, 0)
|
|
|| !config.length) {
|
|
/* At first line, and in English, write ELinks version, may be
|
|
* of some help in future. Please keep that format for it.
|
|
* --Zas */
|
|
add_to_string(&config, "## ELinks " VERSION " configuration file\n\n");
|
|
assert(savestyle >= 0 && savestyle <= 3);
|
|
switch (savestyle) {
|
|
case 0:
|
|
add_to_string(&config, conf_i18n(N_(
|
|
"## This is ELinks configuration file. You can edit it manually,\n"
|
|
"## if you wish so; this file is edited by ELinks when you save\n"
|
|
"## options through UI, however only option values will be altered\n"
|
|
"## and all your formatting, own comments etc will be kept as-is.\n"),
|
|
i18n));
|
|
break;
|
|
case 1: case 3:
|
|
add_to_string(&config, conf_i18n(N_(
|
|
"## This is ELinks configuration file. You can edit it manually,\n"
|
|
"## if you wish so; this file is edited by ELinks when you save\n"
|
|
"## options through UI, however only option values will be altered\n"
|
|
"## and missing options will be added at the end of file; if option\n"
|
|
"## is not written in this file, but in some file included from it,\n"
|
|
"## it is NOT counted as missing. Note that all your formatting,\n"
|
|
"## own comments and so on will be kept as-is.\n"), i18n));
|
|
break;
|
|
case 2:
|
|
add_to_string(&config, conf_i18n(N_(
|
|
"## This is ELinks configuration file. You can edit it manually,\n"
|
|
"## if you wish so, but keep in mind that this file is overwritten\n"
|
|
"## by ELinks when you save options through UI and you are out of\n"
|
|
"## luck with your formatting and own comments then, so beware.\n"),
|
|
i18n));
|
|
break;
|
|
}
|
|
|
|
add_to_string(&config, "##\n");
|
|
|
|
add_to_string(&config, conf_i18n(N_(
|
|
"## Obviously, if you don't like what ELinks is going to do with\n"
|
|
"## this file, you can change it by altering the config.saving_style\n"
|
|
"## option. Come on, aren't we friendly guys after all?\n"), i18n));
|
|
}
|
|
|
|
if (savestyle == 0) goto get_me_out;
|
|
|
|
indentation = get_opt_int("config.indentation", NULL);
|
|
comments = get_opt_int("config.comments", NULL);
|
|
|
|
if (!init_string(&tmpstring)) goto get_me_out;
|
|
|
|
add_cfg_header_to_string(&tmpstring,
|
|
conf_i18n(N_("Automatically saved options\n"), i18n));
|
|
|
|
origlen = tmpstring.length;
|
|
smart_config_string(&tmpstring, 2, i18n, options->value.tree, NULL, 0,
|
|
smart_config_output_fn);
|
|
|
|
{
|
|
struct domain_tree *domain;
|
|
|
|
foreach (domain, domain_trees) {
|
|
smart_config_output_fn_domain = domain->name;
|
|
smart_config_string(&tmpstring, 2, i18n,
|
|
domain->tree->value.tree,
|
|
NULL, 0,
|
|
smart_config_output_fn);
|
|
}
|
|
|
|
smart_config_output_fn_domain = NULL;
|
|
}
|
|
|
|
if (tmpstring.length > origlen)
|
|
add_string_to_string(&config, &tmpstring);
|
|
done_string(&tmpstring);
|
|
|
|
if (!init_string(&tmpstring)) goto get_me_out;
|
|
|
|
add_cfg_header_to_string(&tmpstring,
|
|
conf_i18n(N_("Automatically saved keybindings\n"), i18n));
|
|
|
|
origlen = tmpstring.length;
|
|
bind_config_string(&tmpstring);
|
|
if (tmpstring.length > origlen)
|
|
add_string_to_string(&config, &tmpstring);
|
|
done_string(&tmpstring);
|
|
|
|
get_me_out:
|
|
return config.source;
|
|
}
|
|
|
|
static int
|
|
write_config_file(char *prefix, char *name,
|
|
struct terminal *term)
|
|
{
|
|
int ret = -1;
|
|
struct secure_save_info *ssi;
|
|
char *config_file = NULL;
|
|
char *cfg_str = create_config_string(prefix, name);
|
|
int prefixlen = strlen(prefix);
|
|
int prefix_has_slash = (prefixlen && dir_sep(prefix[prefixlen - 1]));
|
|
int name_has_slash = dir_sep(name[0]);
|
|
const char *slash = name_has_slash || prefix_has_slash ? "" : STRING_DIR_SEP;
|
|
|
|
if (!cfg_str) return -1;
|
|
|
|
if (name_has_slash && prefix_has_slash) name++;
|
|
|
|
config_file = straconcat(prefix, slash, name, (char *) NULL);
|
|
if (!config_file) goto free_cfg_str;
|
|
|
|
ssi = secure_open(config_file);
|
|
if (ssi) {
|
|
secure_fputs(ssi, cfg_str);
|
|
ret = secure_close(ssi);
|
|
if (!ret) {
|
|
struct domain_tree *domain;
|
|
|
|
untouch_options(config_options->value.tree);
|
|
foreach (domain, domain_trees)
|
|
untouch_options(domain->tree->value.tree);
|
|
}
|
|
}
|
|
|
|
if (term) {
|
|
write_config_dialog(term, config_file, secsave_errno, ret);
|
|
}
|
|
mem_free(config_file);
|
|
|
|
free_cfg_str:
|
|
mem_free(cfg_str);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int
|
|
write_config(struct terminal *term)
|
|
{
|
|
if (!elinks_home) {
|
|
if (term) {
|
|
write_config_dialog(term, get_cmd_opt_str("config-file"),
|
|
SS_ERR_DISABLED, 0);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
return write_config_file(elinks_home, get_cmd_opt_str("config-file"),
|
|
term);
|
|
}
|
|
|
|
void
|
|
set_option_or_save(const char *str)
|
|
{
|
|
#define NUMKVPAIRS 16
|
|
int i;
|
|
char *kvpairs[NUMKVPAIRS];
|
|
char *option_name;
|
|
char *option_value;
|
|
char *set;
|
|
char *save;
|
|
|
|
struct string tmp;
|
|
|
|
if (!init_string(&tmp)) {
|
|
return;
|
|
}
|
|
add_to_string(&tmp, str);
|
|
i = qs_parse(tmp.source, kvpairs, 16);
|
|
|
|
option_name = qs_k2v("option", kvpairs, i);
|
|
option_value = qs_k2v("val", kvpairs, i);
|
|
set = qs_k2v("set", kvpairs, i);
|
|
save = qs_k2v("save", kvpairs, i);
|
|
|
|
if (set || save) {
|
|
struct option *opt = get_opt_rec(config_options, option_name);
|
|
|
|
if (opt) {
|
|
/* Set option */
|
|
switch (opt->type) {
|
|
case OPT_BOOL:
|
|
{
|
|
/* option_types[OPT_BOOL].set expects a long even though it
|
|
* saves the value to opt->value.number, which is an int. */
|
|
long value = !!atol(option_value);
|
|
|
|
option_types[opt->type].set(opt, (char *) (&value));
|
|
break;
|
|
}
|
|
case OPT_INT:
|
|
case OPT_LONG:
|
|
{
|
|
/* option_types[OPT_INT].set expects a long even though it
|
|
* saves the value to opt->value.number, which is an int.
|
|
* option_types[OPT_LONG].set of course wants a long too. */
|
|
long value = atol(option_value);
|
|
|
|
option_types[opt->type].set(opt, (char *) (&value));
|
|
break;
|
|
}
|
|
case OPT_STRING:
|
|
case OPT_CODEPAGE:
|
|
case OPT_LANGUAGE:
|
|
case OPT_COLOR:
|
|
option_types[opt->type].set(opt, option_value);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
option_changed((struct session *)sessions.next, opt);
|
|
|
|
if (save) {
|
|
write_config(NULL);
|
|
}
|
|
}
|
|
}
|
|
done_string(&tmp);
|
|
#undef NUMKVPAIRS
|
|
}
|