]> git.sur5r.net Git - i3/i3/commitdiff
Merge branch 'new-parser' into next
authorMichael Stapelberg <michael@stapelberg.de>
Sat, 14 Jan 2012 21:30:02 +0000 (21:30 +0000)
committerMichael Stapelberg <michael@stapelberg.de>
Sat, 14 Jan 2012 21:30:02 +0000 (21:30 +0000)
13 files changed:
Makefile
generate-command-parser.pl [new file with mode: 0755]
include/all.h
include/commands.h [new file with mode: 0644]
include/commands_parser.h [new file with mode: 0644]
parser-specs/commands.spec [new file with mode: 0644]
parser-specs/highlighting.vim [new file with mode: 0644]
src/cmdparse.l
src/cmdparse.y
src/commands.c [new file with mode: 0644]
src/commands_parser.c [new file with mode: 0644]
testcases/t/124-move.t
testcases/t/187-commands-parser.t [new file with mode: 0644]

index 8a34ecfc62f417cca878f5ed8d538c3ca3848f26..4e55b2e18a156235acf912bf2b59600ff6fa2c54 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -7,6 +7,7 @@ AUTOGENERATED:=src/cfgparse.tab.c src/cfgparse.yy.c src/cmdparse.tab.c src/cmdpa
 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
@@ -53,6 +54,22 @@ loglevels.h:
        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) $<
@@ -99,8 +116,8 @@ dist: distclean
        [ ! -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
@@ -121,7 +138,7 @@ dist: distclean
        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
diff --git a/generate-command-parser.pl b/generate-command-parser.pl
new file mode 100755 (executable)
index 0000000..9220b30
--- /dev/null
@@ -0,0 +1,204 @@
+#!/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/\(/(&current_match/;
+        } else {
+            $real_cmd =~ s/\(/(&current_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);
index 72c8c8f42ba978be2545ae61230c4c796cef899f..648b0e0ac40727115d0b5f9e238cd9969a1c71fd 100644 (file)
@@ -73,5 +73,7 @@
 #include "libi3.h"
 #include "startup.h"
 #include "scratchpad.h"
+#include "commands.h"
+#include "commands_parser.h"
 
 #endif
diff --git a/include/commands.h b/include/commands.h
new file mode 100644 (file)
index 0000000..7381245
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * 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_H
+#define _COMMANDS_H
+
+/*
+ * Helper data structure for an operation window (window on which the operation
+ * will be performed). Used to build the TAILQ owindows.
+ *
+ */
+typedef struct owindow {
+    Con *con;
+    TAILQ_ENTRY(owindow) owindows;
+} owindow;
+
+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);
+
+char *cmd_move_con_to_workspace(Match *current_match, char *which);
+char *cmd_move_con_to_workspace_name(Match *current_match, char *name);
+char *cmd_resize(Match *current_match, char *way, char *direction, char *resize_px, char *resize_ppt);
+char *cmd_border(Match *current_match, char *border_style_str);
+char *cmd_nop(Match *current_match, char *comment);
+char *cmd_append_layout(Match *current_match, char *path);
+char *cmd_workspace(Match *current_match, char *which);
+char *cmd_workspace_back_and_forth(Match *current_match);
+char *cmd_workspace_name(Match *current_match, char *name);
+char *cmd_mark(Match *current_match, char *mark);
+char *cmd_mode(Match *current_match, char *mode);
+char *cmd_move_con_to_output(Match *current_match, char *name);
+char *cmd_floating(Match *current_match, char *floating_mode);
+char *cmd_move_workspace_to_output(Match *current_match, char *name);
+char *cmd_split(Match *current_match, char *direction);
+char *cmd_kill(Match *current_match, char *kill_mode);
+char *cmd_exec(Match *current_match, char *nosn, char *command);
+char *cmd_focus_direction(Match *current_match, char *direction);
+char *cmd_focus_window_mode(Match *current_match, char *window_mode);
+char *cmd_focus_level(Match *current_match, char *level);
+char *cmd_focus(Match *current_match);
+char *cmd_fullscreen(Match *current_match, char *fullscreen_mode);
+char *cmd_move_direction(Match *current_match, char *direction, char *px);
+char *cmd_layout(Match *current_match, char *layout);
+char *cmd_exit(Match *current_match);
+char *cmd_reload(Match *current_match);
+char *cmd_restart(Match *current_match);
+char *cmd_open(Match *current_match);
+char *cmd_focus_output(Match *current_match, char *name);
+char *cmd_move_scratchpad(Match *current_match);
+char *cmd_scratchpad_show(Match *current_match);
+
+#endif
diff --git a/include/commands_parser.h b/include/commands_parser.h
new file mode 100644 (file)
index 0000000..77569ec
--- /dev/null
@@ -0,0 +1,15 @@
+/*
+ * 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
diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec
new file mode 100644 (file)
index 0000000..0fba3e3
--- /dev/null
@@ -0,0 +1,233 @@
+# 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()
diff --git a/parser-specs/highlighting.vim b/parser-specs/highlighting.vim
new file mode 100644 (file)
index 0000000..f3d1aab
--- /dev/null
@@ -0,0 +1,20 @@
+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
index c333f7ae39813dc2f6376833812cb2f44dee3fcf..47a4f4e0f7a6fc01bc8b2346464aa3760348a8dd 100644 (file)
@@ -96,6 +96,7 @@ back_and_forth                  { BEGIN(INITIAL); return TOK_BACK_AND_FORTH; }
 <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; }
index 9ae17ff9a556ae81f82b52d3cb2e422b4ab3a201..4a2c6ab378968ac4af02cb931a775f4f1f7e08ab 100644 (file)
 
 #include "all.h"
 
-/** When the command did not include match criteria (!), we use the currently
- * focused command. Do not confuse this case with a command which included
- * criteria but which did not match any windows. This macro has to be called in
- * every command.
- */
-#define HANDLE_EMPTY_MATCH do { \
-    if (match_is_empty(&current_match)) { \
-        owindow *ow = smalloc(sizeof(owindow)); \
-        ow->con = focused; \
-        TAILQ_INIT(&owindows); \
-        TAILQ_INSERT_TAIL(&owindows, ow, owindows); \
-    } \
-} while (0)
-
 typedef struct yy_buffer_state *YY_BUFFER_STATE;
 extern int cmdyylex(struct context *context);
 extern int cmdyyparse(void);
@@ -40,17 +26,6 @@ YY_BUFFER_STATE cmdyy_scan_string(const char *);
 static struct context *context;
 static Match current_match;
 
-/*
- * Helper data structure for an operation window (window on which the operation
- * will be performed). Used to build the TAILQ owindows.
- *
- */
-typedef struct owindow {
-    Con *con;
-    TAILQ_ENTRY(owindow) owindows;
-} owindow;
-static TAILQ_HEAD(owindows_head, owindow) owindows;
-
 /* Holds the JSON which will be returned via IPC or NULL for the default return
  * message */
 static char *json_output;
@@ -80,11 +55,19 @@ int cmdyywrap() {
 }
 
 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);
 
-    match_init(&current_match);
+    cmd_criteria_init(&current_match);
     context = scalloc(sizeof(struct context));
     context->filename = "cmd";
     if (cmdyyparse() != 0) {
@@ -98,6 +81,8 @@ char *parse_cmd(const char *new) {
     }
     printf("done, json output = %s\n", json_output);
 
+    cmd_MIGRATION_validate();
+
     cmdyylex_destroy();
     FREE(context->line_copy);
     FREE(context->compact_error);
@@ -105,39 +90,6 @@ char *parse_cmd(const char *new) {
     return json_output;
 }
 
-static Output *get_output_from_string(Output *current_output, const char *output_str) {
-    Output *output;
-
-    if (strcasecmp(output_str, "left") == 0) {
-        output = get_output_next(D_LEFT, current_output);
-        if (!output)
-            output = get_output_most(D_RIGHT, current_output);
-    } else if (strcasecmp(output_str, "right") == 0) {
-        output = get_output_next(D_RIGHT, current_output);
-        if (!output)
-            output = get_output_most(D_LEFT, current_output);
-    } else if (strcasecmp(output_str, "up") == 0) {
-        output = get_output_next(D_UP, current_output);
-        if (!output)
-            output = get_output_most(D_DOWN, current_output);
-    } else if (strcasecmp(output_str, "down") == 0) {
-        output = get_output_next(D_DOWN, current_output);
-        if (!output)
-            output = get_output_most(D_UP, current_output);
-    } else output = get_output_by_name(output_str);
-
-    return output;
-}
-
-
-/*
- * Returns true if a is definitely greater than b (using the given epsilon)
- *
- */
-bool definitelyGreaterThan(float a, float b, float epsilon) {
-    return (a - b) > ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
-}
-
 %}
 
 %error-verbose
