FILES:=$(filter-out $(AUTOGENERATED),$(wildcard src/*.c))
FILES:=$(FILES:.c=.o)
HEADERS:=$(filter-out include/loglevels.h,$(wildcard include/*.h))
+CMDPARSE_HEADERS:=include/GENERATED_call.h include/GENERATED_enums.h include/GENERATED_tokens.h
# Recursively generate loglevels.h by explicitly calling make
# We need this step because we need to ensure that loglevels.h will be
done; \
echo "};") > include/loglevels.h;
+# The GENERATED_* files are actually all created from a single pass, so all
+# files just depend on the first one.
+include/GENERATED_call.h: generate-command-parser.pl parser-specs/commands.spec
+ echo "[i3] Generating command parser"
+ (cd include; ../generate-command-parser.pl)
+include/GENERATED_enums.h: include/GENERATED_call.h
+include/GENERATED_tokens.h: include/GENERATED_call.h
+
+# This target compiles the command parser twice:
+# Once with -DTEST_PARSER, creating a stand-alone executable used for tests,
+# and once as an object file for i3.
+src/commands_parser.o: src/commands_parser.c ${HEADERS} ${CMDPARSE_HEADERS}
+ echo "[i3] CC $<"
+ $(CC) $(CPPFLAGS) $(CFLAGS) -DTEST_PARSER -DLOGLEVEL="((uint64_t)1 << $(shell awk '/$(shell basename $< .c)/ { print NR; exit 0; }' loglevels.tmp))" -o test.commands_parser $<
+ $(CC) $(CPPFLAGS) $(CFLAGS) -DLOGLEVEL="((uint64_t)1 << $(shell awk '/$(shell basename $< .c)/ { print NR; exit 0; }' loglevels.tmp))" -c -o $@ $<
+
src/cfgparse.yy.o: src/cfgparse.l src/cfgparse.y.o ${HEADERS}
echo "[i3] LEX $<"
$(FLEX) -i -o$(@:.o=.c) $<
[ ! -d i3-${VERSION} ] || rm -rf i3-${VERSION}
[ ! -e i3-${VERSION}.tar.bz2 ] || rm i3-${VERSION}.tar.bz2
mkdir i3-${VERSION}
- cp i3-migrate-config-to-v4 i3-sensible-* i3.config.keycodes DEPENDS GOALS LICENSE PACKAGE-MAINTAINER RELEASE-NOTES-${VERSION} i3.config i3.desktop i3.welcome pseudo-doc.doxygen i3-wsbar Makefile i3-${VERSION}
- cp -r src libi3 i3-msg i3-nagbar i3-config-wizard i3bar i3-dump-log yajl-fallback include man i3-${VERSION}
+ cp i3-migrate-config-to-v4 generate-command-parser.pl i3-sensible-* i3.config.keycodes DEPENDS GOALS LICENSE PACKAGE-MAINTAINER RELEASE-NOTES-${VERSION} i3.config i3.desktop i3.welcome pseudo-doc.doxygen i3-wsbar Makefile i3-${VERSION}
+ cp -r src libi3 i3-msg i3-nagbar i3-config-wizard i3bar i3-dump-log yajl-fallback include man parser-specs i3-${VERSION}
# Only copy toplevel documentation (important stuff)
mkdir i3-${VERSION}/docs
# Pre-generate documentation
rm -rf i3-${VERSION}
clean:
- rm -f src/*.o src/*.gcno src/cfgparse.tab.{c,h} src/cfgparse.yy.c src/cfgparse.{output,dot} src/cmdparse.tab.{c,h} src/cmdparse.yy.c src/cmdparse.{output,dot} loglevels.tmp include/loglevels.h
+ rm -f src/*.o src/*.gcno src/cfgparse.tab.{c,h} src/cfgparse.yy.c src/cfgparse.{output,dot} src/cmdparse.tab.{c,h} src/cmdparse.yy.c src/cmdparse.{output,dot} loglevels.tmp include/loglevels.h include/GENERATED_*
(which lcov >/dev/null 2>&1 && lcov -d . --zerocounters) || true
$(MAKE) -C libi3 clean
$(MAKE) -C docs clean
--- /dev/null
+#!/usr/bin/env perl
+# vim:ts=4:sw=4:expandtab
+#
+# i3 - an improved dynamic tiling window manager
+# © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE)
+#
+# generate-command-parser.pl: script to generate parts of the command parser
+# from its specification file parser-specs/commands.spec.
+#
+# Requires only perl >= 5.10, no modules.
+
+use strict;
+use warnings;
+use Data::Dumper;
+use v5.10;
+
+# reads in a whole file
+sub slurp {
+ open my $fh, '<', shift;
+ local $/;
+ <$fh>;
+}
+
+# Stores the different states.
+my %states;
+
+# XXX: don’t hardcode input and output
+my $input = '../parser-specs/commands.spec';
+my @raw_lines = split("\n", slurp($input));
+my @lines;
+
+# XXX: In the future, we might switch to a different way of parsing this. The
+# parser is in many ways not good — one obvious one is that it is hand-crafted
+# without a good reason, also it preprocesses lines and forgets about line
+# numbers. Luckily, this is just an implementation detail and the specification
+# for the i3 command parser is in-tree (not user input).
+# -- michael, 2012-01-12
+
+# First step of preprocessing:
+# Join token definitions which are spread over multiple lines.
+for my $line (@raw_lines) {
+ next if $line =~ /^\s*#/ || $line =~ /^\s*$/;
+
+ if ($line =~ /^\s+->/) {
+ # This is a continued token definition, append this line to the
+ # previous one.
+ $lines[$#lines] = $lines[$#lines] . $line;
+ } else {
+ push @lines, $line;
+ next;
+ }
+}
+
+# First step: We build up the data structure containing all states and their
+# token rules.
+
+my $current_state;
+
+for my $line (@lines) {
+ if (my ($state) = ($line =~ /^state ([A-Z_]+):$/)) {
+ #say "got a new state: $state";
+ $current_state = $state;
+ } else {
+ # Must be a token definition:
+ # [identifier = ] <tokens> -> <action>
+ #say "token definition: $line";
+
+ my ($identifier, $tokens, $action) =
+ ($line =~ /
+ ^\s* # skip leading whitespace
+ ([a-z_]+ \s* = \s*|) # optional identifier
+ (.*?) -> \s* # token
+ (.*) # optional action
+ /x);
+
+ # Cleanup the identifier (if any).
+ $identifier =~ s/^\s*(\S+)\s*=\s*$/$1/g;
+
+ # Cleanup the tokens (remove whitespace).
+ $tokens =~ s/\s*//g;
+
+ # The default action is to stay in the current state.
+ $action = $current_state if length($action) == 0;
+
+ #say "identifier = *$identifier*, token = *$tokens*, action = *$action*";
+ for my $token (split(',', $tokens)) {
+ my $store_token = {
+ token => $token,
+ identifier => $identifier,
+ next_state => $action,
+ };
+ if (exists $states{$current_state}) {
+ push $states{$current_state}, $store_token;
+ } else {
+ $states{$current_state} = [ $store_token ];
+ }
+ }
+ }
+}
+
+# Second step: Generate the enum values for all states.
+
+# It is important to keep the order the same, so we store the keys once.
+my @keys = keys %states;
+
+open(my $enumfh, '>', 'GENERATED_enums.h');
+
+# XXX: we might want to have a way to do this without a trailing comma, but gcc
+# seems to eat it.
+say $enumfh 'typedef enum {';
+my $cnt = 0;
+for my $state (@keys, '__CALL') {
+ say $enumfh " $state = $cnt,";
+ $cnt++;
+}
+say $enumfh '} cmdp_state;';
+close($enumfh);
+
+# Third step: Generate the call function.
+open(my $callfh, '>', 'GENERATED_call.h');
+say $callfh 'static char *GENERATED_call(const int call_identifier) {';
+say $callfh ' char *output = NULL;';
+say $callfh ' switch (call_identifier) {';
+my $call_id = 0;
+for my $state (@keys) {
+ my $tokens = $states{$state};
+ for my $token (@$tokens) {
+ next unless $token->{next_state} =~ /^call /;
+ my ($cmd) = ($token->{next_state} =~ /^call (.*)/);
+ my ($next_state) = ($cmd =~ /; ([A-Z_]+)$/);
+ $cmd =~ s/; ([A-Z_]+)$//;
+ # Go back to the INITIAL state unless told otherwise.
+ $next_state ||= 'INITIAL';
+ my $fmt = $cmd;
+ # Replace the references to identified literals (like $workspace) with
+ # calls to get_string().
+ $cmd =~ s/\$([a-z_]+)/get_string("$1")/g;
+ # Used only for debugging/testing.
+ $fmt =~ s/\$([a-z_]+)/%s/g;
+ $fmt =~ s/"([a-z0-9_]+)"/%s/g;
+
+ say $callfh " case $call_id:";
+ say $callfh '#ifndef TEST_PARSER';
+ my $real_cmd = $cmd;
+ if ($real_cmd =~ /\(\)/) {
+ $real_cmd =~ s/\(/(¤t_match/;
+ } else {
+ $real_cmd =~ s/\(/(¤t_match, /;
+ }
+ say $callfh " output = $real_cmd;";
+ say $callfh '#else';
+ # debug
+ $cmd =~ s/[^(]+\(//;
+ $cmd =~ s/\)$//;
+ $cmd = ", $cmd" if length($cmd) > 0;
+ say $callfh qq| printf("$fmt\\n"$cmd);|;
+ say $callfh '#endif';
+ say $callfh " state = $next_state;";
+ say $callfh " break;";
+ $token->{next_state} = "call $call_id";
+ $call_id++;
+ }
+}
+say $callfh ' default:';
+say $callfh ' printf("BUG in the parser. state = %d\n", call_identifier);';
+say $callfh ' }';
+say $callfh ' return output;';
+say $callfh '}';
+close($callfh);
+
+# Fourth step: Generate the token datastructures.
+
+open(my $tokfh, '>', 'GENERATED_tokens.h');
+
+for my $state (@keys) {
+ my $tokens = $states{$state};
+ say $tokfh 'cmdp_token tokens_' . $state . '[' . scalar @$tokens . '] = {';
+ for my $token (@$tokens) {
+ my $call_identifier = 0;
+ my $token_name = $token->{token};
+ if ($token_name =~ /^'/) {
+ # To make the C code simpler, we leave out the trailing single
+ # quote of the literal. We can do strdup(literal + 1); then :).
+ $token_name =~ s/'$//;
+ }
+ my $next_state = $token->{next_state};
+ if ($next_state =~ /^call /) {
+ ($call_identifier) = ($next_state =~ /^call ([0-9]+)$/);
+ $next_state = '__CALL';
+ }
+ my $identifier = $token->{identifier};
+ say $tokfh qq| { "$token_name", "$identifier", $next_state, { $call_identifier } }, |;
+ }
+ say $tokfh '};';
+}
+
+say $tokfh 'cmdp_token_ptr tokens[' . scalar @keys . '] = {';
+for my $state (@keys) {
+ my $tokens = $states{$state};
+ say $tokfh ' { tokens_' . $state . ', ' . scalar @$tokens . ' },';
+}
+say $tokfh '};';
+
+close($tokfh);
#include "startup.h"
#include "scratchpad.h"
#include "commands.h"
+#include "commands_parser.h"
#endif
typedef TAILQ_HEAD(owindows_head, owindow) owindows_head;
+void cmd_MIGRATION_enable();
+void cmd_MIGRATION_disable();
+void cmd_MIGRATION_save_new_parameters(Match *current_match, ...);
+void cmd_MIGRATION_save_old_parameters(Match *current_match, ...);
+void cmd_MIGRATION_validate();
+
char *cmd_criteria_init(Match *current_match);
char *cmd_criteria_match_windows(Match *current_match);
char *cmd_criteria_add(Match *current_match, char *ctype, char *cvalue);
--- /dev/null
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ * commands.c: all command functions (see commands_parser.c)
+ *
+ */
+#ifndef _COMMANDS_PARSER_H
+#define _COMMANDS_PARSER_H
+
+char *parse_command(const char *input);
+
+#endif
--- /dev/null
+# vim:ts=2:sw=2:expandtab
+#
+# i3 - an improved dynamic tiling window manager
+# © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE)
+#
+# parser-specs/commands.spec: Specification file for generate-command-parser.pl
+# which will generate the appropriate header files for our C parser.
+#
+# Use :source highlighting.vim in vim to get syntax highlighting
+# for this file.
+
+state INITIAL:
+ # We have an end token here for all the commands which just call some
+ # function without using an explicit 'end' token.
+ end ->
+ '[' -> call cmd_criteria_init(); CRITERIA
+ 'move' -> MOVE
+ 'exec' -> EXEC
+ 'exit' -> call cmd_exit()
+ 'restart' -> call cmd_restart()
+ 'reload' -> call cmd_reload()
+ 'border' -> BORDER
+ 'layout' -> LAYOUT
+ 'append_layout' -> APPEND_LAYOUT
+ 'workspace' -> WORKSPACE
+ 'focus' -> FOCUS
+ 'kill' -> KILL
+ 'open' -> call cmd_open()
+ 'fullscreen' -> FULLSCREEN
+ 'split' -> SPLIT
+ 'floating' -> FLOATING
+ 'mark' -> MARK
+ 'resize' -> RESIZE
+ 'nop' -> NOP
+ 'scratchpad' -> SCRATCHPAD
+ 'mode' -> MODE
+
+state CRITERIA:
+ ctype = 'class' -> CRITERION
+ ctype = 'instance' -> CRITERION
+ ctype = 'window_role' -> CRITERION
+ ctype = 'con_id' -> CRITERION
+ ctype = 'id' -> CRITERION
+ ctype = 'con_mark' -> CRITERION
+ ctype = 'title' -> CRITERION
+ ']' -> call cmd_criteria_match_windows(); INITIAL
+
+state CRITERION:
+ '=' -> CRITERION_STR
+
+state CRITERION_STR:
+ cvalue = word
+ -> call cmd_criteria_add($ctype, $cvalue); CRITERIA
+
+# exec [--no-startup-id] <command>
+state EXEC:
+ nosn = '--no-startup-id'
+ ->
+ command = string
+ -> call cmd_exec($nosn, $command)
+
+# border <normal|none|1pixel|toggle>
+state BORDER:
+ border_style = 'normal', 'none', '1pixel', 'toggle'
+ -> call cmd_border($border_style)
+
+# layout <default|stacked|stacking|tabbed>
+state LAYOUT:
+ layout_mode = 'default', 'stacked', 'stacking', 'tabbed'
+ -> call cmd_layout($layout_mode)
+
+# append_layout <path>
+state APPEND_LAYOUT:
+ path = string -> call cmd_append_layout($path)
+
+# workspace next|prev|next_on_output|prev_on_output
+# workspace back_and_forth
+# workspace <name>
+state WORKSPACE:
+ direction = 'next_on_output', 'prev_on_output', 'next', 'prev'
+ -> call cmd_workspace($direction)
+ 'back_and_forth'
+ -> call cmd_workspace_back_and_forth()
+ workspace = string
+ -> call cmd_workspace_name($workspace)
+
+# focus left|right|up|down
+# focus output <output>
+# focus tiling|floating|mode_toggle
+# focus parent|child
+# focus
+state FOCUS:
+ direction = 'left', 'right', 'up', 'down'
+ -> call cmd_focus_direction($direction)
+ 'output'
+ -> FOCUS_OUTPUT
+ window_mode = 'tiling', 'floating', 'mode_toggle'
+ -> call cmd_focus_window_mode($window_mode)
+ level = 'parent', 'child'
+ -> call cmd_focus_level($level)
+ end
+ -> call cmd_focus()
+
+state FOCUS_OUTPUT:
+ output = string
+ -> call cmd_focus_output($output)
+
+# kill window|client
+# kill
+state KILL:
+ kill_mode = 'window', 'client'
+ -> call cmd_kill($kill_mode)
+ end
+ -> call cmd_kill($kill_mode)
+
+# fullscreen global
+# fullscreen
+state FULLSCREEN:
+ fullscreen_mode = 'global'
+ -> call cmd_fullscreen($fullscreen_mode)
+ end
+ -> call cmd_fullscreen($fullscreen_mode)
+
+# split v|h|vertical|horizontal
+state SPLIT:
+ direction = 'v', 'h', 'vertical', 'horizontal'
+ -> call cmd_split($direction)
+
+# floating enable|disable|toggle
+state FLOATING:
+ floating = 'enable', 'disable', 'toggle'
+ -> call cmd_floating($floating)
+
+# mark <mark>
+state MARK:
+ mark = string
+ -> call cmd_mark($mark)
+
+# resize
+state RESIZE:
+ way = 'grow', 'shrink'
+ -> RESIZE_DIRECTION
+
+state RESIZE_DIRECTION:
+ direction = 'up', 'down', 'left', 'right'
+ -> RESIZE_PX
+
+state RESIZE_PX:
+ resize_px = word
+ -> RESIZE_TILING
+ end
+ -> call cmd_resize($way, $direction, "10", "10")
+
+state RESIZE_TILING:
+ 'px'
+ ->
+ 'or'
+ -> RESIZE_TILING_OR
+ end
+ -> call cmd_resize($way, $direction, $resize_px, "10")
+
+state RESIZE_TILING_OR:
+ 'ppt'
+ ->
+ resize_ppt = word
+ ->
+ end
+ -> call cmd_resize($way, $direction, $resize_px, $resize_ppt)
+
+# move <direction> [<pixels> [px]]
+# move [window|container] [to] workspace <str>
+# move [window|container] [to] output <str>
+# move [window|container] [to] scratchpad
+# move workspace to [output] <str>
+# move scratchpad
+state MOVE:
+ 'window'
+ ->
+ 'container'
+ ->
+ 'to'
+ ->
+ 'workspace'
+ -> MOVE_WORKSPACE
+ 'output'
+ -> MOVE_TO_OUTPUT
+ 'scratchpad'
+ -> call cmd_move_scratchpad()
+ direction = 'left', 'right', 'up', 'down'
+ -> MOVE_DIRECTION
+
+state MOVE_DIRECTION:
+ pixels = word
+ -> MOVE_DIRECTION_PX
+ end
+ -> call cmd_move_direction($direction, "10")
+
+state MOVE_DIRECTION_PX:
+ 'px'
+ -> call cmd_move_direction($direction, $pixels)
+ end
+ -> call cmd_move_direction($direction, $pixels)
+
+state MOVE_WORKSPACE:
+ 'to'
+ -> MOVE_WORKSPACE_TO_OUTPUT
+ workspace = 'next', 'prev', 'next_on_output', 'prev_on_output'
+ -> call cmd_move_con_to_workspace($workspace)
+ workspace = string
+ -> call cmd_move_con_to_workspace_name($workspace)
+
+state MOVE_TO_OUTPUT:
+ output = string
+ -> call cmd_move_con_to_output($output)
+
+state MOVE_WORKSPACE_TO_OUTPUT:
+ 'output'
+ ->
+ output = string
+ -> call cmd_move_workspace_to_output($output)
+
+# mode <string>
+state MODE:
+ mode = string
+ -> call cmd_mode($mode)
+
+state NOP:
+ comment = string
+ -> call cmd_nop($comment)
+
+state SCRATCHPAD:
+ 'show'
+ -> call cmd_scratchpad_show()
--- /dev/null
+set filetype=i3cmd
+syntax case match
+syntax clear
+
+syntax keyword i3specStatement state call
+highlight link i3specStatement Statement
+
+syntax match i3specComment /#.*/
+highlight link i3specComment Comment
+
+syntax region i3specLiteral start=/'/ end=/'/
+syntax keyword i3specToken string word end
+highlight link i3specLiteral String
+highlight link i3specToken String
+
+syntax match i3specState /[A-Z_]\{3,}/
+highlight link i3specState PreProc
+
+syntax match i3specSpecial /[->]/
+highlight link i3specSpecial Special
<MOVE>container { /* eat this token */ }
<MOVE>workspace { yy_pop_state(); yy_push_state(MOVE_WS); yy_push_state(EAT_WHITESPACE); return TOK_WORKSPACE; }
<MOVE>scratchpad { yy_pop_state(); return TOK_SCRATCHPAD; }
+<MOVE>output { yy_pop_state(); return TOK_OUTPUT; }
<MOVE>up { yy_pop_state(); return TOK_UP; }
<MOVE>down { yy_pop_state(); return TOK_DOWN; }
<MOVE>left { yy_pop_state(); return TOK_LEFT; }
}
char *parse_cmd(const char *new) {
+ cmd_MIGRATION_enable();
+ char *output = parse_command(new);
+ if (output != NULL) {
+ printf("MIGRATION: new output != NULL: %s\n", output);
+ free(output);
+ }
+ cmd_MIGRATION_disable();
+
json_output = NULL;
LOG("COMMAND: *%s*\n", new);
cmdyy_scan_string(new);
}
printf("done, json output = %s\n", json_output);
+ cmd_MIGRATION_validate();
+
cmdyylex_destroy();
FREE(context->line_copy);
FREE(context->compact_error);
exec:
TOK_EXEC optional_no_startup_id STR
{
- json_output = cmd_exec(¤t_match, ($2 ? "nosn" : NULL), $3);
+ json_output = cmd_exec(¤t_match, ($2 ? "--no-startup-id" : NULL), $3);
free($3);
}
;
*
*/
#include <float.h>
+#include <stdarg.h>
#include "all.h"
#include "cmdparse.tab.h"
return output;
}
+/*******************************************************************************
+ * Helper functions for the migration testing. We let the new parser call every
+ * function here and save the stack (current_match plus all parameters. Then we
+ * let the old parser call every function and actually execute the code. When
+ * there are differences between the first and the second invocation (or if
+ * there has not been a first invocation at all), we generate an error.
+ ******************************************************************************/
+
+static bool migration_test = false;
+typedef struct stackframe {
+ Match match;
+ int n_args;
+ char *args[10];
+ TAILQ_ENTRY(stackframe) stackframes;
+} stackframe;
+static TAILQ_HEAD(stackframes_head, stackframe) old_stackframes =
+ TAILQ_HEAD_INITIALIZER(old_stackframes);
+static struct stackframes_head new_stackframes =
+ TAILQ_HEAD_INITIALIZER(new_stackframes);
+/* We use this char* to uniquely terminate the list of parameters to save. */
+static char *last_parameter = "0";
+
+void cmd_MIGRATION_enable() {
+ migration_test = true;
+ /* clear the current stack */
+ while (!TAILQ_EMPTY(&old_stackframes)) {
+ stackframe *current = TAILQ_FIRST(&old_stackframes);
+ for (int c = 0; c < current->n_args; c++)
+ if (current->args[c])
+ free(current->args[c]);
+ TAILQ_REMOVE(&old_stackframes, current, stackframes);
+ free(current);
+ }
+ while (!TAILQ_EMPTY(&new_stackframes)) {
+ stackframe *current = TAILQ_FIRST(&new_stackframes);
+ for (int c = 0; c < current->n_args; c++)
+ if (current->args[c])
+ free(current->args[c]);
+ TAILQ_REMOVE(&new_stackframes, current, stackframes);
+ free(current);
+ }
+}
+
+void cmd_MIGRATION_disable() {
+ migration_test = false;
+}
+
+void cmd_MIGRATION_save_new_parameters(Match *current_match, ...) {
+ va_list args;
+
+ DLOG("saving parameters.\n");
+ stackframe *frame = scalloc(sizeof(stackframe));
+ match_copy(&(frame->match), current_match);
+
+ /* All parameters are char*s */
+ va_start(args, current_match);
+ while (true) {
+ char *parameter = va_arg(args, char*);
+ if (parameter == last_parameter)
+ break;
+ DLOG("parameter = %s\n", parameter);
+ if (parameter)
+ frame->args[frame->n_args] = sstrdup(parameter);
+ frame->n_args++;
+ }
+ va_end(args);
+
+ TAILQ_INSERT_TAIL(&new_stackframes, frame, stackframes);
+}
+
+void cmd_MIGRATION_save_old_parameters(Match *current_match, ...) {
+ va_list args;
+
+ DLOG("saving new parameters.\n");
+ stackframe *frame = scalloc(sizeof(stackframe));
+ match_copy(&(frame->match), current_match);
+
+ /* All parameters are char*s */
+ va_start(args, current_match);
+ while (true) {
+ char *parameter = va_arg(args, char*);
+ if (parameter == last_parameter)
+ break;
+ DLOG("parameter = %s\n", parameter);
+ if (parameter)
+ frame->args[frame->n_args] = sstrdup(parameter);
+ frame->n_args++;
+ }
+ va_end(args);
+
+ TAILQ_INSERT_TAIL(&old_stackframes, frame, stackframes);
+}
+
+static bool re_differ(struct regex *new, struct regex *old) {
+ return ((new == NULL && old != NULL) ||
+ (new != NULL && old == NULL) ||
+ (new != NULL && old != NULL &&
+ strcmp(new->pattern, old->pattern) != 0));
+}
+
+static bool str_differ(char *new, char *old) {
+ return ((new == NULL && old != NULL) ||
+ (new != NULL && old == NULL) ||
+ (new != NULL && old != NULL &&
+ strcmp(new, old) != 0));
+}
+
+static pid_t migration_pid = -1;
+
+/*
+ * Handler which will be called when we get a SIGCHLD for the nagbar, meaning
+ * it exited (or could not be started, depending on the exit code).
+ *
+ */
+static void nagbar_exited(EV_P_ ev_child *watcher, int revents) {
+ ev_child_stop(EV_A_ watcher);
+ if (!WIFEXITED(watcher->rstatus)) {
+ fprintf(stderr, "ERROR: i3-nagbar did not exit normally.\n");
+ return;
+ }
+
+ int exitcode = WEXITSTATUS(watcher->rstatus);
+ printf("i3-nagbar process exited with status %d\n", exitcode);
+ if (exitcode == 2) {
+ fprintf(stderr, "ERROR: i3-nagbar could not be found. Is it correctly installed on your system?\n");
+ }
+
+ migration_pid = -1;
+}
+
+/* We need ev >= 4 for the following code. Since it is not *that* important (it
+ * only makes sure that there are no i3-nagbar instances left behind) we still
+ * support old systems with libev 3. */
+#if EV_VERSION_MAJOR >= 4
+/*
+ * Cleanup handler. Will be called when i3 exits. Kills i3-nagbar with signal
+ * SIGKILL (9) to make sure there are no left-over i3-nagbar processes.
+ *
+ */
+static void nagbar_cleanup(EV_P_ ev_cleanup *watcher, int revent) {
+ if (migration_pid != -1) {
+ LOG("Sending SIGKILL (9) to i3-nagbar with PID %d\n", migration_pid);
+ kill(migration_pid, SIGKILL);
+ }
+}
+#endif
+
+void cmd_MIGRATION_start_nagbar() {
+ if (migration_pid != -1) {
+ fprintf(stderr, "i3-nagbar already running.\n");
+ return;
+ }
+ fprintf(stderr, "Starting i3-nagbar, command parsing differs from expected output.\n");
+ ELOG("Please report this on IRC or in the bugtracker. Make sure to include the full debug level logfile:\n");
+ ELOG("i3-dump-log | gzip -9c > /tmp/i3.log.gz\n");
+ ELOG("FYI: Your i3 version is " I3_VERSION "\n");
+ migration_pid = fork();
+ if (migration_pid == -1) {
+ warn("Could not fork()");
+ return;
+ }
+
+ /* child */
+ if (migration_pid == 0) {
+ char *pageraction;
+ sasprintf(&pageraction, "i3-sensible-terminal -e i3-sensible-pager \"%s\"", errorfilename);
+ char *argv[] = {
+ NULL, /* will be replaced by the executable path */
+ "-t",
+ "error",
+ "-m",
+ "You found a parsing error. Please, please, please, report it!",
+ "-b",
+ "show errors",
+ pageraction,
+ NULL
+ };
+ exec_i3_utility("i3-nagbar", argv);
+ }
+
+ /* parent */
+ /* install a child watcher */
+ ev_child *child = smalloc(sizeof(ev_child));
+ ev_child_init(child, &nagbar_exited, migration_pid, 0);
+ ev_child_start(main_loop, child);
+
+/* We need ev >= 4 for the following code. Since it is not *that* important (it
+ * only makes sure that there are no i3-nagbar instances left behind) we still
+ * support old systems with libev 3. */
+#if EV_VERSION_MAJOR >= 4
+ /* install a cleanup watcher (will be called when i3 exits and i3-nagbar is
+ * still running) */
+ ev_cleanup *cleanup = smalloc(sizeof(ev_cleanup));
+ ev_cleanup_init(cleanup, nagbar_cleanup);
+ ev_cleanup_start(main_loop, cleanup);
+#endif
+}
+
+void cmd_MIGRATION_validate() {
+ DLOG("validating the different stacks now\n");
+ int old_count = 0;
+ int new_count = 0;
+ stackframe *current;
+ TAILQ_FOREACH(current, &new_stackframes, stackframes)
+ new_count++;
+ TAILQ_FOREACH(current, &old_stackframes, stackframes)
+ old_count++;
+ if (new_count != old_count) {
+ ELOG("FAILED, new_count == %d != old_count == %d\n", new_count, old_count);
+ cmd_MIGRATION_start_nagbar();
+ return;
+ }
+ DLOG("parameter count matching, comparing one by one...\n");
+
+ stackframe *new_frame = TAILQ_FIRST(&new_stackframes),
+ *old_frame = TAILQ_FIRST(&old_stackframes);
+ for (int i = 0; i < new_count; i++) {
+ if (new_frame->match.dock != old_frame->match.dock ||
+ new_frame->match.id != old_frame->match.id ||
+ new_frame->match.con_id != old_frame->match.con_id ||
+ new_frame->match.floating != old_frame->match.floating ||
+ new_frame->match.insert_where != old_frame->match.insert_where ||
+ re_differ(new_frame->match.title, old_frame->match.title) ||
+ re_differ(new_frame->match.application, old_frame->match.application) ||
+ re_differ(new_frame->match.class, old_frame->match.class) ||
+ re_differ(new_frame->match.instance, old_frame->match.instance) ||
+ re_differ(new_frame->match.mark, old_frame->match.mark) ||
+ re_differ(new_frame->match.role, old_frame->match.role) ) {
+ ELOG("FAILED, new_frame->match != old_frame->match (frame %d)\n", i);
+ cmd_MIGRATION_start_nagbar();
+ return;
+ }
+ if (new_frame->n_args != old_frame->n_args) {
+ ELOG("FAILED, new_frame->n_args == %d != old_frame->n_args == %d (frame %d)\n",
+ new_frame->n_args, old_frame->n_args, i);
+ cmd_MIGRATION_start_nagbar();
+ return;
+ }
+ for (int j = 0; j < new_frame->n_args; j++) {
+ if (str_differ(new_frame->args[j], old_frame->args[j])) {
+ ELOG("FAILED, new_frame->args[%d] == %s != old_frame->args[%d] == %s (frame %d)\n",
+ j, new_frame->args[j], j, old_frame->args[j], i);
+ cmd_MIGRATION_start_nagbar();
+ return;
+ }
+ }
+ new_frame = TAILQ_NEXT(new_frame, stackframes);
+ old_frame = TAILQ_NEXT(old_frame, stackframes);
+ }
+ DLOG("OK\n");
+}
+
+#define MIGRATION_init(x, ...) do { \
+ if (migration_test) { \
+ cmd_MIGRATION_save_new_parameters(current_match, __FUNCTION__, ##__VA_ARGS__ , last_parameter); \
+ return NULL; \
+ } else { \
+ cmd_MIGRATION_save_old_parameters(current_match, __FUNCTION__, ##__VA_ARGS__ , last_parameter); \
+ } \
+} while (0)
+
+
+/*******************************************************************************
+ * Criteria functions.
+ ******************************************************************************/
+
char *cmd_criteria_init(Match *current_match) {
DLOG("Initializing criteria, current_match = %p\n", current_match);
match_init(current_match);
char *cmd_criteria_match_windows(Match *current_match) {
owindow *next, *current;
+ /* The same as MIGRATION_init, but doesn’t return */
+ if (migration_test) {
+ cmd_MIGRATION_save_new_parameters(current_match, __FUNCTION__, last_parameter);
+ } else {
+ cmd_MIGRATION_save_old_parameters(current_match, __FUNCTION__, last_parameter);
+ }
+
DLOG("match specification finished, matching...\n");
/* copy the old list head to iterate through it and start with a fresh
* list which will contain only matching windows */
}
char *cmd_criteria_add(Match *current_match, char *ctype, char *cvalue) {
+ /* The same as MIGRATION_init, but doesn’t return */
+ if (migration_test) {
+ cmd_MIGRATION_save_new_parameters(current_match, __FUNCTION__, last_parameter);
+ } else {
+ cmd_MIGRATION_save_old_parameters(current_match, __FUNCTION__, last_parameter);
+ }
+
DLOG("ctype=*%s*, cvalue=*%s*\n", ctype, cvalue);
if (strcmp(ctype, "class") == 0) {
char *cmd_move_con_to_workspace(Match *current_match, char *which) {
owindow *current;
+ MIGRATION_init(x, which);
+
DLOG("which=%s\n", which);
HANDLE_EMPTY_MATCH;
}
char *cmd_move_con_to_workspace_name(Match *current_match, char *name) {
+ MIGRATION_init(x, name);
+
if (strncasecmp(name, "__i3_", strlen("__i3_")) == 0) {
LOG("You cannot switch to the i3 internal workspaces.\n");
return sstrdup("{\"sucess\": false}");
}
char *cmd_resize(Match *current_match, char *way, char *direction, char *resize_px, char *resize_ppt) {
+ MIGRATION_init(x, way, direction, resize_px, resize_ppt);
/* resize <grow|shrink> <direction> [<px> px] [or <ppt> ppt] */
DLOG("resizing in way %s, direction %s, px %s or ppt %s\n", way, direction, resize_px, resize_ppt);
// TODO: We could either handle this in the parser itself as a separate token (and make the stack typed) or we need a better way to convert a string to a number with error checking
}
char *cmd_border(Match *current_match, char *border_style_str) {
+ MIGRATION_init(x, border_style_str);
DLOG("border style should be changed to %s\n", border_style_str);
owindow *current;
}
char *cmd_nop(Match *current_match, char *comment) {
+ MIGRATION_init(x, comment);
LOG("-------------------------------------------------\n");
LOG(" NOP: %s\n", comment);
LOG("-------------------------------------------------\n");
}
char *cmd_append_layout(Match *current_match, char *path) {
+ MIGRATION_init(x, path);
LOG("Appending layout \"%s\"\n", path);
tree_append_json(path);
tree_render();
}
char *cmd_workspace(Match *current_match, char *which) {
+ MIGRATION_init(x, which);
Con *ws;
DLOG("which=%s\n", which);
}
char *cmd_workspace_back_and_forth(Match *current_match) {
+ MIGRATION_init(x);
workspace_back_and_forth();
tree_render();
}
char *cmd_workspace_name(Match *current_match, char *name) {
+ MIGRATION_init(x, name);
if (strncasecmp(name, "__i3_", strlen("__i3_")) == 0) {
LOG("You cannot switch to the i3 internal workspaces.\n");
return sstrdup("{\"sucess\": false}");
}
char *cmd_mark(Match *current_match, char *mark) {
+ MIGRATION_init(x, mark);
DLOG("Clearing all windows which have that mark first\n");
Con *con;
}
char *cmd_mode(Match *current_match, char *mode) {
+ MIGRATION_init(x, mode);
DLOG("mode=%s\n", mode);
switch_mode(mode);
}
char *cmd_move_con_to_output(Match *current_match, char *name) {
+ MIGRATION_init(x, name);
owindow *current;
DLOG("should move window to output %s\n", name);
}
char *cmd_floating(Match *current_match, char *floating_mode) {
+ MIGRATION_init(x, floating_mode);
owindow *current;
DLOG("floating_mode=%s\n", floating_mode);
}
char *cmd_move_workspace_to_output(Match *current_match, char *name) {
+ MIGRATION_init(x, name);
DLOG("should move workspace to output %s\n", name);
HANDLE_EMPTY_MATCH;
}
char *cmd_split(Match *current_match, char *direction) {
+ MIGRATION_init(x, direction);
/* TODO: use matches */
LOG("splitting in direction %c\n", direction[0]);
tree_split(focused, (direction[0] == 'v' ? VERT : HORIZ));
}
char *cmd_kill(Match *current_match, char *kill_mode_str) {
+ if (kill_mode_str == NULL)
+ kill_mode_str = "window";
+ MIGRATION_init(x, kill_mode_str);
owindow *current;
DLOG("kill_mode=%s\n", kill_mode_str);
int kill_mode;
- if (kill_mode_str == NULL || strcmp(kill_mode_str, "window") == 0)
+ if (strcmp(kill_mode_str, "window") == 0)
kill_mode = KILL_WINDOW;
else if (strcmp(kill_mode_str, "client") == 0)
kill_mode = KILL_CLIENT;
}
char *cmd_exec(Match *current_match, char *nosn, char *command) {
+ MIGRATION_init(x, nosn, command);
bool no_startup_id = (nosn != NULL);
DLOG("should execute %s, no_startup_id = %d\n", command, no_startup_id);
}
char *cmd_focus_direction(Match *current_match, char *direction) {
+ MIGRATION_init(x, direction);
if (focused &&
focused->type != CT_WORKSPACE &&
focused->fullscreen_mode != CF_NONE) {
}
char *cmd_focus_window_mode(Match *current_match, char *window_mode) {
+ MIGRATION_init(x, window_mode);
if (focused &&
focused->type != CT_WORKSPACE &&
focused->fullscreen_mode != CF_NONE) {
}
char *cmd_focus_level(Match *current_match, char *level) {
+ MIGRATION_init(x, level);
if (focused &&
focused->type != CT_WORKSPACE &&
focused->fullscreen_mode != CF_NONE) {
}
char *cmd_focus(Match *current_match) {
+ MIGRATION_init(x);
DLOG("current_match = %p\n", current_match);
if (focused &&
focused->type != CT_WORKSPACE &&
ELOG("You have to specify which window/container should be focused.\n");
ELOG("Example: [class=\"urxvt\" title=\"irssi\"] focus\n");
- // TODO: json output
char *json_output;
sasprintf(&json_output, "{\"success\":false, \"error\":\"You have to "
"specify which window/container should be focused\"}");
return json_output;
}
- LOG("here");
int count = 0;
TAILQ_FOREACH(current, &owindows, owindows) {
Con *ws = con_get_workspace(current->con);
* Just skip it, you cannot focus dock windows. */
if (!ws)
continue;
- LOG("there");
/* If the container is not on the current workspace,
* workspace_show() will switch to a different workspace and (if
}
char *cmd_fullscreen(Match *current_match, char *fullscreen_mode) {
+ if (fullscreen_mode == NULL)
+ fullscreen_mode = "output";
+ MIGRATION_init(x, fullscreen_mode);
DLOG("toggling fullscreen, mode = %s\n", fullscreen_mode);
owindow *current;
TAILQ_FOREACH(current, &owindows, owindows) {
printf("matching: %p / %s\n", current->con, current->con->name);
- con_toggle_fullscreen(current->con, (fullscreen_mode && strcmp(fullscreen_mode, "global") == 0 ? CF_GLOBAL : CF_OUTPUT));
+ con_toggle_fullscreen(current->con, (strcmp(fullscreen_mode, "global") == 0 ? CF_GLOBAL : CF_OUTPUT));
}
tree_render();
}
char *cmd_move_direction(Match *current_match, char *direction, char *move_px) {
+ MIGRATION_init(x, direction, move_px);
// TODO: We could either handle this in the parser itself as a separate token (and make the stack typed) or we need a better way to convert a string to a number with error checking
int px = atoi(move_px);
}
char *cmd_layout(Match *current_match, char *layout_str) {
+ if (strcmp(layout_str, "stacking") == 0)
+ layout_str = "stacked";
+ MIGRATION_init(x, layout_str);
DLOG("changing layout to %s\n", layout_str);
owindow *current;
int layout = (strcmp(layout_str, "default") == 0 ? L_DEFAULT :
- (strcmp(layout_str, "stacked") == 0 || strcmp(layout_str, "stacking") == 0 ? L_STACKED :
+ (strcmp(layout_str, "stacked") == 0 ? L_STACKED :
L_TABBED));
/* check if the match is empty, not if the result is empty */
}
char *cmd_exit(Match *current_match) {
+ MIGRATION_init(x);
LOG("Exiting due to user command.\n");
exit(0);
}
char *cmd_reload(Match *current_match) {
+ MIGRATION_init(x);
LOG("reloading\n");
kill_configerror_nagbar(false);
load_configuration(conn, NULL, true);
}
char *cmd_restart(Match *current_match) {
+ MIGRATION_init(x);
LOG("restarting i3\n");
i3_restart(false);
}
char *cmd_open(Match *current_match) {
+ MIGRATION_init(x);
LOG("opening new container\n");
Con *con = tree_open_con(NULL, NULL);
con_focus(con);
}
char *cmd_focus_output(Match *current_match, char *name) {
+ MIGRATION_init(x, name);
owindow *current;
DLOG("name = %s\n", name);
}
char *cmd_move_scratchpad(Match *current_match) {
+ MIGRATION_init(x);
DLOG("should move window to scratchpad\n");
owindow *current;
}
char *cmd_scratchpad_show(Match *current_match) {
+ MIGRATION_init(x);
DLOG("should show scratchpad window\n");
owindow *current;
--- /dev/null
+/*
+ * vim:ts=4:sw=4:expandtab
+ *
+ * i3 - an improved dynamic tiling window manager
+ * © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE)
+ *
+ * commands_parser.c: hand-written parser to parse commands (commands are what
+ * you bind on keys and what you can send to i3 using the IPC interface, like
+ * 'move left' or 'workspace 4').
+ *
+ * We use a hand-written parser instead of lex/yacc because our commands are
+ * easy for humans, not for computers. Thus, it’s quite hard to specify a
+ * context-free grammar for the commands. A PEG grammar would be easier, but
+ * there’s downsides to every PEG parser generator I have come accross so far.
+ *
+ * This parser is basically a state machine which looks for literals or strings
+ * and can push either on a stack. After identifying a literal or string, it
+ * will either transition to the current state, to a different state, or call a
+ * function (like cmd_move()).
+ *
+ * Special care has been taken that error messages are useful and the code is
+ * well testable (when compiled with -DTEST_PARSER it will output to stdout
+ * instead of actually calling any function).
+ *
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "all.h"
+#include "queue.h"
+
+/*******************************************************************************
+ * The data structures used for parsing. Essentially the current state and a
+ * list of tokens for that state.
+ *
+ * The GENERATED_* files are generated by generate-commands-parser.pl with the
+ * input parser-specs/commands.spec.
+ ******************************************************************************/
+
+#include "GENERATED_enums.h"
+
+typedef struct token {
+ char *name;
+ char *identifier;
+ /* This might be __CALL */
+ cmdp_state next_state;
+ union {
+ uint16_t call_identifier;
+ } extra;
+} cmdp_token;
+
+typedef struct tokenptr {
+ cmdp_token *array;
+ int n;
+} cmdp_token_ptr;
+
+#include "GENERATED_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;
+ char *str;
+};
+
+/* 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) {
+ for (int c = 0; c < 10; c++) {
+ if (stack[c].identifier != NULL)
+ continue;
+ /* Found a free slot, let’s store it here. */
+ stack[c].identifier = identifier;
+ stack[c].str = str;
+ return;
+ }
+
+ /* When we arrive here, the stack is full. This should not happen and
+ * means there’s either a bug in this parser or the specification
+ * contains a command with more than 10 identified tokens. */
+ printf("argh! stack full\n");
+ exit(1);
+}
+
+// XXX: ideally, this would be const char. need to check if that works with all
+// called functions.
+static char *get_string(const char *identifier) {
+ DLOG("Getting string %s from stack...\n", identifier);
+ for (int c = 0; c < 10; c++) {
+ if (stack[c].identifier == NULL)
+ break;
+ if (strcmp(identifier, stack[c].identifier) == 0)
+ return stack[c].str;
+ }
+ return NULL;
+}
+
+static void clear_stack() {
+ DLOG("clearing stack.\n");
+ for (int c = 0; c < 10; c++) {
+ if (stack[c].str != NULL)
+ free(stack[c].str);
+ stack[c].identifier = NULL;
+ stack[c].str = NULL;
+ }
+}
+
+// TODO: remove this if it turns out we don’t need it for testing.
+#if 0
+/*******************************************************************************
+ * A dynamically growing linked list which holds the criteria for the current
+ * command.
+ ******************************************************************************/
+
+typedef struct criterion {
+ char *type;
+ char *value;
+
+ TAILQ_ENTRY(criterion) criteria;
+} criterion;
+
+static TAILQ_HEAD(criteria_head, criterion) criteria =
+ TAILQ_HEAD_INITIALIZER(criteria);
+
+/*
+ * Stores the given type/value in the list of criteria.
+ * Accepts a pointer as first argument, since it is 'call'ed by the parser.
+ *
+ */
+static void push_criterion(void *unused_criteria, const char *type,
+ const char *value) {
+ struct criterion *criterion = malloc(sizeof(struct criterion));
+ criterion->type = strdup(type);
+ criterion->value = strdup(value);
+ TAILQ_INSERT_TAIL(&criteria, criterion, criteria);
+}
+
+/*
+ * Clears the criteria linked list.
+ * Accepts a pointer as first argument, since it is 'call'ed by the parser.
+ *
+ */
+static void clear_criteria(void *unused_criteria) {
+ struct criterion *criterion;
+ while (!TAILQ_EMPTY(&criteria)) {
+ criterion = TAILQ_FIRST(&criteria);
+ free(criterion->type);
+ free(criterion->value);
+ TAILQ_REMOVE(&criteria, criterion, criteria);
+ free(criterion);
+ }
+}
+#endif
+
+/*******************************************************************************
+ * The parser itself.
+ ******************************************************************************/
+
+static cmdp_state state;
+#ifndef TEST_PARSER
+static Match current_match;
+#endif
+static char *json_output;
+
+#include "GENERATED_call.h"
+
+
+static void next_state(const cmdp_token *token) {
+ if (token->next_state == __CALL) {
+ DLOG("should call stuff, yay. call_id = %d\n",
+ token->extra.call_identifier);
+ json_output = GENERATED_call(token->extra.call_identifier);
+ clear_stack();
+ return;
+ }
+
+ state = token->next_state;
+ if (state == INITIAL) {
+ clear_stack();
+ }
+}
+
+/* TODO: Return parsing errors via JSON. */
+char *parse_command(const char *input) {
+ DLOG("new parser handling: %s\n", input);
+ state = INITIAL;
+ json_output = NULL;
+
+ const char *walk = input;
+ const size_t len = strlen(input);
+ int c;
+ const cmdp_token *token;
+ bool token_handled;
+
+ // TODO: make this testable
+#ifndef TEST_PARSER
+ cmd_criteria_init(¤t_match);
+#endif
+
+ /* The "<=" operator is intentional: We also handle the terminating 0-byte
+ * explicitly by looking for an 'end' token. */
+ while ((walk - input) <= len) {
+ /* skip whitespace before every token */
+ while ((*walk == ' ' || *walk == '\t') && *walk != '\0')
+ walk++;
+
+ DLOG("remaining input = %s\n", walk);
+
+ cmdp_token_ptr *ptr = &(tokens[state]);
+ token_handled = false;
+ for (c = 0; c < ptr->n; c++) {
+ token = &(ptr->array[c]);
+ DLOG("trying token %d = %s\n", c, token->name);
+
+ /* A literal. */
+ if (token->name[0] == '\'') {
+ DLOG("literal\n");
+ if (strncasecmp(walk, token->name + 1, strlen(token->name) - 1) == 0) {
+ DLOG("found literal, moving to next state\n");
+ if (token->identifier != NULL)
+ push_string(token->identifier, strdup(token->name + 1));
+ walk += strlen(token->name) - 1;
+ next_state(token);
+ token_handled = true;
+ break;
+ }
+ continue;
+ }
+
+ if (strcmp(token->name, "string") == 0 ||
+ strcmp(token->name, "word") == 0) {
+ DLOG("parsing this as a string\n");
+ const char *beginning = walk;
+ /* Handle quoted strings (or words). */
+ if (*walk == '"') {
+ beginning++;
+ walk++;
+ while (*walk != '"' || *(walk-1) == '\\')
+ walk++;
+ } else {
+ if (token->name[0] == 's') {
+ /* For a string (starting with 's'), the delimiters are
+ * comma (,) and semicolon (;) which introduce a new
+ * operation or command, respectively. */
+ while (*walk != ';' && *walk != ',' && *walk != '\0')
+ walk++;
+ } else {
+ /* For a word, the delimiters are white space (' ' or
+ * '\t'), closing square bracket (]), comma (,) and
+ * semicolon (;). */
+ while (*walk != ' ' && *walk != '\t' && *walk != ']' &&
+ *walk != ',' && *walk != ';' && *walk != '\0')
+ walk++;
+ }
+ }
+ if (walk != beginning) {
+ char *str = calloc(walk-beginning + 1, 1);
+ strncpy(str, beginning, walk-beginning);
+ if (token->identifier)
+ push_string(token->identifier, str);
+ DLOG("str is \"%s\"\n", str);
+ /* If we are at the end of a quoted string, skip the ending
+ * double quote. */
+ if (*walk == '"')
+ walk++;
+ next_state(token);
+ token_handled = true;
+ break;
+ }
+ }
+
+ if (strcmp(token->name, "end") == 0) {
+ DLOG("checking for the end token.\n");
+ if (*walk == '\0' || *walk == ',' || *walk == ';') {
+ DLOG("yes, indeed. end\n");
+ walk++;
+ next_state(token);
+ token_handled = true;
+ /* To make sure we start with an appropriate matching
+ * datastructure for commands which do *not* specify any
+ * criteria, we re-initialize the criteria system after
+ * every command. */
+ // TODO: make this testable
+#ifndef TEST_PARSER
+ if (*walk == '\0' || *walk == ';')
+ cmd_criteria_init(¤t_match);
+#endif
+ break;
+ }
+ }
+ }
+
+ if (!token_handled) {
+ /* Figure out how much memory we will need to fill in the names of
+ * all tokens afterwards. */
+ int tokenlen = 0;
+ for (c = 0; c < ptr->n; c++)
+ tokenlen += strlen(ptr->array[c].name) + strlen("'', ");
+
+ /* Build up a decent error message. We include the problem, the
+ * full input, and underline the position where the parser
+ * currently is. */
+ char *errormessage;
+ char *possible_tokens = malloc(tokenlen + 1);
+ char *tokenwalk = possible_tokens;
+ for (c = 0; c < ptr->n; c++) {
+ token = &(ptr->array[c]);
+ if (token->name[0] == '\'') {
+ /* A literal is copied to the error message enclosed with
+ * single quotes. */
+ *tokenwalk++ = '\'';
+ strcpy(tokenwalk, token->name + 1);
+ tokenwalk += strlen(token->name + 1);
+ *tokenwalk++ = '\'';
+ } else {
+ /* Any other token is copied to the error message enclosed
+ * with angle brackets. */
+ *tokenwalk++ = '<';
+ strcpy(tokenwalk, token->name);
+ tokenwalk += strlen(token->name);
+ *tokenwalk++ = '>';
+ }
+ if (c < (ptr->n - 1)) {
+ *tokenwalk++ = ',';
+ *tokenwalk++ = ' ';
+ }
+ }
+ *tokenwalk = '\0';
+ asprintf(&errormessage, "Expected one of these tokens: %s",
+ possible_tokens);
+ free(possible_tokens);
+
+ /* Contains the same amount of characters as 'input' has, but with
+ * the unparseable part highlighted using ^ characters. */
+ char *position = malloc(len + 1);
+ for (const char *copywalk = input; *copywalk != '\0'; copywalk++)
+ position[(copywalk - input)] = (copywalk >= walk ? '^' : ' ');
+ position[len] = '\0';
+
+ printf("%s\n", errormessage);
+ printf("Your command: %s\n", input);
+ printf(" %s\n", position);
+
+ free(position);
+ free(errormessage);
+ break;
+ }
+ }
+
+ DLOG("json_output = %s\n", json_output);
+ return json_output;
+}
+
+/*******************************************************************************
+ * Code for building the stand-alone binary test.commands_parser which is used
+ * by t/187-commands-parser.t.
+ ******************************************************************************/
+
+#ifdef TEST_PARSER
+
+/*
+ * Logs the given message to stdout while prefixing the current time to it,
+ * but only if the corresponding debug loglevel was activated.
+ * This is to be called by DLOG() which includes filename/linenumber
+ *
+ */
+void debuglog(uint64_t lev, char *fmt, ...) {
+ va_list args;
+
+ va_start(args, fmt);
+ fprintf(stdout, "# ");
+ vfprintf(stdout, fmt, args);
+ va_end(args);
+}
+
+int main(int argc, char *argv[]) {
+ if (argc < 2) {
+ fprintf(stderr, "Syntax: %s <command>\n", argv[0]);
+ return 1;
+ }
+ parse_command(argv[1]);
+}
+#endif
--- /dev/null
+#!perl
+# vim:ts=4:sw=4:expandtab
+#
+# Tests the standalone parser binary to see if it calls the right code when
+# confronted with various commands, if it prints proper error messages for
+# wrong commands and if it terminates in every case.
+#
+use i3test i3_autostart => 0;
+
+sub parser_calls {
+ my ($command) = @_;
+
+ # TODO: use a timeout, so that we can error out if it doesn’t terminate
+ # TODO: better way of passing arguments
+ my $stdout = qx(../test.commands_parser '$command');
+
+ # Filter out all debugging output.
+ my @lines = split("\n", $stdout);
+ @lines = grep { not /^# / } @lines;
+
+ # The criteria management calls are irrelevant and not what we want to test
+ # in the first place.
+ @lines = grep { !(/cmd_criteria_init()/ || /cmd_criteria_match_windows/) } @lines;
+ return join("\n", @lines);
+}
+
+################################################################################
+# 1: First that the parser properly recognizes commands which are ok.
+################################################################################
+
+is(parser_calls('move workspace 3'),
+ 'cmd_move_con_to_workspace_name(3)',
+ 'single number (move workspace 3) ok');
+
+is(parser_calls('move to workspace 3'),
+ 'cmd_move_con_to_workspace_name(3)',
+ 'to (move to workspace 3) ok');
+
+is(parser_calls('move window to workspace 3'),
+ 'cmd_move_con_to_workspace_name(3)',
+ 'window to (move window to workspace 3) ok');
+
+is(parser_calls('move container to workspace 3'),
+ 'cmd_move_con_to_workspace_name(3)',
+ 'container to (move container to workspace 3) ok');
+
+is(parser_calls('move workspace foobar'),
+ 'cmd_move_con_to_workspace_name(foobar)',
+ 'single word (move workspace foobar) ok');
+
+is(parser_calls('move workspace 3: foobar'),
+ 'cmd_move_con_to_workspace_name(3: foobar)',
+ 'multiple words (move workspace 3: foobar) ok');
+
+is(parser_calls('move workspace "3: foobar"'),
+ 'cmd_move_con_to_workspace_name(3: foobar)',
+ 'double quotes (move workspace "3: foobar") ok');
+
+is(parser_calls('move workspace "3: foobar, baz"'),
+ 'cmd_move_con_to_workspace_name(3: foobar, baz)',
+ 'quotes with comma (move workspace "3: foobar, baz") ok');
+
+is(parser_calls('move workspace 3: foobar, nop foo'),
+ "cmd_move_con_to_workspace_name(3: foobar)\n" .
+ "cmd_nop(foo)",
+ 'multiple ops (move workspace 3: foobar, nop foo) ok');
+
+is(parser_calls('exec i3-sensible-terminal'),
+ 'cmd_exec((null), i3-sensible-terminal)',
+ 'exec ok');
+
+is(parser_calls('exec --no-startup-id i3-sensible-terminal'),
+ 'cmd_exec(--no-startup-id, i3-sensible-terminal)',
+ 'exec --no-startup-id ok');
+
+is(parser_calls('resize shrink left'),
+ 'cmd_resize(shrink, left, 10, 10)',
+ 'simple resize ok');
+
+is(parser_calls('resize shrink left 25 px'),
+ 'cmd_resize(shrink, left, 25, 10)',
+ 'px resize ok');
+
+is(parser_calls('resize shrink left 25 px or 33 ppt'),
+ 'cmd_resize(shrink, left, 25, 33)',
+ 'px + ppt resize ok');
+
+is(parser_calls('resize shrink left 25 px or 33 ppt'),
+ 'cmd_resize(shrink, left, 25, 33)',
+ 'px + ppt resize ok');
+
+is(parser_calls('resize shrink left 25 px or 33 ppt,'),
+ 'cmd_resize(shrink, left, 25, 33)',
+ 'trailing comma resize ok');
+
+is(parser_calls('resize shrink left 25 px or 33 ppt;'),
+ 'cmd_resize(shrink, left, 25, 33)',
+ 'trailing semicolon resize ok');
+
+is(parser_calls('resize shrink left 25'),
+ 'cmd_resize(shrink, left, 25, 10)',
+ 'resize early end ok');
+
+is(parser_calls('[con_mark=yay] focus'),
+ "cmd_criteria_add(con_mark, yay)\n" .
+ "cmd_focus()",
+ 'criteria focus ok');
+
+is(parser_calls("[con_mark=yay con_mark=bar] focus"),
+ "cmd_criteria_add(con_mark, yay)\n" .
+ "cmd_criteria_add(con_mark, bar)\n" .
+ "cmd_focus()",
+ 'criteria focus ok');
+
+is(parser_calls("[con_mark=yay\tcon_mark=bar] focus"),
+ "cmd_criteria_add(con_mark, yay)\n" .
+ "cmd_criteria_add(con_mark, bar)\n" .
+ "cmd_focus()",
+ 'criteria focus ok');
+
+is(parser_calls("[con_mark=yay\tcon_mark=bar]\tfocus"),
+ "cmd_criteria_add(con_mark, yay)\n" .
+ "cmd_criteria_add(con_mark, bar)\n" .
+ "cmd_focus()",
+ 'criteria focus ok');
+
+is(parser_calls('[con_mark="yay"] focus'),
+ "cmd_criteria_add(con_mark, yay)\n" .
+ "cmd_focus()",
+ 'quoted criteria focus ok');
+
+################################################################################
+# 2: Verify that the parser spits out the right error message on commands which
+# are not ok.
+################################################################################
+
+is(parser_calls('unknown_literal'),
+ "Expected one of these tokens: <end>, '[', 'move', 'exec', 'exit', 'restart', 'reload', 'border', 'layout', 'append_layout', 'workspace', 'focus', 'kill', 'open', 'fullscreen', 'split', 'floating', 'mark', 'resize', 'nop', 'scratchpad', 'mode'\n" .
+ "Your command: unknown_literal\n" .
+ " ^^^^^^^^^^^^^^^",
+ 'error for unknown literal ok');
+
+is(parser_calls('move something to somewhere'),
+ "Expected one of these tokens: 'window', 'container', 'to', 'workspace', 'output', 'scratchpad', 'left', 'right', 'up', 'down'\n" .
+ "Your command: move something to somewhere\n" .
+ " ^^^^^^^^^^^^^^^^^^^^^^",
+ 'error for unknown literal ok');
+
+done_testing;