Codebase list i3-gaps / master i3-migrate-config-to-v4
master

Tree @master (Download .tar.gz)

i3-migrate-config-to-v4 @masterraw · history · blame

#!/usr/bin/env perl
# vim:ts=4:sw=4:expandtab
#
# script to migrate an old config file (i3 < 4.0) to the new format (>= 4.0)
# this script only uses modules which come with perl 5.10
#
# reads an i3 v3 config from stdin and spits out a v4 config on stdout
# exit codes:
#     0 = the input was a v3 config
#     1 = the input was already a v4 config
#         (the config is printed to stdout nevertheless)
#
# © 2011 Michael Stapelberg and contributors, see LICENSE

use strict;
use warnings;
use Getopt::Long;
use v5.10;

# is this a version 3 config file? disables auto-detection
my $v3 = 0;
my $result = GetOptions('v3' => \$v3);

# reads stdin
sub slurp {
    local $/;
    <>;
}

my @unchanged = qw(
    font
    set
    mode
    exec
    assign
    floating_modifier
    focus_follows_mouse
    ipc-socket
    ipc_socket
    client.focused
    client.focused_inactive
    client.unfocused
    client.urgent
    client.background
);

my %workspace_names;
my $workspace_bar = 1;

my $input = slurp();
my @lines = split /\n/, $input;