@@ -237,15 +189,7 @@ commands:
     commands ';' command
     | command
     {
-        owindow *current;
-
-        printf("single command completely parsed, dropping state...\n");
-        while (!TAILQ_EMPTY(&owindows)) {
-            current = TAILQ_FIRST(&owindows);
-            TAILQ_REMOVE(&owindows, current, owindows);
-            free(current);
-        }
-        match_init(&current_match);
+        cmd_criteria_init(&current_match);
     }
     ;
 
@@ -255,72 +199,16 @@ command:
 
 match:
     | matchstart criteria matchend
-    {
-        printf("match parsed\n");
-    }
     ;
 
 matchstart:
     '['
-    {
-        printf("start\n");
-        match_init(&current_match);
-        TAILQ_INIT(&owindows);
-        /* copy all_cons */
-        Con *con;
-        TAILQ_FOREACH(con, &all_cons, all_cons) {
-            owindow *ow = smalloc(sizeof(owindow));
-            ow->con = con;
-            TAILQ_INSERT_TAIL(&owindows, ow, owindows);
-        }
-    }
     ;
 
 matchend:
     ']'
     {
-        owindow *next, *current;
-
-        printf("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 */
-        struct owindows_head old = owindows;
-        TAILQ_INIT(&owindows);
-        for (next = TAILQ_FIRST(&old); next != TAILQ_END(&old);) {
-            /* make a copy of the next pointer and advance the pointer to the
-             * next element as we are going to invalidate the element’s
-             * next/prev pointers by calling TAILQ_INSERT_TAIL later */
-            current = next;
-            next = TAILQ_NEXT(next, owindows);
-
-            printf("checking if con %p / %s matches\n", current->con, current->con->name);
-            if (current_match.con_id != NULL) {
-                if (current_match.con_id == current->con) {
-                    printf("matches container!\n");
-                    TAILQ_INSERT_TAIL(&owindows, current, owindows);
-
-                }
-            } else if (current_match.mark != NULL && current->con->mark != NULL &&
-                       regex_matches(current_match.mark, current->con->mark)) {
-                printf("match by mark\n");
-                TAILQ_INSERT_TAIL(&owindows, current, owindows);
-            } else {
-                if (current->con->window == NULL)
-                    continue;
-                if (match_matches_window(&current_match, current->con->window)) {
-                    printf("matches window!\n");
-                    TAILQ_INSERT_TAIL(&owindows, current, owindows);
-                } else {
-                    printf("doesnt match\n");
-                    free(current);
-                }
-            }
-        }
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-        }
-
+        json_output = cmd_criteria_match_windows(&current_match);
     }
     ;
 
@@ -332,62 +220,37 @@ criteria:
 criterion:
     TOK_CLASS '=' STR
     {
-        printf("criteria: class = %s\n", $3);
-        current_match.class = regex_new($3);
+        cmd_criteria_add(&current_match, "class", $3);
         free($3);
     }
     | TOK_INSTANCE '=' STR
     {
-        printf("criteria: instance = %s\n", $3);
-        current_match.instance = regex_new($3);
+        cmd_criteria_add(&current_match, "instance", $3);
         free($3);
     }
     | TOK_WINDOW_ROLE '=' STR
     {
-        printf("criteria: window_role = %s\n", $3);
-        current_match.role = regex_new($3);
+        cmd_criteria_add(&current_match, "window_role", $3);
         free($3);
     }
     | TOK_CON_ID '=' STR
     {
-        printf("criteria: id = %s\n", $3);
-        char *end;
-        long parsed = strtol($3, &end, 10);
-        if (parsed == LONG_MIN ||
-            parsed == LONG_MAX ||
-            parsed < 0 ||
-            (end && *end != '\0')) {
-            ELOG("Could not parse con id \"%s\"\n", $3);
-        } else {
-            current_match.con_id = (Con*)parsed;
-            printf("id as int = %p\n", current_match.con_id);
-        }
+        cmd_criteria_add(&current_match, "con_id", $3);
+        free($3);
     }
     | TOK_ID '=' STR
     {
-        printf("criteria: window id = %s\n", $3);
-        char *end;
-        long parsed = strtol($3, &end, 10);
-        if (parsed == LONG_MIN ||
-            parsed == LONG_MAX ||
-            parsed < 0 ||
-            (end && *end != '\0')) {
-            ELOG("Could not parse window id \"%s\"\n", $3);
-        } else {
-            current_match.id = parsed;
-            printf("window id as int = %d\n", current_match.id);
-        }
+        cmd_criteria_add(&current_match, "id", $3);
+        free($3);
     }
     | TOK_MARK '=' STR
     {
-        printf("criteria: mark = %s\n", $3);
-        current_match.mark = regex_new($3);
+        cmd_criteria_add(&current_match, "con_mark", $3);
         free($3);
     }
     | TOK_TITLE '=' STR
     {
-        printf("criteria: title = %s\n", $3);
-        current_match.title = regex_new($3);
+        cmd_criteria_add(&current_match, "title", $3);
         free($3);
     }
     ;
@@ -423,11 +286,7 @@ operation:
 exec:
     TOK_EXEC optional_no_startup_id STR
     {
-        char *command = $3;
-        bool no_startup_id = $2;
-
-        printf("should execute %s, no_startup_id = %d\n", command, no_startup_id);
-        start_application(command, no_startup_id);
+        json_output = cmd_exec(&current_match, ($2 ? "--no-startup-id" : NULL), $3);
         free($3);
     }
     ;
