Merge branch 'release-4.16.1'
[i3/i3] / i3-migrate-config-to-v4
1 #!/usr/bin/env perl
2 # vim:ts=4:sw=4:expandtab
3 #
4 # script to migrate an old config file (i3 < 4.0) to the new format (>= 4.0)
5 # this script only uses modules which come with perl 5.10
6 #
7 # reads an i3 v3 config from stdin and spits out a v4 config on stdout
8 # exit codes:
9 #     0 = the input was a v3 config
10 #     1 = the input was already a v4 config
11 #         (the config is printed to stdout nevertheless)
12 #
13 # © 2011 Michael Stapelberg and contributors, see LICENSE
14
15 use strict;
16 use warnings;
17 use Getopt::Long;
18 use v5.10;
19
20 # is this a version 3 config file? disables auto-detection
21 my $v3 = 0;
22 my $result = GetOptions('v3' => \$v3);
23
24 # reads stdin
25 sub slurp {
26     local $/;
27     <>;
28 }
29
30 my @unchanged = qw(
31     font
32     set
33     mode
34     exec
35     assign
36     floating_modifier
37     focus_follows_mouse
38     ipc-socket
39     ipc_socket
40     client.focused
41     client.focused_inactive
42     client.unfocused
43     client.urgent
44     client.background
45 );
46
47 my %workspace_names;
48 my $workspace_bar = 1;
49
50 my $input = slurp();
51 my @lines = split /\n/, $input;
52
53 # remove whitespaces in the beginning of lines
54 @lines = map { s/^[ \t]*//g; $_ } @lines;
55
56 # Try to auto-detect if this is a v3 config file.
57 sub need_to_convert {
58     # If the user passed --v3, we need to convert in any case
59     return 1 if $v3;
60
61     for my $line (@lines) {
62         # only v4 configfiles can use bindcode or workspace_layout
63         return 0 if $line =~ /^bindcode/ ||
64                     $line =~ /^workspace_layout/ ||
65                     $line =~ /^force_focus_wrapping/;
66
67         # have a look at bindings
68         next unless $line =~ /^bind/;
69
70         my ($statement, $key, $command) = ($line =~ /([a-zA-Z_-]+)\s+([^\s]+)\s+(.*)/);
71         return 0 if $command =~ /^layout/ ||
72                     $command =~ /^floating/ ||
73                     $command =~ /^workspace/ ||
74                     $command =~ /^focus (left|right|up|down)/ ||
75                     $command =~ /^border (normal|1pixel|none)/;
76     }
77
78     return 1;
79 }
80
81 if (!need_to_convert()) {
82     # If this is already a v4 config file, we will spit out the lines
83     # and exit with return code 1
84     print $input;
85     exit 1;
86 }
87
88 # first pass: get workspace names
89 for my $line (@lines) {
90     next if $line =~ /^#/ || $line =~ /^$/;
91
92     my ($statement, $parameters) = ($line =~ /([a-zA-Z._-]+)(.*)/);
93
94     # skip everything but workspace lines
95     next unless defined($statement) and $statement eq 'workspace';
96
97     my ($number, $params) = ($parameters =~ /\s+([0-9]+)\s+(.+)/);
98
99     # save workspace name (unless the line is actually a workspace assignment)
100     $workspace_names{$number} = $params unless $params =~ /^output/;
101 }
102
103 for my $line (@lines) {
104     # directly use comments and empty lines
105     if ($line =~ /^#/ || $line =~ /^$/) {
106         print "$line\n";
107         next;
108     }
109
110     my ($statement, $parameters) = ($line =~ /([a-zA-Z._-]+)(.*)/);
111
112     # directly use lines which have not changed between 3.x and 4.x
113     if (!defined($statement) || (lc $statement ~~ @unchanged)) {
114         print "$line\n";
115         next;
116     }
117
118     # new_container changed only the statement name to workspace_layout
119     if ($statement eq 'new_container') {
120         print "workspace_layout$parameters\n";
121         next;
122     }
123
124     # workspace_bar is gone, you should use i3bar now
125     if ($statement eq 'workspace_bar') {
126         $workspace_bar = ($parameters =~ /\s+(yes|true|on|enable|active)/);
127         print "# XXX: REMOVED workspace_bar line. There is no internal workspace bar in v4.\n";
128         next;
129     }
130
131     # new_window changed the parameters from bb to none etc.
132     if ($statement eq 'new_window') {
133         if ($parameters =~ /bb/) {
134             print "new_window none\n";
135         } elsif ($parameters =~ /bp/) {
136             print "new_window 1pixel\n";
137         } elsif ($parameters =~ /bn/) {
138             print "new_window normal\n";
139         } else {
140             print "# XXX: Invalid parameter for new_window, not touching line:\n";
141             print "$line\n";
142         }
143         next;
144     }
145
146     # bar colors are obsolete, need to be configured in i3bar
147     if ($statement =~ /^bar\./) {
148         print "# XXX: REMOVED $statement, configure i3bar instead.\n";
149         print "# Old line: $line\n";
150         next;
151     }
152
153     # one form of this is still ok (workspace assignments), the other (named workspaces) isn’t
154     if ($statement eq 'workspace') {
155         my ($number, $params) = ($parameters =~ /\s+([0-9]+)\s+(.+)/);
156         if ($params =~ /^output/) {
157             print "$line\n";
158             next;
159         } else {
160             print "# XXX: workspace name will end up in the corresponding bindings.\n";
161             next;
162         }
163     }
164
165     if ($statement eq 'bind' || $statement eq 'bindsym') {
166         convert_command($line);
167         next;
168     }
169
170     print "# XXX: migration script could not handle line: $line\n";
171 }
172
173 #
174 # Converts a command (after bind/bindsym)
175 #
176 sub convert_command {
177     my ($line) = @_;
178
179     my @unchanged_cmds = qw(
180         exec
181         mark
182         kill
183         restart
184         reload
185         exit
186     );
187
188     my ($statement, $key, $command) = ($line =~ /([a-zA-Z_-]+)\s+([^\s]+)\s+(.*)/);
189
190     # turn 'bind' to 'bindcode'
191     $statement = 'bindcode' if $statement eq 'bind';
192
193     # check if it’s one of the unchanged commands
194     my ($cmd) = ($command =~ /([a-zA-Z_-]+)/);
195     if ($cmd ~~ @unchanged_cmds) {
196         print "$statement $key $command\n";
197         return;
198     }
199
200     # simple replacements
201     my @replace = (
202         qr/^s/ => 'layout stacking',
203         qr/^d/ => 'layout toggle split',
204         qr/^T/ => 'layout tabbed',
205         qr/^f($|[^go])/ => 'fullscreen',
206         qr/^fg/ => 'fullscreen global',
207         qr/^t/ => 'floating toggle',
208         qr/^h/ => 'focus left',
209         qr/^j($|[^u])/ => 'focus down',
210         qr/^k/ => 'focus up',
211         qr/^l/ => 'focus right',
212         qr/^mh/ => 'move left',
213         qr/^mj/ => 'move down',
214         qr/^mk/ => 'move up',
215         qr/^ml/ => 'move right',
216         qr/^bn/ => 'border normal',
217         qr/^bp/ => 'border 1pixel',
218         qr/^bb/ => 'border none',
219         qr/^bt/ => 'border toggle',
220         qr/^pw/ => 'workspace prev',
221         qr/^nw/ => 'workspace next',
222     );
223
224     my $replaced = 0;
225     for (my $c = 0; $c < @replace; $c += 2) {
226         if ($command =~ $replace[$c]) {
227             $command = $replace[$c+1];
228             $replaced = 1;
229             last;
230         }
231     }
232
233     # goto command is now obsolete due to criteria + focus command
234     if ($command =~ /^goto/) {
235         my ($mark) = ($command =~ /^goto\s+(.*)/);
236         print qq|$statement $key [con_mark="$mark"] focus\n|;
237         return;
238     }
239
240     # the jump command is also obsolete due to criteria + focus
241     if ($command =~ /^jump/) {
242         my ($params) = ($command =~ /^jump\s+(.*)/);
243         if ($params =~ /^"/) {
244             # jump ["]window class[/window title]["]
245             ($params) = ($params =~ /^"([^"]+)"/);
246
247             # check if a window title was specified
248             if ($params =~ m,/,) {
249                 my ($class, $title) = ($params =~ m,([^/]+)/(.+),);
250                 print qq|$statement $key [class="$class" title="$title"] focus\n|;
251             } else {
252                 print qq|$statement $key [class="$params"] focus\n|;
253             }
254             return;
255         } else {
256             # jump <workspace> [ column row ]
257             print "# XXX: jump workspace is obsolete in 4.x: $line\n";
258             return;
259         }
260     }
261
262     if (!$replaced && $command =~ /^focus/) {
263         my ($what) = ($command =~ /^focus\s+(.*)/);
264         $what =~ s/\s//g;
265         if ($what eq 'ft') {
266             $what = 'mode_toggle';
267         } elsif ($what eq 'floating' || $what eq 'tiling') {
268             # those are unchanged
269         } else {
270             print "# XXX: focus <number> is obsolete in 4.x: $line\n";
271             return;
272         }
273         print qq|$statement $key focus $what\n|;
274         return;
275     }
276
277     if ($command =~ /^mode/) {
278         my ($parameters) = ($command =~ /^mode\s+(.*)/);
279         print qq|$statement $key mode "$parameters"\n|;
280         return;
281     }
282
283     # the parameters of the resize command have changed
284     if ($command =~ /^resize/) {
285         # OLD: resize <left|right|top|bottom> [+|-]<pixels>\n")
286         # NEW: resize <grow|shrink> <direction> [<px> px] [or <ppt> ppt]
287         my ($direction, $sign, $px) = ($command =~ /^resize\s+(left|right|top|bottom)\s+([+-])([0-9]+)/);
288         my $cmd = 'resize ';
289         $cmd .= ($sign eq '+' ? 'grow' : 'shrink') . ' ';
290         if ($direction eq 'top') {
291             $direction = 'up';
292         } elsif ($direction eq 'bottom') {
293             $direction = 'down';
294         }
295         $cmd .= "$direction ";
296         $cmd .= "$px px";
297         print qq|$statement $key $cmd\n|;
298         return;
299     }
300
301     # switch workspace
302     if ($command =~ /^[0-9]+/) {
303         my ($number) = ($command =~ /^([0-9]+)/);
304         if (exists $workspace_names{$number}) {
305             print qq|$statement $key workspace $workspace_names{$number}\n|;
306             return;
307         } else {
308             print qq|$statement $key workspace $number\n|;
309             return;
310         }
311     }
312
313     # move to workspace
314     if ($command =~ /^m[0-9]+/) {
315         my ($number) = ($command =~ /^m([0-9]+)/);
316         if (exists $workspace_names{$number}) {
317             print qq|$statement $key move container to workspace $workspace_names{$number}\n|;
318             return;
319         } else {
320             print qq|$statement $key move container to workspace $number\n|;
321             return;
322         }
323     }
324
325     # With Container-commands are now obsolete due to different abstraction
326     if ($command =~ /^wc/) {
327         $command =~ s/^wc//g;
328         my $wc_replaced = 0;
329         for (my $c = 0; $c < @replace; $c += 2) {
330             if ($command =~ $replace[$c]) {
331                 $command = $replace[$c+1];
332                 $wc_replaced = 1;
333                 last;
334             }
335         }
336         if (!$wc_replaced) {
337             print "# XXX: migration script could not handle command: $line\n";
338         } else {
339             # NOTE: This is not 100% accurate, as it only works for one level
340             # of nested containers. As this is a common use case, we use 'focus
341             # parent; $command' nevertheless. For advanced use cases, the user
342             # has to modify their config.
343             print "$statement $key focus parent; $command\n";
344         }
345         return;
346     }
347
348     if ($replaced) {
349         print "$statement $key $command\n";
350     } else {
351         print "# XXX: migration script could not handle command: $line\n";
352     }
353 }
354
355
356 # add an i3bar invocation automatically if no 'workspace_bar no' was found
357 if ($workspace_bar) {
358     print "\n";
359     print "# XXX: Automatically added a bar configuration\n";
360     print "bar {\n";
361     print "    status_command i3status\n";
362     print "}\n";
363 }