Implement include config directive (#4420)
The implementation uses wordexp(3) just like sway:
https://github.com/i3/i3/issues/1197#issuecomment-226844106
Thanks to jajm for their implementation at
bb55709d0a
This required refactoring the config parser to be re-entrant
(no more global state) and to return an error instead of dying.
In case a file cannot be opened, i3 reports an error but proceeds with the
remaining configuration.
Key bindings can be overwritten or removed using the new --remove flag of the
bindsym/bindcode directive.
All files that were successfully included are displayed in i3 --moreversion.
One caveat is i3 config file variable expansion, see the note in the userguide.
fixes #4192
This commit is contained in:
parent
4c93f61353
commit
eaa5e636f9
@ -35,6 +35,7 @@ option is enabled and only then sets a screenshot as background.
|
||||
• i3bar: use first bar config by default
|
||||
• i3-dump-log -f now uses UNIX sockets instead of pthreads. The UNIX socket approach
|
||||
should be more reliable and also more portable.
|
||||
• Implement the include config directive
|
||||
• Allow for_window to match against WM_CLIENT_MACHINE
|
||||
• Add %machine placeholder (WM_CLIENT_MACHINE) to title_format
|
||||
• Allow multiple output names in 'move container|workspace to output'
|
||||
|
@ -319,6 +319,90 @@ include the following line in your config file:
|
||||
# i3 config file (v4)
|
||||
---------------------
|
||||
|
||||
[[include]]
|
||||
=== Include directive
|
||||
|
||||
Since i3 v4.20, it is possible to include other configuration files from your i3
|
||||
configuration.
|
||||
|
||||
*Syntax*:
|
||||
-----------------
|
||||
include <pattern>
|
||||
-----------------
|
||||
|
||||
i3 expands `pattern` using shell-like word expansion, specifically using the
|
||||
https://manpages.debian.org/wordexp.3[`wordexp(3)` C standard library function].
|
||||
|
||||
*Examples*:
|
||||
--------------------------------------------------------------------------------
|
||||
# Tilde expands to the user’s home directory:
|
||||
include ~/.config/i3/assignments.conf
|
||||
|
||||
# Environment variables are expanded:
|
||||
include $HOME/.config/i3/assignments.conf
|
||||
|
||||
# Wildcards are expanded:
|
||||
include ~/.config/i3/config.d/*.conf
|
||||
|
||||
# Command substitution:
|
||||
include ~/.config/i3/`hostname`.conf
|
||||
|
||||
# i3 loads each path only once, so including the i3 config will not result
|
||||
# in an endless loop, but in an error:
|
||||
include ~/.config/i3/config
|
||||
|
||||
# i3 changes the working directory while parsing a config file
|
||||
# so that relative paths are interpreted relative to the directory
|
||||
# of the config file that contains the path:
|
||||
include assignments.conf
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
If a specified file cannot be read, for example because of a lack of file
|
||||
permissions, or because of a dangling symlink, i3 will report an error and
|
||||
continue processing your remaining configuration.
|
||||
|
||||
To list all loaded configuration files, run `i3 --moreversion`:
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
% i3 --moreversion
|
||||
Binary i3 version: 4.19.2-87-gfcae64f7+ © 2009 Michael Stapelberg and contributors
|
||||
Running i3 version: 4.19.2-87-gfcae64f7+ (pid 963940)
|
||||
Loaded i3 config:
|
||||
/tmp/i3.cfg (main) (last modified: 2021-05-13T16:42:31 CEST, 463 seconds ago)
|
||||
/tmp/included.cfg (included) (last modified: 2021-05-13T16:42:43 CEST, 451 seconds ago)
|
||||
/tmp/another.cfg (included) (last modified: 2021-05-13T16:42:46 CEST, 448 seconds ago)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Variables are shared between all config files, but beware of the following limitation:
|
||||
|
||||
* You can define a variable and use it within an included file.
|
||||
* You cannot use (in the parent file) a variable that was defined within an included file.
|
||||
|
||||
This is a technical limitation: variable expansion happens in a separate stage
|
||||
before parsing include directives.
|
||||
|
||||
Conceptually, included files can only add to the configuration, not undo the
|
||||
effects of already-processed configuration. For example, you can only add new
|
||||
key bindings, not overwrite or remove existing key bindings. This means:
|
||||
|
||||
* The `include` directive is suitable for organizing large configurations into
|
||||
separate files, possibly selecting files based on conditionals.
|
||||
|
||||
* The `include` directive is not suitable for expressing “use the default
|
||||
configuration with the following changes”. For that case, we still recommend
|
||||
copying and modifying the default config.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Implementation-wise, i3 does not currently construct one big configuration from
|
||||
all `include` directives. Instead, i3’s config file parser interprets all
|
||||
configuration directives in its `parse_file()` function. When processing an
|
||||
`include` configuration directive, the parser recursively calls `parse_file()`.
|
||||
|
||||
This means the evaluation order of files forms a tree, or one could say i3 uses
|
||||
depth-first traversal.
|
||||
====
|
||||
|
||||
=== Comments
|
||||
|
||||
It is possible and recommended to use comments in your configuration file to
|
||||
|
@ -133,7 +133,7 @@ close($enumfh);
|
||||
open(my $callfh, '>', "GENERATED_${prefix}_call.h");
|
||||
my $resultname = uc(substr($prefix, 0, 1)) . substr($prefix, 1) . 'ResultIR';
|
||||
say $callfh '#pragma once';
|
||||
say $callfh "static void GENERATED_call(const int call_identifier, struct $resultname *result) {";
|
||||
say $callfh "static void GENERATED_call(Match *current_match, struct stack *stack, const int call_identifier, struct $resultname *result) {";
|
||||
say $callfh ' switch (call_identifier) {';
|
||||
my $call_id = 0;
|
||||
for my $state (@keys) {
|
||||
@ -150,8 +150,8 @@ for my $state (@keys) {
|
||||
# calls to get_string(). Also replaces state names (like FOR_WINDOW)
|
||||
# with their ID (useful for cfg_criteria_init(FOR_WINDOW) e.g.).
|
||||
$cmd =~ s/$_/$statenum{$_}/g for @keys;
|
||||
$cmd =~ s/\$([a-z_]+)/get_string("$1")/g;
|
||||
$cmd =~ s/\&([a-z_]+)/get_long("$1")/g;
|
||||
$cmd =~ s/\$([a-z_]+)/get_string(stack, "$1")/g;
|
||||
$cmd =~ s/\&([a-z_]+)/get_long(stack, "$1")/g;
|
||||
# For debugging/testing, we print the call using printf() and thus need
|
||||
# to generate a format string. The format uses %d for <number>s,
|
||||
# literal numbers or state IDs and %s for NULL, <string>s and literal
|
||||
@ -175,9 +175,9 @@ for my $state (@keys) {
|
||||
say $callfh '#ifndef TEST_PARSER';
|
||||
my $real_cmd = $cmd;
|
||||
if ($real_cmd =~ /\(\)/) {
|
||||
$real_cmd =~ s/\(/(¤t_match, result/;
|
||||
$real_cmd =~ s/\(/(current_match, result/;
|
||||
} else {
|
||||
$real_cmd =~ s/\(/(¤t_match, result, /;
|
||||
$real_cmd =~ s/\(/(current_match, result, /;
|
||||
}
|
||||
say $callfh " $real_cmd;";
|
||||
say $callfh '#else';
|
||||
|
@ -39,6 +39,7 @@ CFGFUN(criteria_init, int _state);
|
||||
CFGFUN(criteria_add, const char *ctype, const char *cvalue);
|
||||
CFGFUN(criteria_pop_state);
|
||||
|
||||
CFGFUN(include, const char *pattern);
|
||||
CFGFUN(font, const char *font);
|
||||
CFGFUN(exec, const char *exectype, const char *no_startup_id, const char *command);
|
||||
CFGFUN(for_window, const char *command);
|
||||
|
@ -16,6 +16,50 @@
|
||||
SLIST_HEAD(variables_head, Variable);
|
||||
extern pid_t config_error_nagbar_pid;
|
||||
|
||||
struct stack_entry {
|
||||
/* Just a pointer, not dynamically allocated. */
|
||||
const char *identifier;
|
||||
enum {
|
||||
STACK_STR = 0,
|
||||
STACK_LONG = 1,
|
||||
} type;
|
||||
union {
|
||||
char *str;
|
||||
long num;
|
||||
} val;
|
||||
};
|
||||
|
||||
struct stack {
|
||||
struct stack_entry stack[10];
|
||||
};
|
||||
|
||||
struct parser_ctx {
|
||||
bool use_nagbar;
|
||||
bool assume_v4;
|
||||
|
||||
int state;
|
||||
Match current_match;
|
||||
|
||||
/* A list which contains the states that lead to the current state, e.g.
|
||||
* INITIAL, WORKSPACE_LAYOUT.
|
||||
* When jumping back to INITIAL, statelist_idx will simply be set to 1
|
||||
* (likewise for other states, e.g. MODE or BAR).
|
||||
* This list is used to process the nearest error token. */
|
||||
int statelist[10];
|
||||
/* NB: statelist_idx points to where the next entry will be inserted */
|
||||
int statelist_idx;
|
||||
|
||||
/*******************************************************************************
|
||||
* The (small) stack where identified literals are stored during the parsing
|
||||
* of a single config directive (like $workspace).
|
||||
******************************************************************************/
|
||||
struct stack *stack;
|
||||
|
||||
struct variables_head variables;
|
||||
|
||||
bool has_errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* An intermediate reprsentation of the result of a parse_config call.
|
||||
* Currently unused, but the JSON output will be useful in the future when we
|
||||
@ -23,22 +67,34 @@ extern pid_t config_error_nagbar_pid;
|
||||
*
|
||||
*/
|
||||
struct ConfigResultIR {
|
||||
/* The JSON generator to append a reply to. */
|
||||
yajl_gen json_gen;
|
||||
struct parser_ctx *ctx;
|
||||
|
||||
/* The next state to transition to. Passed to the function so that we can
|
||||
* determine the next state as a result of a function call, like
|
||||
* cfg_criteria_pop_state() does. */
|
||||
int next_state;
|
||||
};
|
||||
|
||||
struct ConfigResultIR *parse_config(const char *input, struct context *context);
|
||||
/* Whether any error happened while processing this config directive. */
|
||||
bool has_errors;
|
||||
};
|
||||
|
||||
/**
|
||||
* launch nagbar to indicate errors in the configuration file.
|
||||
*/
|
||||
void start_config_error_nagbar(const char *configpath, bool has_errors);
|
||||
|
||||
/**
|
||||
* Releases the memory of all variables in ctx.
|
||||
*
|
||||
*/
|
||||
void free_variables(struct parser_ctx *ctx);
|
||||
|
||||
typedef enum {
|
||||
PARSE_FILE_FAILED = -1,
|
||||
PARSE_FILE_SUCCESS = 0,
|
||||
PARSE_FILE_CONFIG_ERRORS = 1,
|
||||
} parse_file_result_t;
|
||||
|
||||
/**
|
||||
* Parses the given file by first replacing the variables, then calling
|
||||
* parse_config and launching i3-nagbar if use_nagbar is true.
|
||||
@ -47,4 +103,4 @@ void start_config_error_nagbar(const char *configpath, bool has_errors);
|
||||
* parsing.
|
||||
*
|
||||
*/
|
||||
bool parse_file(const char *f, bool use_nagbar);
|
||||
parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f);
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include "queue.h"
|
||||
#include "i3.h"
|
||||
|
||||
typedef struct IncludedFile IncludedFile;
|
||||
typedef struct Config Config;
|
||||
typedef struct Barconfig Barconfig;
|
||||
extern char *current_configpath;
|
||||
@ -22,6 +23,7 @@ extern char *current_config;
|
||||
extern Config config;
|
||||
extern SLIST_HEAD(modes_head, Mode) modes;
|
||||
extern TAILQ_HEAD(barconfig_head, Barconfig) barconfigs;
|
||||
extern TAILQ_HEAD(includedfiles_head, IncludedFile) included_files;
|
||||
|
||||
/**
|
||||
* Used during the config file lexing/parsing to keep the state of the lexer
|
||||
@ -69,6 +71,16 @@ struct Variable {
|
||||
SLIST_ENTRY(Variable) variables;
|
||||
};
|
||||
|
||||
/**
|
||||
* List entry struct for an included file.
|
||||
*
|
||||
*/
|
||||
struct IncludedFile {
|
||||
char *path;
|
||||
|
||||
TAILQ_ENTRY(IncludedFile) files;
|
||||
};
|
||||
|
||||
/**
|
||||
* The configuration file can contain multiple sets of bindings. Apart from the
|
||||
* default set (name == "default"), you can specify other sets and change the
|
||||
|
@ -20,6 +20,7 @@ state INITIAL:
|
||||
'set ' -> IGNORE_LINE
|
||||
'set ' -> IGNORE_LINE
|
||||
'set_from_resource' -> IGNORE_LINE
|
||||
'include' -> INCLUDE
|
||||
bindtype = 'bindsym', 'bindcode', 'bind' -> BINDING
|
||||
'bar' -> BARBRACE
|
||||
'font' -> FONT
|
||||
@ -63,6 +64,11 @@ state IGNORE_LINE:
|
||||
line
|
||||
-> INITIAL
|
||||
|
||||
# include <pattern>
|
||||
state INCLUDE:
|
||||
pattern = string
|
||||
-> call cfg_include($pattern)
|
||||
|
||||
# floating_minimum_size <width> x <height>
|
||||
state FLOATING_MINIMUM_SIZE_WIDTH:
|
||||
width = number
|
||||
@ -394,6 +400,8 @@ state BINDCOMMAND:
|
||||
->
|
||||
command = string
|
||||
-> call cfg_binding($bindtype, $modifiers, $key, $release, $border, $whole_window, $exclude_titlebar, $command)
|
||||
end
|
||||
-> call cfg_binding($bindtype, $modifiers, $key, $release, $border, $whole_window, $exclude_titlebar, $command)
|
||||
|
||||
################################################################################
|
||||
# Mode configuration
|
||||
|
@ -717,6 +717,40 @@ void reorder_bindings(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if a is a key binding for the same key as b.
|
||||
*
|
||||
*/
|
||||
static bool binding_same_key(Binding *a, Binding *b) {
|
||||
/* Check if the input types are different */
|
||||
if (a->input_type != b->input_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check if one is using keysym while the other is using bindsym. */
|
||||
if ((a->symbol == NULL && b->symbol != NULL) ||
|
||||
(a->symbol != NULL && b->symbol == NULL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* If a is NULL, b has to be NULL, too (see previous conditional).
|
||||
* If the keycodes differ, it can't be a duplicate. */
|
||||
if (a->symbol != NULL &&
|
||||
strcasecmp(a->symbol, b->symbol) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check if the keycodes or modifiers are different. If so, they
|
||||
* can't be duplicate */
|
||||
if (a->keycode != b->keycode ||
|
||||
a->event_state_mask != b->event_state_mask ||
|
||||
a->release != b->release) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks for duplicate key bindings (the same keycode or keysym is configured
|
||||
* more than once). If a duplicate binding is found, a message is printed to
|
||||
@ -730,31 +764,13 @@ void check_for_duplicate_bindings(struct context *context) {
|
||||
TAILQ_FOREACH (bind, bindings, bindings) {
|
||||
/* Abort when we reach the current keybinding, only check the
|
||||
* bindings before */
|
||||
if (bind == current)
|
||||
if (bind == current) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Check if the input types are different */
|
||||
if (bind->input_type != current->input_type)
|
||||
continue;
|
||||
|
||||
/* Check if one is using keysym while the other is using bindsym.
|
||||
* If so, skip. */
|
||||
if ((bind->symbol == NULL && current->symbol != NULL) ||
|
||||
(bind->symbol != NULL && current->symbol == NULL))
|
||||
continue;
|
||||
|
||||
/* If bind is NULL, current has to be NULL, too (see above).
|
||||
* If the keycodes differ, it can't be a duplicate. */
|
||||
if (bind->symbol != NULL &&
|
||||
strcasecmp(bind->symbol, current->symbol) != 0)
|
||||
continue;
|
||||
|
||||
/* Check if the keycodes or modifiers are different. If so, they
|
||||
* can't be duplicate */
|
||||
if (bind->keycode != current->keycode ||
|
||||
bind->event_state_mask != current->event_state_mask ||
|
||||
bind->release != current->release)
|
||||
if (!binding_same_key(bind, current)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
context->has_errors = true;
|
||||
if (current->keycode != 0) {
|
||||
|
@ -56,40 +56,19 @@ typedef struct tokenptr {
|
||||
|
||||
#include "GENERATED_command_tokens.h"
|
||||
|
||||
/*******************************************************************************
|
||||
* The (small) stack where identified literals are stored during the parsing
|
||||
* of a single command (like $workspace).
|
||||
******************************************************************************/
|
||||
|
||||
struct stack_entry {
|
||||
/* Just a pointer, not dynamically allocated. */
|
||||
const char *identifier;
|
||||
enum {
|
||||
STACK_STR = 0,
|
||||
STACK_LONG = 1,
|
||||
} type;
|
||||
union {
|
||||
char *str;
|
||||
long num;
|
||||
} val;
|
||||
};
|
||||
|
||||
/* 10 entries should be enough for everybody. */
|
||||
static struct stack_entry stack[10];
|
||||
|
||||
/*
|
||||
* Pushes a string (identified by 'identifier') on the stack. We simply use a
|
||||
* single array, since the number of entries we have to store is very small.
|
||||
*
|
||||
*/
|
||||
static void push_string(const char *identifier, char *str) {
|
||||
static void push_string(struct stack *stack, const char *identifier, char *str) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier != NULL)
|
||||
if (stack->stack[c].identifier != NULL)
|
||||
continue;
|
||||
/* Found a free slot, let’s store it here. */
|
||||
stack[c].identifier = identifier;
|
||||
stack[c].val.str = str;
|
||||
stack[c].type = STACK_STR;
|
||||
stack->stack[c].identifier = identifier;
|
||||
stack->stack[c].val.str = str;
|
||||
stack->stack[c].type = STACK_STR;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -103,15 +82,15 @@ static void push_string(const char *identifier, char *str) {
|
||||
}
|
||||
|
||||
// TODO move to a common util
|
||||
static void push_long(const char *identifier, long num) {
|
||||
static void push_long(struct stack *stack, const char *identifier, long num) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier != NULL) {
|
||||
if (stack->stack[c].identifier != NULL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stack[c].identifier = identifier;
|
||||
stack[c].val.num = num;
|
||||
stack[c].type = STACK_LONG;
|
||||
stack->stack[c].identifier = identifier;
|
||||
stack->stack[c].val.num = num;
|
||||
stack->stack[c].type = STACK_LONG;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -125,36 +104,36 @@ static void push_long(const char *identifier, long num) {
|
||||
}
|
||||
|
||||
// TODO move to a common util
|
||||
static const char *get_string(const char *identifier) {
|
||||
static const char *get_string(struct stack *stack, const char *identifier) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier == NULL)
|
||||
if (stack->stack[c].identifier == NULL)
|
||||
break;
|
||||
if (strcmp(identifier, stack[c].identifier) == 0)
|
||||
return stack[c].val.str;
|
||||
if (strcmp(identifier, stack->stack[c].identifier) == 0)
|
||||
return stack->stack[c].val.str;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// TODO move to a common util
|
||||
static long get_long(const char *identifier) {
|
||||
static long get_long(struct stack *stack, const char *identifier) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier == NULL)
|
||||
if (stack->stack[c].identifier == NULL)
|
||||
break;
|
||||
if (strcmp(identifier, stack[c].identifier) == 0)
|
||||
return stack[c].val.num;
|
||||
if (strcmp(identifier, stack->stack[c].identifier) == 0)
|
||||
return stack->stack[c].val.num;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO move to a common util
|
||||
static void clear_stack(void) {
|
||||
static void clear_stack(struct stack *stack) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].type == STACK_STR)
|
||||
free(stack[c].val.str);
|
||||
stack[c].identifier = NULL;
|
||||
stack[c].val.str = NULL;
|
||||
stack[c].val.num = 0;
|
||||
if (stack->stack[c].type == STACK_STR)
|
||||
free(stack->stack[c].val.str);
|
||||
stack->stack[c].identifier = NULL;
|
||||
stack->stack[c].val.str = NULL;
|
||||
stack->stack[c].val.num = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,9 +142,12 @@ static void clear_stack(void) {
|
||||
******************************************************************************/
|
||||
|
||||
static cmdp_state state;
|
||||
#ifndef TEST_PARSER
|
||||
static Match current_match;
|
||||
#endif
|
||||
/*******************************************************************************
|
||||
* The (small) stack where identified literals are stored during the parsing
|
||||
* of a single command (like $workspace).
|
||||
******************************************************************************/
|
||||
static struct stack stack;
|
||||
static struct CommandResultIR subcommand_output;
|
||||
static struct CommandResultIR command_output;
|
||||
|
||||
@ -176,19 +158,19 @@ static void next_state(const cmdp_token *token) {
|
||||
subcommand_output.json_gen = command_output.json_gen;
|
||||
subcommand_output.client = command_output.client;
|
||||
subcommand_output.needs_tree_render = false;
|
||||
GENERATED_call(token->extra.call_identifier, &subcommand_output);
|
||||
GENERATED_call(¤t_match, &stack, token->extra.call_identifier, &subcommand_output);
|
||||
state = subcommand_output.next_state;
|
||||
/* If any subcommand requires a tree_render(), we need to make the
|
||||
* whole parser result request a tree_render(). */
|
||||
if (subcommand_output.needs_tree_render)
|
||||
command_output.needs_tree_render = true;
|
||||
clear_stack();
|
||||
clear_stack(&stack);
|
||||
return;
|
||||
}
|
||||
|
||||
state = token->next_state;
|
||||
if (state == INITIAL) {
|
||||
clear_stack();
|
||||
clear_stack(&stack);
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,8 +278,9 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client
|
||||
/* A literal. */
|
||||
if (token->name[0] == '\'') {
|
||||
if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) {
|
||||
if (token->identifier != NULL)
|
||||
push_string(token->identifier, sstrdup(token->name + 1));
|
||||
if (token->identifier != NULL) {
|
||||
push_string(&stack, token->identifier, sstrdup(token->name + 1));
|
||||
}
|
||||
walk += strlen(token->name) - 1;
|
||||
next_state(token);
|
||||
token_handled = true;
|
||||
@ -319,8 +302,9 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client
|
||||
if (end == walk)
|
||||
continue;
|
||||
|
||||
if (token->identifier != NULL)
|
||||
push_long(token->identifier, num);
|
||||
if (token->identifier != NULL) {
|
||||
push_long(&stack, token->identifier, num);
|
||||
}
|
||||
|
||||
/* Set walk to the first non-number character */
|
||||
walk = end;
|
||||
@ -333,8 +317,9 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client
|
||||
strcmp(token->name, "word") == 0) {
|
||||
char *str = parse_string(&walk, (token->name[0] != 's'));
|
||||
if (str != NULL) {
|
||||
if (token->identifier)
|
||||
push_string(token->identifier, str);
|
||||
if (token->identifier) {
|
||||
push_string(&stack, token->identifier, str);
|
||||
}
|
||||
/* If we are at the end of a quoted string, skip the ending
|
||||
* double quote. */
|
||||
if (*walk == '"')
|
||||
@ -436,7 +421,7 @@ CommandResult *parse_command(const char *input, yajl_gen gen, ipc_client *client
|
||||
y(map_close);
|
||||
|
||||
free(position);
|
||||
clear_stack();
|
||||
clear_stack(&stack);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
44
src/config.c
44
src/config.c
@ -10,6 +10,9 @@
|
||||
*/
|
||||
#include "all.h"
|
||||
|
||||
#include <libgen.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
char *current_configpath = NULL;
|
||||
@ -17,6 +20,7 @@ char *current_config = NULL;
|
||||
Config config;
|
||||
struct modes_head modes;
|
||||
struct barconfig_head barconfigs = TAILQ_HEAD_INITIALIZER(barconfigs);
|
||||
struct includedfiles_head included_files = TAILQ_HEAD_INITIALIZER(included_files);
|
||||
|
||||
/*
|
||||
* Ungrabs all keys, to be called before re-grabbing the keys because of a
|
||||
@ -225,8 +229,42 @@ bool load_configuration(const char *override_configpath, config_load_t load_type
|
||||
"$XDG_CONFIG_HOME/i3/config, ~/.i3/config, $XDG_CONFIG_DIRS/i3/config "
|
||||
"and " SYSCONFDIR "/i3/config)");
|
||||
}
|
||||
LOG("Parsing configfile %s\n", current_configpath);
|
||||
const bool result = parse_file(current_configpath, load_type != C_VALIDATE);
|
||||
|
||||
IncludedFile *file;
|
||||
while (!TAILQ_EMPTY(&included_files)) {
|
||||
file = TAILQ_FIRST(&included_files);
|
||||
FREE(file->path);
|
||||
TAILQ_REMOVE(&included_files, file, files);
|
||||
FREE(file);
|
||||
}
|
||||
|
||||
char resolved_path[PATH_MAX] = {'\0'};
|
||||
if (realpath(current_configpath, resolved_path) == NULL) {
|
||||
die("realpath(%s): %s", current_configpath, strerror(errno));
|
||||
}
|
||||
|
||||
file = scalloc(1, sizeof(IncludedFile));
|
||||
file->path = sstrdup(resolved_path);
|
||||
TAILQ_INSERT_TAIL(&included_files, file, files);
|
||||
|
||||
LOG("Parsing configfile %s\n", resolved_path);
|
||||
struct stack stack;
|
||||
memset(&stack, '\0', sizeof(struct stack));
|
||||
struct parser_ctx ctx = {
|
||||
.use_nagbar = (load_type != C_VALIDATE),
|
||||
.assume_v4 = false,
|
||||
.stack = &stack,
|
||||
};
|
||||
SLIST_INIT(&(ctx.variables));
|
||||
FREE(current_config);
|
||||
const int result = parse_file(&ctx, resolved_path);
|
||||
free_variables(&ctx);
|
||||
if (result == -1) {
|
||||
die("Could not open configuration file: %s\n", strerror(errno));
|
||||
}
|
||||
|
||||
extract_workspace_names_from_bindings();
|
||||
reorder_bindings();
|
||||
|
||||
if (config.font.type == FONT_TYPE_NONE && load_type != C_VALIDATE) {
|
||||
ELOG("You did not specify required configuration option \"font\"\n");
|
||||
@ -245,5 +283,5 @@ bool load_configuration(const char *override_configpath, config_load_t load_type
|
||||
xcb_flush(conn);
|
||||
}
|
||||
|
||||
return result;
|
||||
return result == 0;
|
||||
}
|
||||
|
@ -9,6 +9,85 @@
|
||||
*/
|
||||
#include "all.h"
|
||||
|
||||
#include <wordexp.h>
|
||||
|
||||
/*******************************************************************************
|
||||
* Include functions.
|
||||
******************************************************************************/
|
||||
|
||||
CFGFUN(include, const char *pattern) {
|
||||
DLOG("include %s\n", pattern);
|
||||
|
||||
wordexp_t p;
|
||||
const int ret = wordexp(pattern, &p, 0);
|
||||
if (ret != 0) {
|
||||
ELOG("wordexp(%s): error %d\n", pattern, ret);
|
||||
result->has_errors = true;
|
||||
return;
|
||||
}
|
||||
char **w = p.we_wordv;
|
||||
for (size_t i = 0; i < p.we_wordc; i++) {
|
||||
char resolved_path[PATH_MAX] = {'\0'};
|
||||
if (realpath(w[i], resolved_path) == NULL) {
|
||||
ELOG("realpath(%s): %s\n", w[i], strerror(errno));
|
||||
result->has_errors = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool skip = false;
|
||||
IncludedFile *file;
|
||||
TAILQ_FOREACH (file, &included_files, files) {
|
||||
if (strcmp(file->path, resolved_path) == 0) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (skip) {
|
||||
LOG("Skipping file %s (already included)\n", resolved_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG("Including config file %s\n", resolved_path);
|
||||
|
||||
file = scalloc(1, sizeof(IncludedFile));
|
||||
file->path = sstrdup(resolved_path);
|
||||
TAILQ_INSERT_TAIL(&included_files, file, files);
|
||||
|
||||
struct stack stack;
|
||||
memset(&stack, '\0', sizeof(struct stack));
|
||||
struct parser_ctx ctx = {
|
||||
.use_nagbar = result->ctx->use_nagbar,
|
||||
/* The include mechanism was added in v4, so we can skip the
|
||||
* auto-detection and get rid of the risk of detecting the wrong
|
||||
* version in potentially very short include fragments: */
|
||||
.assume_v4 = true,
|
||||
.stack = &stack,
|
||||
.variables = result->ctx->variables,
|
||||
};
|
||||
switch (parse_file(&ctx, resolved_path)) {
|
||||
case PARSE_FILE_SUCCESS:
|
||||
break;
|
||||
|
||||
case PARSE_FILE_FAILED:
|
||||
ELOG("including config file %s: %s\n", resolved_path, strerror(errno));
|
||||
/* fallthrough */
|
||||
|
||||
case PARSE_FILE_CONFIG_ERRORS:
|
||||
result->has_errors = true;
|
||||
TAILQ_REMOVE(&included_files, file, files);
|
||||
FREE(file->path);
|
||||
FREE(file);
|
||||
break;
|
||||
|
||||
default:
|
||||
/* missing case statement */
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
wordfree(&p);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* Criteria functions.
|
||||
******************************************************************************/
|
||||
|
@ -35,18 +35,14 @@
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <libgen.h>
|
||||
|
||||
#include <xcb/xcb_xrm.h>
|
||||
|
||||
// Macros to make the YAJL API a bit easier to use.
|
||||
#define y(x, ...) yajl_gen_##x(command_output.json_gen, ##__VA_ARGS__)
|
||||
#define ystr(str) yajl_gen_string(command_output.json_gen, (unsigned char *)str, strlen(str))
|
||||
|
||||
xcb_xrm_database_t *database = NULL;
|
||||
|
||||
#ifndef TEST_PARSER
|
||||
pid_t config_error_nagbar_pid = -1;
|
||||
static struct context *context;
|
||||
#endif
|
||||
|
||||
/*******************************************************************************
|
||||
@ -76,46 +72,25 @@ typedef struct tokenptr {
|
||||
|
||||
#include "GENERATED_config_tokens.h"
|
||||
|
||||
/*******************************************************************************
|
||||
* The (small) stack where identified literals are stored during the parsing
|
||||
* of a single command (like $workspace).
|
||||
******************************************************************************/
|
||||
|
||||
struct stack_entry {
|
||||
/* Just a pointer, not dynamically allocated. */
|
||||
const char *identifier;
|
||||
enum {
|
||||
STACK_STR = 0,
|
||||
STACK_LONG = 1,
|
||||
} type;
|
||||
union {
|
||||
char *str;
|
||||
long num;
|
||||
} val;
|
||||
};
|
||||
|
||||
/* 10 entries should be enough for everybody. */
|
||||
static struct stack_entry stack[10];
|
||||
|
||||
/*
|
||||
* Pushes a string (identified by 'identifier') on the stack. We simply use a
|
||||
* single array, since the number of entries we have to store is very small.
|
||||
*
|
||||
*/
|
||||
static void push_string(const char *identifier, const char *str) {
|
||||
static void push_string(struct stack *ctx, const char *identifier, const char *str) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier != NULL &&
|
||||
strcmp(stack[c].identifier, identifier) != 0)
|
||||
if (ctx->stack[c].identifier != NULL &&
|
||||
strcmp(ctx->stack[c].identifier, identifier) != 0)
|
||||
continue;
|
||||
if (stack[c].identifier == NULL) {
|
||||
if (ctx->stack[c].identifier == NULL) {
|
||||
/* Found a free slot, let’s store it here. */
|
||||
stack[c].identifier = identifier;
|
||||
stack[c].val.str = sstrdup(str);
|
||||
stack[c].type = STACK_STR;
|
||||
ctx->stack[c].identifier = identifier;
|
||||
ctx->stack[c].val.str = sstrdup(str);
|
||||
ctx->stack[c].type = STACK_STR;
|
||||
} else {
|
||||
/* Append the value. */
|
||||
char *prev = stack[c].val.str;
|
||||
sasprintf(&(stack[c].val.str), "%s,%s", prev, str);
|
||||
char *prev = ctx->stack[c].val.str;
|
||||
sasprintf(&(ctx->stack[c].val.str), "%s,%s", prev, str);
|
||||
free(prev);
|
||||
}
|
||||
return;
|
||||
@ -130,14 +105,15 @@ static void push_string(const char *identifier, const char *str) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static void push_long(const char *identifier, long num) {
|
||||
static void push_long(struct stack *ctx, const char *identifier, long num) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier != NULL)
|
||||
if (ctx->stack[c].identifier != NULL) {
|
||||
continue;
|
||||
}
|
||||
/* Found a free slot, let’s store it here. */
|
||||
stack[c].identifier = identifier;
|
||||
stack[c].val.num = num;
|
||||
stack[c].type = STACK_LONG;
|
||||
ctx->stack[c].identifier = identifier;
|
||||
ctx->stack[c].val.num = num;
|
||||
ctx->stack[c].type = STACK_LONG;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -150,33 +126,33 @@ static void push_long(const char *identifier, long num) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static const char *get_string(const char *identifier) {
|
||||
static const char *get_string(struct stack *ctx, const char *identifier) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier == NULL)
|
||||
if (ctx->stack[c].identifier == NULL)
|
||||
break;
|
||||
if (strcmp(identifier, stack[c].identifier) == 0)
|
||||
return stack[c].val.str;
|
||||
if (strcmp(identifier, ctx->stack[c].identifier) == 0)
|
||||
return ctx->stack[c].val.str;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static long get_long(const char *identifier) {
|
||||
static long get_long(struct stack *ctx, const char *identifier) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].identifier == NULL)
|
||||
if (ctx->stack[c].identifier == NULL)
|
||||
break;
|
||||
if (strcmp(identifier, stack[c].identifier) == 0)
|
||||
return stack[c].val.num;
|
||||
if (strcmp(identifier, ctx->stack[c].identifier) == 0)
|
||||
return ctx->stack[c].val.num;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void clear_stack(void) {
|
||||
static void clear_stack(struct stack *ctx) {
|
||||
for (int c = 0; c < 10; c++) {
|
||||
if (stack[c].type == STACK_STR)
|
||||
free(stack[c].val.str);
|
||||
stack[c].identifier = NULL;
|
||||
stack[c].val.str = NULL;
|
||||
stack[c].val.num = 0;
|
||||
if (ctx->stack[c].type == STACK_STR)
|
||||
free(ctx->stack[c].val.str);
|
||||
ctx->stack[c].identifier = NULL;
|
||||
ctx->stack[c].val.str = NULL;
|
||||
ctx->stack[c].val.num = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,50 +160,42 @@ static void clear_stack(void) {
|
||||
* The parser itself.
|
||||
******************************************************************************/
|
||||
|
||||
static cmdp_state state;
|
||||
static Match current_match;
|
||||
static struct ConfigResultIR subcommand_output;
|
||||
static struct ConfigResultIR command_output;
|
||||
|
||||
/* A list which contains the states that lead to the current state, e.g.
|
||||
* INITIAL, WORKSPACE_LAYOUT.
|
||||
* When jumping back to INITIAL, statelist_idx will simply be set to 1
|
||||
* (likewise for other states, e.g. MODE or BAR).
|
||||
* This list is used to process the nearest error token. */
|
||||
static cmdp_state statelist[10] = {INITIAL};
|
||||
/* NB: statelist_idx points to where the next entry will be inserted */
|
||||
static int statelist_idx = 1;
|
||||
|
||||
#include "GENERATED_config_call.h"
|
||||
|
||||
static void next_state(const cmdp_token *token) {
|
||||
static void next_state(const cmdp_token *token, struct parser_ctx *ctx) {
|
||||
cmdp_state _next_state = token->next_state;
|
||||
|
||||
//printf("token = name %s identifier %s\n", token->name, token->identifier);
|
||||
//printf("next_state = %d\n", token->next_state);
|
||||
if (token->next_state == __CALL) {
|
||||
subcommand_output.json_gen = command_output.json_gen;
|
||||
GENERATED_call(token->extra.call_identifier, &subcommand_output);
|
||||
struct ConfigResultIR subcommand_output = {
|
||||
.ctx = ctx,
|
||||
};
|
||||
GENERATED_call(&(ctx->current_match), ctx->stack, token->extra.call_identifier, &subcommand_output);
|
||||
if (subcommand_output.has_errors) {
|
||||
ctx->has_errors = true;
|
||||
}
|
||||
_next_state = subcommand_output.next_state;
|
||||
clear_stack();
|
||||
clear_stack(ctx->stack);
|
||||
}
|
||||
|
||||
state = _next_state;
|
||||
if (state == INITIAL) {
|
||||
clear_stack();
|
||||
ctx->state = _next_state;
|
||||
if (ctx->state == INITIAL) {
|
||||
clear_stack(ctx->stack);
|
||||
}
|
||||
|
||||
/* See if we are jumping back to a state in which we were in previously
|
||||
* (statelist contains INITIAL) and just move statelist_idx accordingly. */
|
||||
for (int i = 0; i < statelist_idx; i++) {
|
||||
if (statelist[i] != _next_state)
|
||||
for (int i = 0; i < ctx->statelist_idx; i++) {
|
||||
if ((cmdp_state)(ctx->statelist[i]) != _next_state) {
|
||||
continue;
|
||||
statelist_idx = i + 1;
|
||||
}
|
||||
ctx->statelist_idx = i + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Otherwise, the state is new and we add it to the list */
|
||||
statelist[statelist_idx++] = _next_state;
|
||||
ctx->statelist[ctx->statelist_idx++] = _next_state;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -257,7 +225,7 @@ static char *single_line(const char *start) {
|
||||
return result;
|
||||
}
|
||||
|
||||
struct ConfigResultIR *parse_config(const char *input, struct context *context) {
|
||||
static void parse_config(struct parser_ctx *ctx, const char *input, struct context *context) {
|
||||
/* Dump the entire config file into the debug log. We cannot just use
|
||||
* DLOG("%s", input); because one log message must not exceed 4 KiB. */
|
||||
const char *dumpwalk = input;
|
||||
@ -273,13 +241,11 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
}
|
||||
linecnt++;
|
||||
}
|
||||
state = INITIAL;
|
||||
statelist_idx = 1;
|
||||
|
||||
/* A YAJL JSON generator used for formatting replies. */
|
||||
command_output.json_gen = yajl_gen_alloc(NULL);
|
||||
|
||||
y(array_open);
|
||||
ctx->state = INITIAL;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ctx->statelist[i] = INITIAL;
|
||||
}
|
||||
ctx->statelist_idx = 1;
|
||||
|
||||
const char *walk = input;
|
||||
const size_t len = strlen(input);
|
||||
@ -290,7 +256,10 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
|
||||
// TODO: make this testable
|
||||
#ifndef TEST_PARSER
|
||||
cfg_criteria_init(¤t_match, &subcommand_output, INITIAL);
|
||||
struct ConfigResultIR subcommand_output = {
|
||||
.ctx = ctx,
|
||||
};
|
||||
cfg_criteria_init(&(ctx->current_match), &subcommand_output, INITIAL);
|
||||
#endif
|
||||
|
||||
/* The "<=" operator is intentional: We also handle the terminating 0-byte
|
||||
@ -303,7 +272,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
|
||||
//printf("remaining input: %s\n", walk);
|
||||
|
||||
cmdp_token_ptr *ptr = &(tokens[state]);
|
||||
cmdp_token_ptr *ptr = &(tokens[ctx->state]);
|
||||
token_handled = false;
|
||||
for (c = 0; c < ptr->n; c++) {
|
||||
token = &(ptr->array[c]);
|
||||
@ -311,10 +280,11 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
/* A literal. */
|
||||
if (token->name[0] == '\'') {
|
||||
if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) {
|
||||
if (token->identifier != NULL)
|
||||
push_string(token->identifier, token->name + 1);
|
||||
if (token->identifier != NULL) {
|
||||
push_string(ctx->stack, token->identifier, token->name + 1);
|
||||
}
|
||||
walk += strlen(token->name) - 1;
|
||||
next_state(token);
|
||||
next_state(token, ctx);
|
||||
token_handled = true;
|
||||
break;
|
||||
}
|
||||
@ -334,12 +304,13 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
if (end == walk)
|
||||
continue;
|
||||
|
||||
if (token->identifier != NULL)
|
||||
push_long(token->identifier, num);
|
||||
if (token->identifier != NULL) {
|
||||
push_long(ctx->stack, token->identifier, num);
|
||||
}
|
||||
|
||||
/* Set walk to the first non-number character */
|
||||
walk = end;
|
||||
next_state(token);
|
||||
next_state(token, ctx);
|
||||
token_handled = true;
|
||||
break;
|
||||
}
|
||||
@ -382,14 +353,15 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
inpos++;
|
||||
str[outpos] = beginning[inpos];
|
||||
}
|
||||
if (token->identifier)
|
||||
push_string(token->identifier, str);
|
||||
if (token->identifier) {
|
||||
push_string(ctx->stack, token->identifier, str);
|
||||
}
|
||||
free(str);
|
||||
/* If we are at the end of a quoted string, skip the ending
|
||||
* double quote. */
|
||||
if (*walk == '"')
|
||||
walk++;
|
||||
next_state(token);
|
||||
next_state(token, ctx);
|
||||
token_handled = true;
|
||||
break;
|
||||
}
|
||||
@ -398,7 +370,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
if (strcmp(token->name, "line") == 0) {
|
||||
while (*walk != '\0' && *walk != '\n' && *walk != '\r')
|
||||
walk++;
|
||||
next_state(token);
|
||||
next_state(token, ctx);
|
||||
token_handled = true;
|
||||
linecnt++;
|
||||
walk++;
|
||||
@ -408,7 +380,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
if (strcmp(token->name, "end") == 0) {
|
||||
//printf("checking for end: *%s*\n", walk);
|
||||
if (*walk == '\0' || *walk == '\n' || *walk == '\r') {
|
||||
next_state(token);
|
||||
next_state(token, ctx);
|
||||
token_handled = true;
|
||||
/* To make sure we start with an appropriate matching
|
||||
* datastructure for commands which do *not* specify any
|
||||
@ -416,7 +388,7 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
* every command. */
|
||||
// TODO: make this testable
|
||||
#ifndef TEST_PARSER
|
||||
cfg_criteria_init(¤t_match, &subcommand_output, INITIAL);
|
||||
cfg_criteria_init(&(ctx->current_match), &subcommand_output, INITIAL);
|
||||
#endif
|
||||
linecnt++;
|
||||
walk++;
|
||||
@ -515,41 +487,24 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
|
||||
context->has_errors = true;
|
||||
|
||||
/* Format this error message as a JSON reply. */
|
||||
y(map_open);
|
||||
ystr("success");
|
||||
y(bool, false);
|
||||
/* We set parse_error to true to distinguish this from other
|
||||
* errors. i3-nagbar is spawned upon keypresses only for parser
|
||||
* errors. */
|
||||
ystr("parse_error");
|
||||
y(bool, true);
|
||||
ystr("error");
|
||||
ystr(errormessage);
|
||||
ystr("input");
|
||||
ystr(input);
|
||||
ystr("errorposition");
|
||||
ystr(position);
|
||||
y(map_close);
|
||||
|
||||
/* Skip the rest of this line, but continue parsing. */
|
||||
while ((size_t)(walk - input) <= len && *walk != '\n')
|
||||
walk++;
|
||||
|
||||
free(position);
|
||||
free(errormessage);
|
||||
clear_stack();
|
||||
clear_stack(ctx->stack);
|
||||
|
||||
/* To figure out in which state to go (e.g. MODE or INITIAL),
|
||||
* we find the nearest state which contains an <error> token
|
||||
* and follow that one. */
|
||||
bool error_token_found = false;
|
||||
for (int i = statelist_idx - 1; (i >= 0) && !error_token_found; i--) {
|
||||
cmdp_token_ptr *errptr = &(tokens[statelist[i]]);
|
||||
for (int i = ctx->statelist_idx - 1; (i >= 0) && !error_token_found; i--) {
|
||||
cmdp_token_ptr *errptr = &(tokens[ctx->statelist[i]]);
|
||||
for (int j = 0; j < errptr->n; j++) {
|
||||
if (strcmp(errptr->array[j].name, "error") != 0)
|
||||
continue;
|
||||
next_state(&(errptr->array[j]));
|
||||
next_state(&(errptr->array[j]), ctx);
|
||||
error_token_found = true;
|
||||
break;
|
||||
}
|
||||
@ -558,10 +513,6 @@ struct ConfigResultIR *parse_config(const char *input, struct context *context)
|
||||
assert(error_token_found);
|
||||
}
|
||||
}
|
||||
|
||||
y(array_close);
|
||||
|
||||
return &command_output;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@ -612,9 +563,17 @@ int main(int argc, char *argv[]) {
|
||||
fprintf(stderr, "Syntax: %s <command>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
struct stack stack;
|
||||
memset(&stack, '\0', sizeof(struct stack));
|
||||
struct parser_ctx ctx = {
|
||||
.use_nagbar = false,
|
||||
.assume_v4 = false,
|
||||
.stack = &stack,
|
||||
};
|
||||
SLIST_INIT(&(ctx.variables));
|
||||
struct context context;
|
||||
context.filename = "<stdin>";
|
||||
parse_config(argv[1], &context);
|
||||
parse_config(&ctx, argv[1], &context);
|
||||
}
|
||||
|
||||
#else
|
||||
@ -636,6 +595,7 @@ static int detect_version(char *buf) {
|
||||
|
||||
/* check for some v4-only statements */
|
||||
if (strncasecmp(line, "bindcode", strlen("bindcode")) == 0 ||
|
||||
strncasecmp(line, "include", strlen("include")) == 0 ||
|
||||
strncasecmp(line, "force_focus_wrapping", strlen("force_focus_wrapping")) == 0 ||
|
||||
strncasecmp(line, "# i3 config file (v4)", strlen("# i3 config file (v4)")) == 0 ||
|
||||
strncasecmp(line, "workspace_layout", strlen("workspace_layout")) == 0) {
|
||||
@ -877,36 +837,67 @@ static char *get_resource(char *name) {
|
||||
return resource;
|
||||
}
|
||||
|
||||
/*
|
||||
* Releases the memory of all variables in ctx.
|
||||
*
|
||||
*/
|
||||
void free_variables(struct parser_ctx *ctx) {
|
||||
struct Variable *current;
|
||||
while (!SLIST_EMPTY(&(ctx->variables))) {
|
||||
current = SLIST_FIRST(&(ctx->variables));
|
||||
FREE(current->key);
|
||||
FREE(current->value);
|
||||
SLIST_REMOVE_HEAD(&(ctx->variables), variables);
|
||||
FREE(current);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Parses the given file by first replacing the variables, then calling
|
||||
* parse_config and possibly launching i3-nagbar.
|
||||
*
|
||||
*/
|
||||
bool parse_file(const char *f, bool use_nagbar) {
|
||||
struct variables_head variables = SLIST_HEAD_INITIALIZER(&variables);
|
||||
parse_file_result_t parse_file(struct parser_ctx *ctx, const char *f) {
|
||||
int fd;
|
||||
struct stat stbuf;
|
||||
char *buf;
|
||||
FILE *fstr;
|
||||
char buffer[4096], key[512], value[4096], *continuation = NULL;
|
||||
|
||||
if ((fd = open(f, O_RDONLY)) == -1)
|
||||
die("Could not open configuration file: %s\n", strerror(errno));
|
||||
char *old_dir = get_current_dir_name();
|
||||
char *dir = NULL;
|
||||
/* dirname(3) might modify the buffer, so make a copy: */
|
||||
char *dirbuf = sstrdup(f);
|
||||
if ((dir = dirname(dirbuf)) != NULL) {
|
||||
LOG("Changing working directory to config file directory %s\n", dir);
|
||||
if (chdir(dir) == -1) {
|
||||
ELOG("chdir(%s) failed: %s\n", dir, strerror(errno));
|
||||
return PARSE_FILE_FAILED;
|
||||
}
|
||||
}
|
||||
free(dirbuf);
|
||||
|
||||
if (fstat(fd, &stbuf) == -1)
|
||||
die("Could not fstat file: %s\n", strerror(errno));
|
||||
if ((fd = open(f, O_RDONLY)) == -1) {
|
||||
return PARSE_FILE_FAILED;
|
||||
}
|
||||
|
||||
if (fstat(fd, &stbuf) == -1) {
|
||||
return PARSE_FILE_FAILED;
|
||||
}
|
||||
|
||||
buf = scalloc(stbuf.st_size + 1, 1);
|
||||
|
||||
if ((fstr = fdopen(fd, "r")) == NULL)
|
||||
die("Could not fdopen: %s\n", strerror(errno));
|
||||
|
||||
FREE(current_config);
|
||||
current_config = scalloc(stbuf.st_size + 1, 1);
|
||||
if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) {
|
||||
die("Could not fread: %s\n", strerror(errno));
|
||||
if ((fstr = fdopen(fd, "r")) == NULL) {
|
||||
return PARSE_FILE_FAILED;
|
||||
}
|
||||
|
||||
if (current_config == NULL) {
|
||||
current_config = scalloc(stbuf.st_size + 1, 1);
|
||||
if ((ssize_t)fread(current_config, 1, stbuf.st_size, fstr) != stbuf.st_size) {
|
||||
return PARSE_FILE_FAILED;
|
||||
}
|
||||
rewind(fstr);
|
||||
}
|
||||
rewind(fstr);
|
||||
|
||||
bool invalid_sets = false;
|
||||
|
||||
@ -916,7 +907,7 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
if (fgets(continuation, sizeof(buffer) - (continuation - buffer), fstr) == NULL) {
|
||||
if (feof(fstr))
|
||||
break;
|
||||
die("Could not read configuration file\n");
|
||||
return PARSE_FILE_FAILED;
|
||||
}
|
||||
if (buffer[strlen(buffer) - 1] != '\n' && !feof(fstr)) {
|
||||
ELOG("Your line continuation is too long, it exceeds %zd bytes\n", sizeof(buffer));
|
||||
@ -960,7 +951,7 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
continue;
|
||||
}
|
||||
|
||||
upsert_variable(&variables, v_key, v_value);
|
||||
upsert_variable(&(ctx->variables), v_key, v_value);
|
||||
continue;
|
||||
} else if (strcasecmp(key, "set_from_resource") == 0) {
|
||||
char res_name[512] = {'\0'};
|
||||
@ -993,7 +984,7 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
res_value = sstrdup(fallback);
|
||||
}
|
||||
|
||||
upsert_variable(&variables, v_key, res_value);
|
||||
upsert_variable(&(ctx->variables), v_key, res_value);
|
||||
FREE(res_value);
|
||||
continue;
|
||||
}
|
||||
@ -1014,7 +1005,7 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
* variables (otherwise we will count them twice, which is bad when
|
||||
* 'extra' is negative) */
|
||||
char *bufcopy = sstrdup(buf);
|
||||
SLIST_FOREACH (current, &variables, variables) {
|
||||
SLIST_FOREACH (current, &(ctx->variables), variables) {
|
||||
int extra = (strlen(current->value) - strlen(current->key));
|
||||
char *next;
|
||||
for (next = bufcopy;
|
||||
@ -1034,12 +1025,12 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
destwalk = new;
|
||||
while (walk < (buf + stbuf.st_size)) {
|
||||
/* Find the next variable */
|
||||
SLIST_FOREACH (current, &variables, variables) {
|
||||
SLIST_FOREACH (current, &(ctx->variables), variables) {
|
||||
current->next_match = strcasestr(walk, current->key);
|
||||
}
|
||||
nearest = NULL;
|
||||
int distance = stbuf.st_size;
|
||||
SLIST_FOREACH (current, &variables, variables) {
|
||||
SLIST_FOREACH (current, &(ctx->variables), variables) {
|
||||
if (current->next_match == NULL)
|
||||
continue;
|
||||
if ((current->next_match - walk) < distance) {
|
||||
@ -1064,7 +1055,10 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
|
||||
/* analyze the string to find out whether this is an old config file (3.x)
|
||||
* or a new config file (4.x). If it’s old, we run the converter script. */
|
||||
int version = detect_version(buf);
|
||||
int version = 4;
|
||||
if (!ctx->assume_v4) {
|
||||
version = detect_version(buf);
|
||||
}
|
||||
if (version == 3) {
|
||||
/* We need to convert this v3 configuration */
|
||||
char *converted = migrate_config(new, strlen(new));
|
||||
@ -1090,17 +1084,16 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
}
|
||||
}
|
||||
|
||||
context = scalloc(1, sizeof(struct context));
|
||||
struct context *context = scalloc(1, sizeof(struct context));
|
||||
context->filename = f;
|
||||
parse_config(ctx, new, context);
|
||||
if (ctx->has_errors) {
|
||||
context->has_errors = true;
|
||||
}
|
||||
|
||||
struct ConfigResultIR *config_output = parse_config(new, context);
|
||||
yajl_gen_free(config_output->json_gen);
|
||||
|
||||
extract_workspace_names_from_bindings();
|
||||
check_for_duplicate_bindings(context);
|
||||
reorder_bindings();
|
||||
|
||||
if (use_nagbar && (context->has_errors || context->has_warnings || invalid_sets)) {
|
||||
if (ctx->use_nagbar && (context->has_errors || context->has_warnings || invalid_sets)) {
|
||||
ELOG("FYI: You are using i3 version %s\n", i3_version);
|
||||
if (version == 3)
|
||||
ELOG("Please convert your configfile first, then fix any remaining errors (see above).\n");
|
||||
@ -1108,22 +1101,22 @@ bool parse_file(const char *f, bool use_nagbar) {
|
||||
start_config_error_nagbar(f, context->has_errors || invalid_sets);
|
||||
}
|
||||
|
||||
bool has_errors = context->has_errors;
|
||||
const bool has_errors = context->has_errors;
|
||||
|
||||
FREE(context->line_copy);
|
||||
free(context);
|
||||
free(new);
|
||||
free(buf);
|
||||
|
||||
while (!SLIST_EMPTY(&variables)) {
|
||||
current = SLIST_FIRST(&variables);
|
||||
FREE(current->key);
|
||||
FREE(current->value);
|
||||
SLIST_REMOVE_HEAD(&variables, variables);
|
||||
FREE(current);
|
||||
if (chdir(old_dir) == -1) {
|
||||
ELOG("chdir(%s) failed: %s\n", old_dir, strerror(errno));
|
||||
return PARSE_FILE_FAILED;
|
||||
}
|
||||
|
||||
return !has_errors;
|
||||
free(old_dir);
|
||||
if (has_errors) {
|
||||
return PARSE_FILE_CONFIG_ERRORS;
|
||||
}
|
||||
return PARSE_FILE_SUCCESS;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -14,22 +14,34 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static bool human_readable_key, loaded_config_file_name_key;
|
||||
static char *human_readable_version, *loaded_config_file_name;
|
||||
static bool human_readable_key;
|
||||
static bool loaded_config_file_name_key;
|
||||
static bool included_config_file_names;
|
||||
|
||||
static char *human_readable_version;
|
||||
static char *loaded_config_file_name;
|
||||
|
||||
static int version_string(void *ctx, const unsigned char *val, size_t len) {
|
||||
if (human_readable_key)
|
||||
if (human_readable_key) {
|
||||
sasprintf(&human_readable_version, "%.*s", (int)len, val);
|
||||
if (loaded_config_file_name_key)
|
||||
}
|
||||
if (loaded_config_file_name_key) {
|
||||
sasprintf(&loaded_config_file_name, "%.*s", (int)len, val);
|
||||
}
|
||||
if (included_config_file_names) {
|
||||
IncludedFile *file = scalloc(1, sizeof(IncludedFile));
|
||||
sasprintf(&(file->path), "%.*s", (int)len, val);
|
||||
TAILQ_INSERT_TAIL(&included_files, file, files);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int version_map_key(void *ctx, const unsigned char *stringval, size_t stringlen) {
|
||||
human_readable_key = (stringlen == strlen("human_readable") &&
|
||||
strncmp((const char *)stringval, "human_readable", strlen("human_readable")) == 0);
|
||||
loaded_config_file_name_key = (stringlen == strlen("loaded_config_file_name") &&
|
||||
strncmp((const char *)stringval, "loaded_config_file_name", strlen("loaded_config_file_name")) == 0);
|
||||
#define KEY_MATCHES(x) (stringlen == strlen(x) && strncmp((const char *)stringval, x, strlen(x)) == 0)
|
||||
human_readable_key = KEY_MATCHES("human_readable");
|
||||
loaded_config_file_name_key = KEY_MATCHES("loaded_config_file_name");
|
||||
included_config_file_names = KEY_MATCHES("included_config_file_names");
|
||||
#undef KEY_MATCHES
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -38,6 +50,22 @@ static yajl_callbacks version_callbacks = {
|
||||
.yajl_map_key = version_map_key,
|
||||
};
|
||||
|
||||
static void print_config_path(const char *path, const char *role) {
|
||||
struct stat sb;
|
||||
time_t now;
|
||||
char mtime[64];
|
||||
|
||||
printf(" %s (%s)", path, role);
|
||||
if (stat(path, &sb) == -1) {
|
||||
printf("\n");
|
||||
ELOG("Cannot stat config file \"%s\"\n", path);
|
||||
} else {
|
||||
strftime(mtime, sizeof(mtime), "%c", localtime(&(sb.st_mtime)));
|
||||
time(&now);
|
||||
printf(" (last modified: %s, %.f seconds ago)\n", mtime, difftime(now, sb.st_mtime));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Connects to i3 to find out the currently running version. Useful since it
|
||||
* might be different from the version compiled into this binary (maybe the
|
||||
@ -98,17 +126,11 @@ void display_running_version(void) {
|
||||
printf("Running i3 version: %s (pid %s)\n", human_readable_version, pid_from_atom);
|
||||
|
||||
if (loaded_config_file_name) {
|
||||
struct stat sb;
|
||||
time_t now;
|
||||
char mtime[64];
|
||||
printf("Loaded i3 config: %s", loaded_config_file_name);
|
||||
if (stat(loaded_config_file_name, &sb) == -1) {
|
||||
printf("\n");
|
||||
ELOG("Cannot stat config file \"%s\"\n", loaded_config_file_name);
|
||||
} else {
|
||||
strftime(mtime, sizeof(mtime), "%c", localtime(&(sb.st_mtime)));
|
||||
time(&now);
|
||||
printf(" (Last modified: %s, %.f seconds ago)\n", mtime, difftime(now, sb.st_mtime));
|
||||
printf("Loaded i3 config:\n");
|
||||
print_config_path(loaded_config_file_name, "main");
|
||||
IncludedFile *file;
|
||||
TAILQ_FOREACH (file, &included_files, files) {
|
||||
print_config_path(file->path, "included");
|
||||
}
|
||||
}
|
||||
|
||||
|
11
src/ipc.c
11
src/ipc.c
@ -1040,6 +1040,17 @@ IPC_HANDLER(get_version) {
|
||||
ystr("loaded_config_file_name");
|
||||
ystr(current_configpath);
|
||||
|
||||
ystr("included_config_file_names");
|
||||
y(array_open);
|
||||
IncludedFile *file;
|
||||
TAILQ_FOREACH (file, &included_files, files) {
|
||||
if (file == TAILQ_FIRST(&included_files)) {
|
||||
/* Skip the first file, which is current_configpath. */
|
||||
continue;
|
||||
}
|
||||
ystr(file->path);
|
||||
}
|
||||
y(array_close);
|
||||
y(map_close);
|
||||
|
||||
const unsigned char *payload;
|
||||
|
@ -506,6 +506,7 @@ EOT
|
||||
|
||||
my $expected_all_tokens = "ERROR: CONFIG: Expected one of these tokens: <end>, '#', '" . join("', '", 'set ', 'set ', qw(
|
||||
set_from_resource
|
||||
include
|
||||
bindsym
|
||||
bindcode
|
||||
bind
|
||||
|
338
testcases/t/313-include.t
Normal file
338
testcases/t/313-include.t
Normal file
@ -0,0 +1,338 @@
|
||||
#!perl
|
||||
# vim:ts=4:sw=4:expandtab
|
||||
#
|
||||
# Please read the following documents before working on tests:
|
||||
# • https://build.i3wm.org/docs/testsuite.html
|
||||
# (or docs/testsuite)
|
||||
#
|
||||
# • https://build.i3wm.org/docs/lib-i3test.html
|
||||
# (alternatively: perldoc ./testcases/lib/i3test.pm)
|
||||
#
|
||||
# • https://build.i3wm.org/docs/ipc.html
|
||||
# (or docs/ipc)
|
||||
#
|
||||
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
|
||||
# (unless you are already familiar with Perl)
|
||||
#
|
||||
# Verifies the include directive.
|
||||
|
||||
use File::Temp qw(tempfile tempdir);
|
||||
use File::Basename qw(basename);
|
||||
use i3test i3_autostart => 0;
|
||||
|
||||
# starts i3 with the given config, opens a window, returns its border style
|
||||
sub launch_get_border {
|
||||
my ($config) = @_;
|
||||
|
||||
my $pid = launch_with_config($config);
|
||||
|
||||
my $i3 = i3(get_socket_path(0));
|
||||
my $tmp = fresh_workspace;
|
||||
|
||||
my $window = open_window(name => 'special title');
|
||||
|
||||
my @content = @{get_ws_content($tmp)};
|
||||
cmp_ok(@content, '==', 1, 'one node on this workspace now');
|
||||
my $border = $content[0]->{border};
|
||||
|
||||
exit_gracefully($pid);
|
||||
|
||||
return $border;
|
||||
}
|
||||
|
||||
#####################################################################
|
||||
# test thet windows get the default border
|
||||
#####################################################################
|
||||
|
||||
my $config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'normal', 'normal border');
|
||||
|
||||
#####################################################################
|
||||
# now use a variable and for_window
|
||||
#####################################################################
|
||||
|
||||
my ($fh, $filename) = tempfile(UNLINK => 1);
|
||||
print $fh <<'EOT';
|
||||
set $vartest special title
|
||||
for_window [title="$vartest"] border none
|
||||
EOT
|
||||
$fh->flush;
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $filename
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# nested includes
|
||||
################################################################################
|
||||
|
||||
my ($indirectfh, $indirectfilename) = tempfile(UNLINK => 1);
|
||||
print $indirectfh <<EOT;
|
||||
include $filename
|
||||
EOT
|
||||
$indirectfh->flush;
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $indirectfilename
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# nested includes with relative paths
|
||||
################################################################################
|
||||
|
||||
my $relative = basename($filename);
|
||||
my ($indirectfh2, $indirectfilename2) = tempfile(UNLINK => 1);
|
||||
print $indirectfh2 <<EOT;
|
||||
include $relative
|
||||
EOT
|
||||
$indirectfh2->flush;
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $indirectfilename2
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# command substitution
|
||||
################################################################################
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include `echo $filename`
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# failing command substitution
|
||||
################################################################################
|
||||
|
||||
$config = <<'EOT';
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include i3-`false`.conf
|
||||
|
||||
set $vartest special title
|
||||
for_window [title="$vartest"] border none
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# permission denied
|
||||
################################################################################
|
||||
|
||||
my ($permissiondeniedfh, $permissiondenied) = tempfile(UNLINK => 1);
|
||||
$permissiondeniedfh->flush;
|
||||
my $mode = 0055;
|
||||
chmod($mode, $permissiondenied);
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $permissiondenied
|
||||
include $filename
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# dangling symlink
|
||||
################################################################################
|
||||
|
||||
my ($danglingfh, $dangling) = tempfile(UNLINK => 1);
|
||||
unlink($dangling);
|
||||
symlink("/dangling", $dangling);
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $dangling
|
||||
set \$vartest special title
|
||||
for_window [title="\$vartest"] border none
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# variables defined in the main file and used in the included file
|
||||
################################################################################
|
||||
|
||||
my ($varfh, $var) = tempfile(UNLINK => 1);
|
||||
print $varfh <<'EOT';
|
||||
for_window [title="$vartest"] border none
|
||||
|
||||
EOT
|
||||
$varfh->flush;
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
set \$vartest special title
|
||||
include $var
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
SKIP: {
|
||||
skip "not implemented";
|
||||
|
||||
################################################################################
|
||||
# variables defined in the included file and used in the main file
|
||||
################################################################################
|
||||
|
||||
($varfh, $var) = tempfile(UNLINK => 1);
|
||||
print $varfh <<'EOT';
|
||||
set $vartest special title
|
||||
EOT
|
||||
$varfh->flush;
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $var
|
||||
for_window [title="\$vartest"] border none
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# workspace names are loaded in the correct order (before reorder_bindings)
|
||||
################################################################################
|
||||
|
||||
# The included config can be empty, the issue lies with calling parse_file
|
||||
# multiple times.
|
||||
my ($wsfh, $ws) = tempfile(UNLINK => 1);
|
||||
$wsfh->flush;
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
bindsym 1 workspace 1: eggs
|
||||
bindsym Mod4+Shift+1 workspace 11: tomatoes
|
||||
|
||||
include $var
|
||||
EOT
|
||||
|
||||
# starts i3 with the given config, opens a window, returns its border style
|
||||
sub launch_get_workspace_name {
|
||||
my ($config) = @_;
|
||||
|
||||
my $pid = launch_with_config($config);
|
||||
|
||||
my $i3 = i3(get_socket_path(0));
|
||||
my $name = $i3->get_workspaces->recv->[0]->{name};
|
||||
|
||||
exit_gracefully($pid);
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
is(launch_get_workspace_name($config), '1: eggs', 'workspace name');
|
||||
|
||||
################################################################################
|
||||
# loop prevention
|
||||
################################################################################
|
||||
|
||||
my ($loopfh1, $loopname1) = tempfile(UNLINK => 1);
|
||||
my ($loopfh2, $loopname2) = tempfile(UNLINK => 1);
|
||||
|
||||
print $loopfh1 <<EOT;
|
||||
include $loopname2
|
||||
EOT
|
||||
$loopfh1->flush;
|
||||
|
||||
print $loopfh2 <<EOT;
|
||||
include $loopname1
|
||||
EOT
|
||||
$loopfh2->flush;
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
# loop
|
||||
include $loopname1
|
||||
|
||||
set \$vartest special title
|
||||
for_window [title="\$vartest"] border none
|
||||
EOT
|
||||
|
||||
is(launch_get_border($config), 'none', 'no border');
|
||||
|
||||
################################################################################
|
||||
# Verify the GET_VERSION IPC reply contains all included files
|
||||
################################################################################
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $indirectfilename2
|
||||
EOT
|
||||
|
||||
my $pid = launch_with_config($config);
|
||||
|
||||
my $i3 = i3(get_socket_path(0));
|
||||
my $version = $i3->get_version()->recv;
|
||||
my $included = $version->{included_config_file_names};
|
||||
|
||||
is_deeply($included, [ $indirectfilename2, $filename ], 'included config file names correct');
|
||||
|
||||
exit_gracefully($pid);
|
||||
|
||||
################################################################################
|
||||
# Verify the GET_CONFIG IPC reply returns the top-level config
|
||||
################################################################################
|
||||
|
||||
my $tmpdir = tempdir(CLEANUP => 1);
|
||||
my $socketpath = $tmpdir . "/config.sock";
|
||||
ok(! -e $socketpath, "$socketpath does not exist yet");
|
||||
|
||||
$config = <<EOT;
|
||||
# i3 config file (v4)
|
||||
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
|
||||
|
||||
include $indirectfilename2
|
||||
|
||||
ipc-socket $socketpath
|
||||
EOT
|
||||
|
||||
my $pid = launch_with_config($config, dont_add_socket_path => 1, dont_create_temp_dir => 1);
|
||||
|
||||
my $i3 = i3(get_socket_path(0));
|
||||
my $config_reply = $i3->get_config()->recv;
|
||||
|
||||
is($config_reply->{config}, $config, 'GET_CONFIG returns the top-level config file');
|
||||
|
||||
exit_gracefully($pid);
|
||||
|
||||
|
||||
done_testing;
|
Loading…
x
Reference in New Issue
Block a user