@@ -440,208 +299,53 @@ optional_no_startup_id:
 exit:
     TOK_EXIT
     {
-        printf("exit, bye bye\n");
-        exit(0);
+        json_output = cmd_exit(&current_match);
     }
     ;
 
 reload:
     TOK_RELOAD
     {
-        printf("reloading\n");
-        kill_configerror_nagbar(false);
-        load_configuration(conn, NULL, true);
-        x_set_i3_atoms();
-        /* Send an IPC event just in case the ws names have changed */
-        ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"reload\"}");
+        json_output = cmd_reload(&current_match);
     }
     ;
 
 restart:
     TOK_RESTART
     {
-        printf("restarting i3\n");
-        i3_restart(false);
+        json_output = cmd_restart(&current_match);
     }
     ;
 
 focus:
     TOK_FOCUS
     {
-        if (focused &&
-            focused->type != CT_WORKSPACE &&
-            focused->fullscreen_mode != CF_NONE) {
-            LOG("Cannot change focus while in fullscreen mode.\n");
-            break;
-        }
-
-        owindow *current;
-
-        if (match_is_empty(&current_match)) {
-            ELOG("You have to specify which window/container should be focused.\n");
-            ELOG("Example: [class=\"urxvt\" title=\"irssi\"] focus\n");
-
-            sasprintf(&json_output, "{\"success\":false, \"error\":\"You have to "
-                      "specify which window/container should be focused\"}");
-            break;
-        }
-
-        int count = 0;
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            Con *ws = con_get_workspace(current->con);
-            /* If no workspace could be found, this was a dock window.
-             * Just skip it, you cannot focus dock windows. */
-            if (!ws)
-                continue;
-
-            /* If the container is not on the current workspace,
-             * workspace_show() will switch to a different workspace and (if
-             * enabled) trigger a mouse pointer warp to the currently focused
-             * container (!) on the target workspace.
-             *
-             * Therefore, before calling workspace_show(), we make sure that
-             * 'current' will be focused on the workspace. However, we cannot
-             * just con_focus(current) because then the pointer will not be
-             * warped at all (the code thinks we are already there).
-             *
-             * So we focus 'current' to make it the currently focused window of
-             * the target workspace, then revert focus. */
-            Con *currently_focused = focused;
-            con_focus(current->con);
-            con_focus(currently_focused);
-
-            /* Now switch to the workspace, then focus */
-            workspace_show(ws);
-            LOG("focusing %p / %s\n", current->con, current->con->name);
-            con_focus(current->con);
-            count++;
-        }
-
-        if (count > 1)
-            LOG("WARNING: Your criteria for the focus command matches %d containers, "
-                "while only exactly one container can be focused at a time.\n", count);
-
-        tree_render();
+        json_output = cmd_focus(&current_match);
     }
     | TOK_FOCUS direction
     {
-        if (focused &&
-            focused->type != CT_WORKSPACE &&
-            focused->fullscreen_mode != CF_NONE) {
-            LOG("Cannot change focus while in fullscreen mode.\n");
-            break;
-        }
-
-        int direction = $2;
-        switch (direction) {
-            case TOK_LEFT:
-                LOG("Focusing left\n");
-                tree_next('p', HORIZ);
-                break;
-            case TOK_RIGHT:
-                LOG("Focusing right\n");
-                tree_next('n', HORIZ);
-                break;
-            case TOK_UP:
-                LOG("Focusing up\n");
-                tree_next('p', VERT);
-                break;
-            case TOK_DOWN:
-                LOG("Focusing down\n");
-                tree_next('n', VERT);
-                break;
-            default:
-                ELOG("Invalid focus direction (%d)\n", direction);
-                break;
-        }
-
-        tree_render();
+        json_output = cmd_focus_direction(&current_match,
+                            ($2 == TOK_LEFT ? "left" :
+                             ($2 == TOK_RIGHT ? "right" :
+                              ($2 == TOK_UP ? "up" :
+                               "down"))));
     }
     | TOK_FOCUS TOK_OUTPUT STR
     {
-        owindow *current;
-
-        HANDLE_EMPTY_MATCH;
-
-        /* get the output */
-        Output *current_output = NULL;
-        Output *output;
-
-        TAILQ_FOREACH(current, &owindows, owindows)
-            current_output = get_output_containing(current->con->rect.x, current->con->rect.y);
-        assert(current_output != NULL);
-
-        output = get_output_from_string(current_output, $3);
-
+        json_output = cmd_focus_output(&current_match, $3);
         free($3);
-
-        if (!output) {
-            printf("No such output found.\n");
-            break;
-        }
-
-        /* get visible workspace on output */
-        Con *ws = NULL;
-        GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child));
-        if (!ws)
-            break;
-
-        workspace_show(ws);
-        tree_render();
     }
     | TOK_FOCUS window_mode
     {
-        if (focused &&
-            focused->type != CT_WORKSPACE &&
-            focused->fullscreen_mode != CF_NONE) {
-            LOG("Cannot change focus while in fullscreen mode.\n");
-            break;
-        }
-
-        printf("should focus: ");
-
-        if ($2 == TOK_TILING)
-            printf("tiling\n");
-        else if ($2 == TOK_FLOATING)
-            printf("floating\n");
-        else printf("mode toggle\n");
-
-        Con *ws = con_get_workspace(focused);
-        Con *current;
-        if (ws != NULL) {
-            int to_focus = $2;
-            if ($2 == TOK_MODE_TOGGLE) {
-                current = TAILQ_FIRST(&(ws->focus_head));
-                if (current != NULL && current->type == CT_FLOATING_CON)
-                    to_focus = TOK_TILING;
-                else to_focus = TOK_FLOATING;
-            }
-            TAILQ_FOREACH(current, &(ws->focus_head), focused) {
-                if ((to_focus == TOK_FLOATING && current->type != CT_FLOATING_CON) ||
-                    (to_focus == TOK_TILING && current->type == CT_FLOATING_CON))
-                    continue;
-
-                con_focus(con_descend_focused(current));
-                break;
-            }
-        }
-
-        tree_render();
+
+        json_output = cmd_focus_window_mode(&current_match,
+                              ($2 == TOK_TILING ? "tiling" :
+                               ($2 == TOK_FLOATING ? "floating" :
+                                "mode_toggle")));
     }
     | TOK_FOCUS level
     {
-        if (focused &&
-            focused->type != CT_WORKSPACE &&
-            focused->fullscreen_mode != CF_NONE) {
-            LOG("Cannot change focus while in fullscreen mode.\n");
-            break;
-        }
-
-        if ($2 == TOK_PARENT)
-            level_up();
-        else level_down();
-
-        tree_render();
+        json_output = cmd_focus_level(&current_match, ($2 == TOK_PARENT ? "parent" : "child"));
     }
     ;
 
@@ -659,20 +363,7 @@ level:
 kill:
     TOK_KILL optional_kill_mode
     {
-        owindow *current;
-
-        printf("killing!\n");
-        /* check if the match is empty, not if the result is empty */
-        if (match_is_empty(&current_match))
-            tree_close_con($2);
-        else {
-            TAILQ_FOREACH(current, &owindows, owindows) {
-                printf("matching: %p / %s\n", current->con, current->con->name);
-                tree_close(current->con, $2, false, false);
-            }
-        }
-
-        tree_render();
+        json_output = cmd_kill(&current_match, ($2 == KILL_WINDOW ? "window" : "client"));
     }
     ;
 
@@ -685,84 +376,42 @@ optional_kill_mode:
 workspace:
     TOK_WORKSPACE TOK_NEXT
     {
-        workspace_show(workspace_next());
-        tree_render();
+        json_output = cmd_workspace(&current_match, "next");
     }
     | TOK_WORKSPACE TOK_PREV
     {
-        workspace_show(workspace_prev());
-        tree_render();
+        json_output = cmd_workspace(&current_match, "prev");
     }
     | TOK_WORKSPACE TOK_NEXT_ON_OUTPUT
     {
-        workspace_show(workspace_next_on_output());
-        tree_render();
+        json_output = cmd_workspace(&current_match, "next_on_output");
     }
     | TOK_WORKSPACE TOK_PREV_ON_OUTPUT
     {
-        workspace_show(workspace_prev_on_output());
-        tree_render();
+        json_output = cmd_workspace(&current_match, "prev_on_output");
     }
     | TOK_WORKSPACE TOK_BACK_AND_FORTH
     {
-        workspace_back_and_forth();
-        tree_render();
+        json_output = cmd_workspace_back_and_forth(&current_match);
     }
     | TOK_WORKSPACE STR
     {
-        if (strncasecmp($2, "__i3_", strlen("__i3_")) == 0) {
-            printf("You cannot switch to the i3 internal workspaces.\n");
-            break;
-        }
-
-        printf("should switch to workspace %s\n", $2);
-
-        Con *ws = con_get_workspace(focused);
-
-        /* Check if the command wants to switch to the current workspace */
-        if (strcmp(ws->name, $2) == 0) {
-            printf("This workspace is already focused.\n");
-            if (config.workspace_auto_back_and_forth) {
-                workspace_back_and_forth();
-                free($2);
-                tree_render();
-            }
-            break;
-        }
-
-        workspace_show_by_name($2);
+        json_output = cmd_workspace_name(&current_match, $2);
         free($2);
-
-        tree_render();
     }
     ;
 
 open:
     TOK_OPEN
     {
-        printf("opening new container\n");
-        Con *con = tree_open_con(NULL, NULL);
-        con_focus(con);
-        sasprintf(&json_output, "{\"success\":true, \"id\":%ld}", (long int)con);
-
-        tree_render();
+        json_output = cmd_open(&current_match);
     }
     ;
 
 fullscreen:
     TOK_FULLSCREEN fullscreen_mode
     {
-        printf("toggling fullscreen, mode = %s\n", ($2 == CF_OUTPUT ? "normal" : "global"));
-        owindow *current;
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            con_toggle_fullscreen(current->con, $2);
-        }
-
-        tree_render();
+        json_output = cmd_fullscreen(&current_match, ($2 == CF_OUTPUT ? "output" : "global"));
     }
     ;
 
@@ -774,11 +423,9 @@ fullscreen_mode:
 split:
     TOK_SPLIT split_direction
     {
-        /* TODO: use matches */
-        printf("splitting in direction %c\n", $2);
-        tree_split(focused, ($2 == 'v' ? VERT : HORIZ));
-
-        tree_render();
+        char buf[2] = {'\0', '\0'};
+        buf[0] = $2;
+        json_output = cmd_split(&current_match, buf);
     }
     ;
 
@@ -792,25 +439,10 @@ split_direction:
 floating:
     TOK_FLOATING boolean
     {
-        HANDLE_EMPTY_MATCH;
-
-        owindow *current;
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            if ($2 == TOK_TOGGLE) {
-                printf("should toggle mode\n");
-                toggle_floating_mode(current->con, false);
-            } else {
-                printf("should switch mode to %s\n", ($2 == TOK_FLOATING ? "floating" : "tiling"));
-                if ($2 == TOK_ENABLE) {
-                    floating_enable(current->con, false);
-                } else {
-                    floating_disable(current->con, false);
-                }
-            }
-        }
-
-        tree_render();
+        json_output = cmd_floating(&current_match,
+                     ($2 == TOK_ENABLE ? "enable" :
+                      ($2 == TOK_DISABLE ? "disable" :
+                       "toggle")));
     }
     ;
 