# remove whitespaces in the beginning of lines
@lines = map { s/^[ \t]*//g; $_ } @lines;

# Try to auto-detect if this is a v3 config file.
sub need_to_convert {
    # If the user passed --v3, we need to convert in any case
    return 1 if $v3;

    for my $line (@lines) {
        # only v4 configfiles can use bindcode or workspace_layout
        return 0 if $line =~ /^bindcode/ ||
                    $line =~ /^workspace_layout/ ||
                    $line =~ /^force_focus_wrapping/;

        # have a look at bindings
        next unless $line =~ /^bind/;

        my ($statement, $key, $command) = ($line =~ /([a-zA-Z_-]+)\s+([^\s]+)\s+(.*)/);
        return 0 if $command =~ /^layout/ ||
                    $command =~ /^floating/ ||
                    $command =~ /^workspace/ ||
                    $command =~ /^focus (left|right|up|down)/ ||
                    $command =~ /^border (normal|1pixel|none)/;
    }

    return 1;
}

if (!need_to_convert()) {
    # If this is already a v4 config file, we will spit out the lines
    # and exit with return code 1
    print $input;
    exit 1;
}

# first pass: get workspace names
for my $line (@lines) {
    next if $line =~ /^#/ || $line =~ /^$/;

    my ($statement, $parameters) = ($line =~ /([a-zA-Z._-]+)(.*)/);

    # skip everything but workspace lines
    next unless defined($statement) and $statement eq 'workspace';

    my ($number, $params) = ($parameters =~ /\s+([0-9]+)\s+(.+)/);

    # save workspace name (unless the line is actually a workspace assignment)
    $workspace_names{$number} = $params unless $params =~ /^output/;
}

for my $line (@lines) {
    # directly use comments and empty lines
    if ($line =~ /^#/ || $line =~ /^$/) {
        print "$line\n";
        next;
    }

    my ($statement, $parameters) = ($line =~ /([a-zA-Z._-]+)(.*)/);

    # directly use lines which have not changed between 3.x and 4.x
    if (!defined($statement) || (lc $statement ~~ @unchanged)) {
        print "$line\n";
        next;
    }

    # new_container changed only the statement name to workspace_layout
    if ($statement eq 'new_container') {
        print "workspace_layout$parameters\n";
        next;
    }

    # workspace_bar is gone, you should use i3bar now
    if ($statement eq 'workspace_bar') {
        $workspace_bar = ($parameters =~ /\s+(yes|true|on|enable|active)/);
        print "# XXX: REMOVED workspace_bar line. There is no internal workspace bar in v4.\n";
        next;
    }

    # new_window changed the parameters from bb to none etc.
    if ($statement eq 'new_window') {
        if ($parameters =~ /bb/) {
            print "new_window none\n";
        } elsif ($parameters =~ /bp/) {
            print "new_window 1pixel\n";
        } elsif ($parameters =~ /bn/) {
            print "new_window normal\n";
        } else {
            print "# XXX: Invalid parameter for new_window, not touching line:\n";
            print "$line\n";
        }
        next;
    }

    # bar colors are obsolete, need to be configured in i3bar
    if ($statement =~ /^bar\./) {
        print "# XXX: REMOVED $statement, configure i3bar instead.\n";
        print "# Old line: $line\n";
        next;
    }

    # one form of this is still ok (workspace assignments), the other (named workspaces) isn’t
    if ($statement eq 'workspace') {
        my ($number, $params) = ($parameters =~ /\s+([0-9]+)\s+(.+)/);
        if ($params =~ /^output/) {
            print "$line\n";
            next;
        } else {
            print "# XXX: workspace name will end up in the corresponding bindings.\n";
            next;
        }
    }

    if ($statement eq 'bind' || $statement eq 'bindsym') {
        convert_command($line);
        next;
    }

    print "# XXX: migration script could not handle line: $line\n";
}

#
# Converts a command (after bind/bindsym)
#
sub convert_command {
    my ($line) = @_;

    my @unchanged_cmds = qw(
        exec
        mark
        kill
        restart
        reload
        exit
    );

    my ($statement, $key, $command) = ($line =~ /([a-zA-Z_-]+)\s+([^\s]+)\s+(.*)/);

    # turn 'bind' to 'bindcode'
    $statement = 'bindcode' if $statement eq 'bind';

    # check if it’s one of the unchanged commands
    my ($cmd) = ($command =~ /([a-zA-Z_-]+)/);
    if ($cmd ~~ @unchanged_cmds) {
        print "$statement $key $command\n";
        return;
    }

    # simple replacements
    my @replace = (
        qr/^s/ => 'layout stacking',
        qr/^d/ => 'layout toggle split',
        qr/^T/ => 'layout tabbed',
        qr/^f($|[^go])/ => 'fullscreen',
        qr/^fg/ => 'fullscreen global',
        qr/^t/ => 'floating toggle',
        qr/^h/ => 'focus left',
        qr/^j($|[^u])/ => 'focus down',
        qr/^k/ => 'focus up',
        qr/^l/ => 'focus right',
        qr/^mh/ => 'move left',
        qr/^mj/ => 'move down',
        qr/^mk/ => 'move up',
        qr/^ml/ => 'move right',
        qr/^bn/ => 'border normal',
        qr/^bp/ => 'border 1pixel',
        qr/^bb/ => 'border none',
        qr/^bt/ => 'border toggle',
        qr/^pw/ => 'workspace prev',
        qr/^nw/ => 'workspace next',
    );

    my $replaced = 0;
    for (my $c = 0; $c < @replace; $c += 2) {
        if ($command =~ $replace[$c]) {
            $command = $replace[$c+1];
            $replaced = 1;
            last;
        }
    }

    # goto command is now obsolete due to criteria + focus command
    if ($command =~ /^goto/) {
        my ($mark) = ($command =~ /^goto\s+(.*)/);
        print qq|$statement $key [con_mark="$mark"] focus\n|;
        return;
    }

    # the jump command is also obsolete due to criteria + focus
    if ($command =~ /^jump/) {
        my ($params) = ($command =~ /^jump\s+(.*)/);
        if ($params =~ /^"/) {
            # jump ["]window class[/window title]["]
            ($params) = ($params =~ /^"([^"]+)"/);

            # check if a window title was specified
            if ($params =~ m,/,) {
                my ($class, $title) = ($params =~ m,([^/]+)/(.+),);
                print qq|$statement $key [class="$class" title="$title"] focus\n|;
            } else {
                print qq|$statement $key [class="$params"] focus\n|;
            }
            return;
        } else {
            # jump <workspace> [ column row ]
            print "# XXX: jump workspace is obsolete in 4.x: $line\n";
            return;
        }
    }

    if (!$replaced && $command =~ /^focus/) {
        my ($what) = ($command =~ /^focus\s+(.*)/);
        $what =~ s/\s//g;
        if ($what eq 'ft') {
            $what = 'mode_toggle';
        } elsif ($what eq 'floating' || $what eq 'tiling') {
            # those are unchanged
        } else {
            print "# XXX: focus <number> is obsolete in 4.x: $line\n";
            return;
        }
        print qq|$statement $key focus $what\n|;
        return;
    }

    if ($command =~ /^mode/) {
        my ($parameters) = ($command =~ /^mode\s+(.*)/);
        print qq|$statement $key mode "$parameters"\n|;
        return;
    }

    # the parameters of the resize command have changed
    if ($command =~ /^resize/) {
        # OLD: resize <left|right|top|bottom> [+|-]<pixels>\n")
        # NEW: resize <grow|shrink> <direction> [<px> px] [or <ppt> ppt]
        my ($direction, $sign, $px) = ($command =~ /^resize\s+(left|right|top|bottom)\s+([+-])([0-9]+)/);
        my $cmd = 'resize ';
        $cmd .= ($sign eq '+' ? 'grow' : 'shrink') . ' ';
        if ($direction eq 'top') {
            $direction = 'up';
        } elsif ($direction eq 'bottom') {
            $direction = 'down';
        }
        $cmd .= "$direction ";
        $cmd .= "$px px";
        print qq|$statement $key $cmd\n|;
        return;
    }

    # switch workspace
    if ($command =~ /^[0-9]+/) {
        my ($number) = ($command =~ /^([0-9]+)/);
        if (exists $workspace_names{$number}) {
            print qq|$statement $key workspace $workspace_names{$number}\n|;
            return;
        } else {
            print qq|$statement $key workspace $number\n|;
            return;
        }
    }

    # move to workspace
    if ($command =~ /^m[0-9]+/) {
        my ($number) = ($command =~ /^m([0-9]+)/);
        if (exists $workspace_names{$number}) {
            print qq|$statement $key move container to workspace $workspace_names{$number}\n|;
            return;
        } else {
            print qq|$statement $key move container to workspace $number\n|;
            return;
        }
    }

    # With Container-commands are now obsolete due to different abstraction
    if ($command =~ /^wc/) {
        $command =~ s/^wc//g;
        my $wc_replaced = 0;
        for (my $c = 0; $c < @replace; $c += 2) {
            if ($command =~ $replace[$c]) {
                $command = $replace[$c+1];
                $wc_replaced = 1;
                last;
            }
        }
        if (!$wc_replaced) {
            print "# XXX: migration script could not handle command: $line\n";
        } else {
            # NOTE: This is not 100% accurate, as it only works for one level
            # of nested containers. As this is a common use case, we use 'focus
            # parent; $command' nevertheless. For advanced use cases, the user
            # has to modify their config.
            print "$statement $key focus parent; $command\n";
        }
        return;
    }

    if ($replaced) {
        print "$statement $key $command\n";
    } else {
        print "# XXX: migration script could not handle command: $line\n";
    }
}


# add an i3bar invocation automatically if no 'workspace_bar no' was found
if ($workspace_bar) {
    print "\n";
    print "# XXX: Automatically added a bar configuration\n";
    print "bar {\n";
    print "    status_command i3status\n";
    print "}\n";
}