@@ -823,22 +455,11 @@ boolean:
 border:
     TOK_BORDER border_style
     {
-        printf("border style should be changed to %d\n", $2);
-        owindow *current;
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            int border_style = current->con->border_style;
-            if ($2 == TOK_TOGGLE) {
-                border_style++;
-                border_style %= 3;
-            } else border_style = $2;
-            con_set_border_style(current->con, border_style);
-        }
-
-        tree_render();
+        json_output = cmd_border(&current_match,
+                   ($2 == BS_NORMAL ? "normal" :
+                    ($2 == BS_NONE ? "none" :
+                     ($2 == BS_1PIXEL ? "1pixel" :
+                      "toggle"))));
     }
     ;
 
@@ -852,256 +473,66 @@ border_style:
 move:
     TOK_MOVE direction resize_px
     {
-        int direction = $2;
-        int px = $3;
-
-        /* TODO: make 'move' work with criteria. */
-        printf("moving in direction %d\n", direction);
-        if (con_is_floating(focused)) {
-            printf("floating move with %d pixels\n", px);
-            Rect newrect = focused->parent->rect;
-            if (direction == TOK_LEFT) {
-                newrect.x -= px;
-            } else if (direction == TOK_RIGHT) {
-                newrect.x += px;
-            } else if (direction == TOK_UP) {
-                newrect.y -= px;
-            } else if (direction == TOK_DOWN) {
-                newrect.y += px;
-            }
-            floating_reposition(focused->parent, newrect);
-        } else {
-            tree_move(direction);
-            tree_render();
-        }
+        char buffer[128];
+        snprintf(buffer, sizeof(buffer), "%d", $3);
+        json_output = cmd_move_direction(&current_match,
+                           ($2 == TOK_LEFT ? "left" :
+                            ($2 == TOK_RIGHT ? "right" :
+                             ($2 == TOK_UP ? "up" :
+                              "down"))),
+                           buffer);
     }
     | TOK_MOVE TOK_WORKSPACE STR
     {
-        if (strncasecmp($3, "__i3_", strlen("__i3_")) == 0) {
-            printf("You cannot switch to the i3 internal workspaces.\n");
-            break;
-        }
-
-        owindow *current;
-
-        /* Error out early to not create a non-existing workspace (in
-         * workspace_get()) if we are not actually able to move anything. */
-        if (match_is_empty(&current_match) && focused->type == CT_WORKSPACE)
-            break;
-
-        printf("should move window to workspace %s\n", $3);
-        /* get the workspace */
-        Con *ws = workspace_get($3, NULL);
-        free($3);
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            con_move_to_workspace(current->con, ws, true, false);
-        }
-
-        tree_render();
+        json_output = cmd_move_con_to_workspace_name(&current_match, $3);
     }
     | TOK_MOVE TOK_WORKSPACE TOK_NEXT
     {
-        owindow *current;
-
-        /* get the workspace */
-        Con *ws = workspace_next();
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            con_move_to_workspace(current->con, ws, true, false);
-        }
-
-        tree_render();
+        json_output = cmd_move_con_to_workspace(&current_match, "next");
     }
     | TOK_MOVE TOK_WORKSPACE TOK_PREV
     {
-        owindow *current;
-
-        /* get the workspace */
-        Con *ws = workspace_prev();
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            con_move_to_workspace(current->con, ws, true, false);
-        }
-
-        tree_render();
+        json_output = cmd_move_con_to_workspace(&current_match, "prev");
     }
     | TOK_MOVE TOK_WORKSPACE TOK_NEXT_ON_OUTPUT
     {
-        owindow *current;
-
-        /* get the workspace */
-        Con *ws = workspace_next_on_output();
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            con_move_to_workspace(current->con, ws, true, false);
-        }
-
-        tree_render();
+        json_output = cmd_move_con_to_workspace(&current_match, "next_on_output");
     }
     | TOK_MOVE TOK_WORKSPACE TOK_PREV_ON_OUTPUT
     {
-        owindow *current;
-
-        /* get the workspace */
-        Con *ws = workspace_prev_on_output();
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            con_move_to_workspace(current->con, ws, true, false);
-        }
-
-        tree_render();
+        json_output = cmd_move_con_to_workspace(&current_match, "prev_on_output");
     }
     | TOK_MOVE TOK_OUTPUT STR
     {
-        owindow *current;
-
-        printf("should move window to output %s\n", $3);
-
-        HANDLE_EMPTY_MATCH;
-
-        /* get the output */
-        Output *current_output = NULL;
-        Output *output;
-
-        TAILQ_FOREACH(current, &owindows, owindows)
-            current_output = get_output_containing(current->con->rect.x, current->con->rect.y);
-
-        assert(current_output != NULL);
-
-        if (strcasecmp($3, "up") == 0)
-            output = get_output_next(D_UP, current_output);
-        else if (strcasecmp($3, "down") == 0)
-            output = get_output_next(D_DOWN, current_output);
-        else if (strcasecmp($3, "left") == 0)
-            output = get_output_next(D_LEFT, current_output);
-        else if (strcasecmp($3, "right") == 0)
-            output = get_output_next(D_RIGHT, current_output);
-        else
-            output = get_output_by_name($3);
+        json_output = cmd_move_con_to_output(&current_match, $3);
         free($3);
-
-        if (!output) {
-            printf("No such output found.\n");
-            break;
-        }
-
-        /* get visible workspace on output */
-        Con *ws = NULL;
-        GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child));
-        if (!ws)
-            break;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            con_move_to_workspace(current->con, ws, true, false);
-        }
-
-        tree_render();
     }
     | TOK_MOVE TOK_SCRATCHPAD
     {
-        printf("should move window to scratchpad\n");
-        owindow *current;
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            scratchpad_move(current->con);
-        }
-
-        tree_render();
+        json_output = cmd_move_scratchpad(&current_match);
     }
     | TOK_MOVE TOK_WORKSPACE TOK_TO TOK_OUTPUT STR
     {
-        printf("should move workspace to output %s\n", $5);
-
-        HANDLE_EMPTY_MATCH;
-
-        owindow *current;
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            Output *current_output = get_output_containing(current->con->rect.x,
-                                                           current->con->rect.y);
-            Output *output = get_output_from_string(current_output, $5);
-            if (!output) {
-                printf("No such output\n");
-                break;
-            }
-
-            Con *content = output_get_content(output->con);
-            LOG("got output %p with content %p\n", output, content);
-
-            Con *ws = con_get_workspace(current->con);
-            printf("should move workspace %p / %s\n", ws, ws->name);
-            if (con_num_children(ws->parent) == 1) {
-                printf("Not moving workspace \"%s\", it is the only workspace on its output.\n", ws->name);
-                continue;
-            }
-            bool workspace_was_visible = workspace_is_visible(ws);
-            Con *old_content = ws->parent;
-            con_detach(ws);
-            if (workspace_was_visible) {
-                /* The workspace which we just detached was visible, so focus
-                 * the next one in the focus-stack. */
-                Con *focus_ws = TAILQ_FIRST(&(old_content->focus_head));
-                printf("workspace was visible, focusing %p / %s now\n", focus_ws, focus_ws->name);
-                workspace_show(focus_ws);
-            }
-            con_attach(ws, content, false);
-            ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"move\"}");
-            if (workspace_was_visible) {
-                /* Focus the moved workspace on the destination output. */
-                workspace_show(ws);
-            }
-        }
-
-        tree_render();
+        json_output = cmd_move_workspace_to_output(&current_match, $5);
+        free($5);
     }
     ;
 
 append_layout:
     TOK_APPEND_LAYOUT STR
     {
-        printf("restoring \"%s\"\n", $2);
-        tree_append_json($2);
+        json_output = cmd_append_layout(&current_match, $2);
         free($2);
-        tree_render();
     }
     ;
 
 layout:
     TOK_LAYOUT layout_mode
     {
-        printf("changing layout to %d\n", $2);
-        owindow *current;
-
-        /* check if the match is empty, not if the result is empty */
-        if (match_is_empty(&current_match))
-            con_set_layout(focused->parent, $2);
-        else {
-            TAILQ_FOREACH(current, &owindows, owindows) {
-                printf("matching: %p / %s\n", current->con, current->con->name);
-                con_set_layout(current->con, $2);
-            }
-        }
-
-        tree_render();
+        json_output = cmd_layout(&current_match,
+                   ($2 == L_DEFAULT ? "default" :
+                    ($2 == L_STACKED ? "stacked" :
+                     "tabbed")));
     }
     ;
 
@@ -1114,34 +545,15 @@ layout_mode:
 mark:
     TOK_MARK STR
     {
-        printf("Clearing all windows which have that mark first\n");
-
-        Con *con;
-        TAILQ_FOREACH(con, &all_cons, all_cons) {
-            if (con->mark && strcmp(con->mark, $2) == 0)
-                FREE(con->mark);
-        }
-
-        printf("marking window with str %s\n", $2);
-        owindow *current;
-
-        HANDLE_EMPTY_MATCH;
-
-        TAILQ_FOREACH(current, &owindows, owindows) {
-            printf("matching: %p / %s\n", current->con, current->con->name);
-            current->con->mark = $2;
-        }
-
-        tree_render();
+        json_output = cmd_mark(&current_match, $2);
+        free($2);
     }
     ;
 
 nop:
     TOK_NOP STR
     {
-        printf("-------------------------------------------------\n");
-        printf("  NOP: %s\n", $2);
-        printf("-------------------------------------------------\n");
+        json_output = cmd_nop(&current_match, $2);
         free($2);
     }
     ;
@@ -1149,19 +561,7 @@ nop:
 scratchpad:
     TOK_SCRATCHPAD TOK_SHOW
     {
-        printf("should show scratchpad window\n");
-        owindow *current;
-
-        if (match_is_empty(&current_match)) {
-            scratchpad_show(NULL);
-        } else {
-            TAILQ_FOREACH(current, &owindows, owindows) {
-                printf("matching: %p / %s\n", current->con, current->con->name);
-                scratchpad_show(current->con);
-            }
-        }
-
-        tree_render();
+        json_output = cmd_scratchpad_show(&current_match);
     }
     ;
 
@@ -1169,98 +569,17 @@ scratchpad:
 resize:
     TOK_RESIZE resize_way direction resize_px resize_tiling
     {
-        /* resize <grow|shrink> <direction> [<px> px] [or <ppt> ppt] */
-        printf("resizing in way %d, direction %d, px %d or ppt %d\n", $2, $3, $4, $5);
-        int direction = $3;
-        int px = $4;
-        int ppt = $5;
-        if ($2 == TOK_SHRINK) {
-            px *= -1;
-            ppt *= -1;
-        }
-
-        Con *floating_con;
-        if ((floating_con = con_inside_floating(focused))) {
-            printf("floating resize\n");
-            if (direction == TOK_UP) {
-                floating_con->rect.y -= px;
-                floating_con->rect.height += px;
-            } else if (direction == TOK_DOWN) {
-                floating_con->rect.height += px;
-            } else if (direction == TOK_LEFT) {
-                floating_con->rect.x -= px;
-                floating_con->rect.width += px;
-            } else {
-                floating_con->rect.width += px;
-            }
-        } else {
-            LOG("tiling resize\n");
-            /* get the appropriate current container (skip stacked/tabbed cons) */
-            Con *current = focused;
-            while (current->parent->layout == L_STACKED ||
-                   current->parent->layout == L_TABBED)
-                current = current->parent;
-
-            /* Then further go up until we find one with the matching orientation. */
-            orientation_t search_orientation =
-                (direction == TOK_LEFT || direction == TOK_RIGHT ? HORIZ : VERT);
-
-            while (current->type != CT_WORKSPACE &&
-                   current->type != CT_FLOATING_CON &&
-                   current->parent->orientation != search_orientation)
-                current = current->parent;
-
-            /* get the default percentage */
-            int children = con_num_children(current->parent);
-            Con *other;
-            LOG("ins. %d children\n", children);
-            double percentage = 1.0 / children;
-            LOG("default percentage = %f\n", percentage);
-
-            orientation_t orientation = current->parent->orientation;
-
-            if ((orientation == HORIZ &&
-                 (direction == TOK_UP || direction == TOK_DOWN)) ||
-                (orientation == VERT &&
-                 (direction == TOK_LEFT || direction == TOK_RIGHT))) {
-                LOG("You cannot resize in that direction. Your focus is in a %s split container currently.\n",
-                    (orientation == HORIZ ? "horizontal" : "vertical"));
-                break;
-            }
-
-            if (direction == TOK_UP || direction == TOK_LEFT) {
-                other = TAILQ_PREV(current, nodes_head, nodes);
-            } else {
-                other = TAILQ_NEXT(current, nodes);
-            }
-            if (other == TAILQ_END(workspaces)) {
-                LOG("No other container in this direction found, cannot resize.\n");
-                break;
-            }
-            LOG("other->percent = %f\n", other->percent);
-            LOG("current->percent before = %f\n", current->percent);
-            if (current->percent == 0.0)
-                current->percent = percentage;
-            if (other->percent == 0.0)
-                other->percent = percentage;
-            double new_current_percent = current->percent + ((double)ppt / 100.0);
-            double new_other_percent = other->percent - ((double)ppt / 100.0);
-            LOG("new_current_percent = %f\n", new_current_percent);
-            LOG("new_other_percent = %f\n", new_other_percent);
-            /* Ensure that the new percentages are positive and greater than
-             * 0.05 to have a reasonable minimum size. */
-            if (definitelyGreaterThan(new_current_percent, 0.05, DBL_EPSILON) &&
-                definitelyGreaterThan(new_other_percent, 0.05, DBL_EPSILON)) {
-                current->percent += ((double)ppt / 100.0);
-                other->percent -= ((double)ppt / 100.0);
-                LOG("current->percent after = %f\n", current->percent);
-                LOG("other->percent after = %f\n", other->percent);
-            } else {
-                LOG("Not resizing, already at minimum size\n");
-            }
-        }
-
-        tree_render();
+        char buffer1[128], buffer2[128];
+        snprintf(buffer1, sizeof(buffer1), "%d", $4);
+        snprintf(buffer2, sizeof(buffer2), "%d", $5);
+        json_output = cmd_resize(&current_match,
+                   ($2 == TOK_SHRINK ? "shrink" : "grow"),
+                   ($3 == TOK_LEFT ? "left" :
+                    ($3 == TOK_RIGHT ? "right" :
+                     ($3 == TOK_DOWN ? "down" :
+                      "up"))),
+                   buffer1,
+                   buffer2);
     }
     ;
 
@@ -1301,6 +620,7 @@ direction:
 mode:
     TOK_MODE STR
     {
-        switch_mode($2);
+        json_output = cmd_mode(&current_match, $2);
+        free($2);
     }
     ;
diff --git a/src/commands.c b/src/commands.c
new file mode 100644 (file)
index 0000000..4071097
--- /dev/null
@@ -0,0 +1,1319 @@
+/*
+ * 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)
+ *
+ */
+#include <float.h>
+#include <stdarg.h>
+
+#include "all.h"
+#include "cmdparse.tab.h"
+
+/** When the command did not include match criteria (!), we use the currently
+ * focused command. Do not confuse this case with a command which included
+ * criteria but which did not match any windows. This macro has to be called in
+ * every command.
+ */
+#define HANDLE_EMPTY_MATCH do { \
+    if (match_is_empty(current_match)) { \
+        owindow *ow = smalloc(sizeof(owindow)); \
+        ow->con = focused; \
+        TAILQ_INIT(&owindows); \
+        TAILQ_INSERT_TAIL(&owindows, ow, owindows); \
+    } \
+} while (0)
+
+static owindows_head owindows;
+
+/*
+ * Returns true if a is definitely greater than b (using the given epsilon)
+ *
+ */
+static bool definitelyGreaterThan(float a, float b, float epsilon) {
+    return (a - b) > ( (fabs(a) < fabs(b) ? fabs(b) : fabs(a)) * epsilon);
+}
+
+static Output *get_output_from_string(Output *current_output, const char *output_str) {
+    Output *output;
+
+    if (strcasecmp(output_str, "left") == 0) {
+        output = get_output_next(D_LEFT, current_output);
+        if (!output)
+            output = get_output_most(D_RIGHT, current_output);
+    } else if (strcasecmp(output_str, "right") == 0) {
+        output = get_output_next(D_RIGHT, current_output);
+        if (!output)
+            output = get_output_most(D_LEFT, current_output);
+    } else if (strcasecmp(output_str, "up") == 0) {
+        output = get_output_next(D_UP, current_output);
+        if (!output)
+            output = get_output_most(D_DOWN, current_output);
+    } else if (strcasecmp(output_str, "down") == 0) {
+        output = get_output_next(D_DOWN, current_output);
+        if (!output)
+            output = get_output_most(D_UP, current_output);
+    } else output = get_output_by_name(output_str);
+
+    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);
+    TAILQ_INIT(&owindows);
+    /* copy all_cons */
+    Con *con;
+    TAILQ_FOREACH(con, &all_cons, all_cons) {
+        owindow *ow = smalloc(sizeof(owindow));
+        ow->con = con;
+        TAILQ_INSERT_TAIL(&owindows, ow, owindows);
+    }
+
+    /* This command is internal and does not generate a JSON reply. */
+    return NULL;
+}
+
+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 */
+    struct owindows_head old = owindows;
+    TAILQ_INIT(&owindows);
+    for (next = TAILQ_FIRST(&old); next != TAILQ_END(&old);) {
+        /* make a copy of the next pointer and advance the pointer to the
+         * next element as we are going to invalidate the element’s
+         * next/prev pointers by calling TAILQ_INSERT_TAIL later */
+        current = next;
+        next = TAILQ_NEXT(next, owindows);
+
+        DLOG("checking if con %p / %s matches\n", current->con, current->con->name);
+        if (current_match->con_id != NULL) {
+            if (current_match->con_id == current->con) {
+                DLOG("matches container!\n");
+                TAILQ_INSERT_TAIL(&owindows, current, owindows);
+            }
+        } else if (current_match->mark != NULL && current->con->mark != NULL &&
+                   regex_matches(current_match->mark, current->con->mark)) {
+            DLOG("match by mark\n");
+            TAILQ_INSERT_TAIL(&owindows, current, owindows);
+        } else {
+            if (current->con->window == NULL)
+                continue;
+            if (match_matches_window(current_match, current->con->window)) {
+                DLOG("matches window!\n");
+                TAILQ_INSERT_TAIL(&owindows, current, owindows);
+            } else {
+                DLOG("doesnt match\n");
+                free(current);
+            }
+        }
+    }
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+    }
+
+    /* This command is internal and does not generate a JSON reply. */
+    return NULL;
+}
+
+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) {
+        current_match->class = regex_new(cvalue);
+        return NULL;
+    }
+
+    if (strcmp(ctype, "instance") == 0) {
+        current_match->instance = regex_new(cvalue);
+        return NULL;
+    }
+
+    if (strcmp(ctype, "window_role") == 0) {
+        current_match->role = regex_new(cvalue);
+        return NULL;
+    }
+
+    if (strcmp(ctype, "con_id") == 0) {
+        char *end;
+        long parsed = strtol(cvalue, &end, 10);
+        if (parsed == LONG_MIN ||
+            parsed == LONG_MAX ||
+            parsed < 0 ||
+            (end && *end != '\0')) {
+            ELOG("Could not parse con id \"%s\"\n", cvalue);
+        } else {
+            current_match->con_id = (Con*)parsed;
+            printf("id as int = %p\n", current_match->con_id);
+        }
+        return NULL;
+    }
+
+    if (strcmp(ctype, "id") == 0) {
+        char *end;
+        long parsed = strtol(cvalue, &end, 10);
+        if (parsed == LONG_MIN ||
+            parsed == LONG_MAX ||
+            parsed < 0 ||
+            (end && *end != '\0')) {
+            ELOG("Could not parse window id \"%s\"\n", cvalue);
+        } else {
+            current_match->id = parsed;
+            printf("window id as int = %d\n", current_match->id);
+        }
+        return NULL;
+    }
+
+    if (strcmp(ctype, "con_mark") == 0) {
+        current_match->mark = regex_new(cvalue);
+        return NULL;
+    }
+
+    if (strcmp(ctype, "title") == 0) {
+        current_match->title = regex_new(cvalue);
+        return NULL;
+    }
+
+    ELOG("Unknown criterion: %s\n", ctype);
+
+    /* This command is internal and does not generate a JSON reply. */
+    return NULL;
+}
+
+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;
+
+    /* get the workspace */
+    Con *ws;
+    if (strcmp(which, "next") == 0)
+        ws = workspace_next();
+    else if (strcmp(which, "prev") == 0)
+        ws = workspace_prev();
+    else if (strcmp(which, "next_on_output") == 0)
+        ws = workspace_next_on_output();
+    else if (strcmp(which, "prev_on_output") == 0)
+        ws = workspace_prev_on_output();
+    else {
+        ELOG("BUG: called with which=%s\n", which);
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+        con_move_to_workspace(current->con, ws, true, false);
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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}");
+    }
+
+    owindow *current;
+
+    /* Error out early to not create a non-existing workspace (in
+     * workspace_get()) if we are not actually able to move anything. */
+    if (match_is_empty(current_match) && focused->type == CT_WORKSPACE)
+        return sstrdup("{\"sucess\": false}");
+
+    LOG("should move window to workspace %s\n", name);
+    /* get the workspace */
+    Con *ws = workspace_get(name, NULL);
+
+    HANDLE_EMPTY_MATCH;
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+        con_move_to_workspace(current->con, ws, true, false);
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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
+    int px = atoi(resize_px);
+    int ppt = atoi(resize_ppt);
+    if (strcmp(way, "shrink") == 0) {
+        px *= -1;
+        ppt *= -1;
+    }
+
+    Con *floating_con;
+    if ((floating_con = con_inside_floating(focused))) {
+        printf("floating resize\n");
+        if (strcmp(direction, "up") == 0) {
+            floating_con->rect.y -= px;
+            floating_con->rect.height += px;
+        } else if (strcmp(direction, "down") == 0) {
+            floating_con->rect.height += px;
+        } else if (strcmp(direction, "left") == 0) {
+            floating_con->rect.x -= px;
+            floating_con->rect.width += px;
+        } else {
+            floating_con->rect.width += px;
+        }
+    } else {
+        LOG("tiling resize\n");
+        /* get the appropriate current container (skip stacked/tabbed cons) */
+        Con *current = focused;
+        while (current->parent->layout == L_STACKED ||
+               current->parent->layout == L_TABBED)
+            current = current->parent;
+
+        /* Then further go up until we find one with the matching orientation. */
+        orientation_t search_orientation =
+            (strcmp(direction, "left") == 0 || strcmp(direction, "right") == 0 ? HORIZ : VERT);
+
+        while (current->type != CT_WORKSPACE &&
+               current->type != CT_FLOATING_CON &&
+               current->parent->orientation != search_orientation)
+            current = current->parent;
+
+        /* get the default percentage */
+        int children = con_num_children(current->parent);
+        Con *other;
+        LOG("ins. %d children\n", children);
+        double percentage = 1.0 / children;
+        LOG("default percentage = %f\n", percentage);
+
+        orientation_t orientation = current->parent->orientation;
+
+        if ((orientation == HORIZ &&
+             (strcmp(direction, "up") == 0 || strcmp(direction, "down") == 0)) ||
+            (orientation == VERT &&
+             (strcmp(direction, "left") == 0 || strcmp(direction, "right") == 0))) {
+            LOG("You cannot resize in that direction. Your focus is in a %s split container currently.\n",
+                (orientation == HORIZ ? "horizontal" : "vertical"));
+            return sstrdup("{\"sucess\": false}");
+        }
+
+        if (strcmp(direction, "up") == 0 || strcmp(direction, "left") == 0) {
+            other = TAILQ_PREV(current, nodes_head, nodes);
+        } else {
+            other = TAILQ_NEXT(current, nodes);
+        }
+        if (other == TAILQ_END(workspaces)) {
+            LOG("No other container in this direction found, cannot resize.\n");
+            return sstrdup("{\"sucess\": false}");
+        }
+        LOG("other->percent = %f\n", other->percent);
+        LOG("current->percent before = %f\n", current->percent);
+        if (current->percent == 0.0)
+            current->percent = percentage;
+        if (other->percent == 0.0)
+            other->percent = percentage;
+        double new_current_percent = current->percent + ((double)ppt / 100.0);
+        double new_other_percent = other->percent - ((double)ppt / 100.0);
+        LOG("new_current_percent = %f\n", new_current_percent);
+        LOG("new_other_percent = %f\n", new_other_percent);
+        /* Ensure that the new percentages are positive and greater than
+         * 0.05 to have a reasonable minimum size. */
+        if (definitelyGreaterThan(new_current_percent, 0.05, DBL_EPSILON) &&
+            definitelyGreaterThan(new_other_percent, 0.05, DBL_EPSILON)) {
+            current->percent += ((double)ppt / 100.0);
+            other->percent -= ((double)ppt / 100.0);
+            LOG("current->percent after = %f\n", current->percent);
+            LOG("other->percent after = %f\n", other->percent);
+        } else {
+            LOG("Not resizing, already at minimum size\n");
+        }
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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;
+
+    HANDLE_EMPTY_MATCH;
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+        int border_style = current->con->border_style;
+        if (strcmp(border_style_str, "toggle") == 0) {
+            border_style++;
+            border_style %= 3;
+        } else {
+            if (strcmp(border_style_str, "normal") == 0)
+                border_style = BS_NORMAL;
+            else if (strcmp(border_style_str, "none") == 0)
+                border_style = BS_NONE;
+            else if (strcmp(border_style_str, "1pixel") == 0)
+                border_style = BS_1PIXEL;
+            else {
+                ELOG("BUG: called with border_style=%s\n", border_style_str);
+                return sstrdup("{\"sucess\": false}");
+            }
+        }
+        con_set_border_style(current->con, border_style);
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_nop(Match *current_match, char *comment) {
+    MIGRATION_init(x, comment);
+    LOG("-------------------------------------------------\n");
+    LOG("  NOP: %s\n", comment);
+    LOG("-------------------------------------------------\n");
+
+    return NULL;
+}
+
+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();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_workspace(Match *current_match, char *which) {
+    MIGRATION_init(x, which);
+    Con *ws;
+
+    DLOG("which=%s\n", which);
+
+    if (strcmp(which, "next") == 0)
+        ws = workspace_next();
+    else if (strcmp(which, "prev") == 0)
+        ws = workspace_prev();
+    else if (strcmp(which, "next_on_output") == 0)
+        ws = workspace_next_on_output();
+    else if (strcmp(which, "prev_on_output") == 0)
+        ws = workspace_prev_on_output();
+    else {
+        ELOG("BUG: called with which=%s\n", which);
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    workspace_show(ws);
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_workspace_back_and_forth(Match *current_match) {
+    MIGRATION_init(x);
+    workspace_back_and_forth();
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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}");
+    }
+
+    DLOG("should switch to workspace %s\n", name);
+
+    Con *ws = con_get_workspace(focused);
+
+    /* Check if the command wants to switch to the current workspace */
+    if (strcmp(ws->name, name) == 0) {
+        DLOG("This workspace is already focused.\n");
+        if (config.workspace_auto_back_and_forth) {
+            workspace_back_and_forth();
+            tree_render();
+        }
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    workspace_show_by_name(name);
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_mark(Match *current_match, char *mark) {
+    MIGRATION_init(x, mark);
+    DLOG("Clearing all windows which have that mark first\n");
+
+    Con *con;
+    TAILQ_FOREACH(con, &all_cons, all_cons) {
+        if (con->mark && strcmp(con->mark, mark) == 0)
+            FREE(con->mark);
+    }
+
+    DLOG("marking window with str %s\n", mark);
+    owindow *current;
+
+    HANDLE_EMPTY_MATCH;
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+        current->con->mark = sstrdup(mark);
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_mode(Match *current_match, char *mode) {
+    MIGRATION_init(x, mode);
+    DLOG("mode=%s\n", mode);
+    switch_mode(mode);
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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);
+
+    HANDLE_EMPTY_MATCH;
+
+    /* get the output */
+    Output *current_output = NULL;
+    Output *output;
+
+    // TODO: fix the handling of criteria
+    TAILQ_FOREACH(current, &owindows, owindows)
+        current_output = get_output_containing(current->con->rect.x, current->con->rect.y);
+
+    assert(current_output != NULL);
+
+    // TODO: clean this up with commands.spec as soon as we switched away from the lex/yacc command parser
+    if (strcasecmp(name, "up") == 0)
+        output = get_output_next(D_UP, current_output);
+    else if (strcasecmp(name, "down") == 0)
+        output = get_output_next(D_DOWN, current_output);
+    else if (strcasecmp(name, "left") == 0)
+        output = get_output_next(D_LEFT, current_output);
+    else if (strcasecmp(name, "right") == 0)
+        output = get_output_next(D_RIGHT, current_output);
+    else
+        output = get_output_by_name(name);
+
+    if (!output) {
+        LOG("No such output found.\n");
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    /* get visible workspace on output */
+    Con *ws = NULL;
+    GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child));
+    if (!ws)
+        return sstrdup("{\"sucess\": false}");
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+        con_move_to_workspace(current->con, ws, true, false);
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_floating(Match *current_match, char *floating_mode) {
+    MIGRATION_init(x, floating_mode);
+    owindow *current;
+
+    DLOG("floating_mode=%s\n", floating_mode);
+
+    HANDLE_EMPTY_MATCH;
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+        if (strcmp(floating_mode, "toggle") == 0) {
+            DLOG("should toggle mode\n");
+            toggle_floating_mode(current->con, false);
+        } else {
+            DLOG("should switch mode to %s\n", floating_mode);
+            if (strcmp(floating_mode, "enable") == 0) {
+                floating_enable(current->con, false);
+            } else {
+                floating_disable(current->con, false);
+            }
+        }
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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;
+
+    owindow *current;
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        Output *current_output = get_output_containing(current->con->rect.x,
+                                                       current->con->rect.y);
+        Output *output = get_output_from_string(current_output, name);
+        if (!output) {
+            LOG("No such output\n");
+            return sstrdup("{\"sucess\": false}");
+        }
+
+        Con *content = output_get_content(output->con);
+        LOG("got output %p with content %p\n", output, content);
+
+        Con *ws = con_get_workspace(current->con);
+        LOG("should move workspace %p / %s\n", ws, ws->name);
+        if (con_num_children(ws->parent) == 1) {
+            LOG("Not moving workspace \"%s\", it is the only workspace on its output.\n", ws->name);
+            continue;
+        }
+        bool workspace_was_visible = workspace_is_visible(ws);
+        Con *old_content = ws->parent;
+        con_detach(ws);
+        if (workspace_was_visible) {
+            /* The workspace which we just detached was visible, so focus
+             * the next one in the focus-stack. */
+            Con *focus_ws = TAILQ_FIRST(&(old_content->focus_head));
+            LOG("workspace was visible, focusing %p / %s now\n", focus_ws, focus_ws->name);
+            workspace_show(focus_ws);
+        }
+        con_attach(ws, content, false);
+        ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"move\"}");
+        if (workspace_was_visible) {
+            /* Focus the moved workspace on the destination output. */
+            workspace_show(ws);
+        }
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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));
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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 (strcmp(kill_mode_str, "window") == 0)
+        kill_mode = KILL_WINDOW;
+    else if (strcmp(kill_mode_str, "client") == 0)
+        kill_mode = KILL_CLIENT;
+    else {
+        ELOG("BUG: called with kill_mode=%s\n", kill_mode_str);
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    /* check if the match is empty, not if the result is empty */
+    if (match_is_empty(current_match))
+        tree_close_con(kill_mode);
+    else {
+        TAILQ_FOREACH(current, &owindows, owindows) {
+            DLOG("matching: %p / %s\n", current->con, current->con->name);
+            tree_close(current->con, kill_mode, false, false);
+        }
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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);
+    start_application(command, no_startup_id);
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_focus_direction(Match *current_match, char *direction) {
+    MIGRATION_init(x, direction);
+    if (focused &&
+        focused->type != CT_WORKSPACE &&
+        focused->fullscreen_mode != CF_NONE) {
+        LOG("Cannot change focus while in fullscreen mode.\n");
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    DLOG("direction = *%s*\n", direction);
+
+    if (strcmp(direction, "left") == 0)
+        tree_next('p', HORIZ);
+    else if (strcmp(direction, "right") == 0)
+        tree_next('n', HORIZ);
+    else if (strcmp(direction, "up") == 0)
+        tree_next('p', VERT);
+    else if (strcmp(direction, "down") == 0)
+        tree_next('n', VERT);
+    else {
+        ELOG("Invalid focus direction (%s)\n", direction);
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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) {
+        LOG("Cannot change focus while in fullscreen mode.\n");
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    DLOG("window_mode = %s\n", window_mode);
+
+    Con *ws = con_get_workspace(focused);
+    Con *current;
+    if (ws != NULL) {
+        if (strcmp(window_mode, "mode_toggle") == 0) {
+            current = TAILQ_FIRST(&(ws->focus_head));
+            if (current != NULL && current->type == CT_FLOATING_CON)
+                window_mode = "tiling";
+            else window_mode = "floating";
+        }
+        TAILQ_FOREACH(current, &(ws->focus_head), focused) {
+            if ((strcmp(window_mode, "floating") == 0 && current->type != CT_FLOATING_CON) ||
+                (strcmp(window_mode, "tiling") == 0 && current->type == CT_FLOATING_CON))
+                continue;
+
+            con_focus(con_descend_focused(current));
+            break;
+        }
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_focus_level(Match *current_match, char *level) {
+    MIGRATION_init(x, level);
+    if (focused &&
+        focused->type != CT_WORKSPACE &&
+        focused->fullscreen_mode != CF_NONE) {
+        LOG("Cannot change focus while in fullscreen mode.\n");
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    DLOG("level = %s\n", level);
+
+    if (strcmp(level, "parent") == 0)
+        level_up();
+    else level_down();
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_focus(Match *current_match) {
+    MIGRATION_init(x);
+    DLOG("current_match = %p\n", current_match);
+    if (focused &&
+        focused->type != CT_WORKSPACE &&
+        focused->fullscreen_mode != CF_NONE) {
+        LOG("Cannot change focus while in fullscreen mode.\n");
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    owindow *current;
+
+    if (match_is_empty(current_match)) {
+        ELOG("You have to specify which window/container should be focused.\n");
+        ELOG("Example: [class=\"urxvt\" title=\"irssi\"] focus\n");
+
+        char *json_output;
+        sasprintf(&json_output, "{\"success\":false, \"error\":\"You have to "
+                  "specify which window/container should be focused\"}");
+        return json_output;
+    }
+
+    int count = 0;
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        Con *ws = con_get_workspace(current->con);
+        /* If no workspace could be found, this was a dock window.
+         * Just skip it, you cannot focus dock windows. */
+        if (!ws)
+            continue;
+
+        /* If the container is not on the current workspace,
+         * workspace_show() will switch to a different workspace and (if
+         * enabled) trigger a mouse pointer warp to the currently focused
+         * container (!) on the target workspace.
+         *
+         * Therefore, before calling workspace_show(), we make sure that
+         * 'current' will be focused on the workspace. However, we cannot
+         * just con_focus(current) because then the pointer will not be
+         * warped at all (the code thinks we are already there).
+         *
+         * So we focus 'current' to make it the currently focused window of
+         * the target workspace, then revert focus. */
+        Con *currently_focused = focused;
+        con_focus(current->con);
+        con_focus(currently_focused);
+
+        /* Now switch to the workspace, then focus */
+        workspace_show(ws);
+        LOG("focusing %p / %s\n", current->con, current->con->name);
+        con_focus(current->con);
+        count++;
+    }
+
+    if (count > 1)
+        LOG("WARNING: Your criteria for the focus command matches %d containers, "
+            "while only exactly one container can be focused at a time.\n", count);
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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;
+
+    HANDLE_EMPTY_MATCH;
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        printf("matching: %p / %s\n", current->con, current->con->name);
+        con_toggle_fullscreen(current->con, (strcmp(fullscreen_mode, "global") == 0 ? CF_GLOBAL : CF_OUTPUT));
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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);
+
+    /* TODO: make 'move' work with criteria. */
+    DLOG("moving in direction %s, px %s\n", direction, move_px);
+    if (con_is_floating(focused)) {
+        DLOG("floating move with %d pixels\n", px);
+        Rect newrect = focused->parent->rect;
+        if (strcmp(direction, "left") == 0) {
+            newrect.x -= px;
+        } else if (strcmp(direction, "right") == 0) {
+            newrect.x += px;
+        } else if (strcmp(direction, "up") == 0) {
+            newrect.y -= px;
+        } else if (strcmp(direction, "down") == 0) {
+            newrect.y += px;
+        }
+        floating_reposition(focused->parent, newrect);
+    } else {
+        tree_move((strcmp(direction, "right") == 0 ? TOK_RIGHT :
+                   (strcmp(direction, "left") == 0 ? TOK_LEFT :
+                    (strcmp(direction, "up") == 0 ? TOK_UP :
+                     TOK_DOWN))));
+        tree_render();
+    }
+
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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 ? L_STACKED :
+                   L_TABBED));
+
+    /* check if the match is empty, not if the result is empty */
+    if (match_is_empty(current_match))
+        con_set_layout(focused->parent, layout);
+    else {
+        TAILQ_FOREACH(current, &owindows, owindows) {
+            DLOG("matching: %p / %s\n", current->con, current->con->name);
+            con_set_layout(current->con, layout);
+        }
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_exit(Match *current_match) {
+    MIGRATION_init(x);
+    LOG("Exiting due to user command.\n");
+    exit(0);
+
+    /* unreached */
+}
+
+char *cmd_reload(Match *current_match) {
+    MIGRATION_init(x);
+    LOG("reloading\n");
+    kill_configerror_nagbar(false);
+    load_configuration(conn, NULL, true);
+    x_set_i3_atoms();
+    /* Send an IPC event just in case the ws names have changed */
+    ipc_send_event("workspace", I3_IPC_EVENT_WORKSPACE, "{\"change\":\"reload\"}");
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_restart(Match *current_match) {
+    MIGRATION_init(x);
+    LOG("restarting i3\n");
+    i3_restart(false);
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+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 *json_output;
+    sasprintf(&json_output, "{\"success\":true, \"id\":%ld}", (long int)con);
+
+    tree_render();
+
+    return json_output;
+}
+
+char *cmd_focus_output(Match *current_match, char *name) {
+    MIGRATION_init(x, name);
+    owindow *current;
+
+    DLOG("name = %s\n", name);
+
+    HANDLE_EMPTY_MATCH;
+
+    /* get the output */
+    Output *current_output = NULL;
+    Output *output;
+
+    TAILQ_FOREACH(current, &owindows, owindows)
+        current_output = get_output_containing(current->con->rect.x, current->con->rect.y);
+    assert(current_output != NULL);
+
+    output = get_output_from_string(current_output, name);
+
+    if (!output) {
+        LOG("No such output found.\n");
+        return sstrdup("{\"sucess\": false}");
+    }
+
+    /* get visible workspace on output */
+    Con *ws = NULL;
+    GREP_FIRST(ws, output_get_content(output->con), workspace_is_visible(child));
+    if (!ws)
+        return sstrdup("{\"sucess\": false}");
+
+    workspace_show(ws);
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_move_scratchpad(Match *current_match) {
+    MIGRATION_init(x);
+    DLOG("should move window to scratchpad\n");
+    owindow *current;
+
+    HANDLE_EMPTY_MATCH;
+
+    TAILQ_FOREACH(current, &owindows, owindows) {
+        DLOG("matching: %p / %s\n", current->con, current->con->name);
+        scratchpad_move(current->con);
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
+
+char *cmd_scratchpad_show(Match *current_match) {
+    MIGRATION_init(x);
+    DLOG("should show scratchpad window\n");
+    owindow *current;
+
+    if (match_is_empty(current_match)) {
+        scratchpad_show(NULL);
+    } else {
+        TAILQ_FOREACH(current, &owindows, owindows) {
+            DLOG("matching: %p / %s\n", current->con, current->con->name);
+            scratchpad_show(current->con);
+        }
+    }
+
+    tree_render();
+
+    // XXX: default reply for now, make this a better reply
+    return sstrdup("{\"success\": true}");
+}
diff --git a/src/commands_parser.c b/src/commands_parser.c
new file mode 100644 (file)
index 0000000..0413052
--- /dev/null
@@ -0,0 +1,397 @@
+/*
+ * 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(&current_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(&current_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
index a078a9e842f5f4454dc71ecd7ac8864c611e515c..ae989f065d0727eff7ede111aa971616312d0bc1 100644 (file)
@@ -195,7 +195,7 @@ cmd 'move left 20 px';
 
 ($absolute, $top) = $floatwin->rect;
 
-is($absolute->x, ($absolute_before->x - 20), 'moved 10 px to the left');
+is($absolute->x, ($absolute_before->x - 20), 'moved 20 px to the left');
 is($absolute->y, $absolute_before->y, 'y not changed');
 is($absolute->width, $absolute_before->width, 'width not changed');
 is($absolute->height, $absolute_before->height, 'height not changed');
diff --git a/testcases/t/187-commands-parser.t b/testcases/t/187-commands-parser.t
new file mode 100644 (file)
index 0000000..35eaef7
--- /dev/null
@@ -0,0 +1,149 @@
+#!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;