]> git.sur5r.net Git - i3/i3/blob - i3-save-tree
Merge branch 'master' into next
[i3/i3] / i3-save-tree
1 #!/usr/bin/env perl
2 # vim:ts=4:sw=4:expandtab
3 #
4 # © 2013-2014 Michael Stapelberg
5 #
6 # Requires perl ≥ v5.10, AnyEvent::I3 and JSON::XS
7
8 use strict;
9 use warnings qw(FATAL utf8);
10 use Data::Dumper;
11 use IPC::Open2;
12 use POSIX qw(locale_h);
13 use File::Find;
14 use File::Basename qw(basename);
15 use File::Temp qw(tempfile);
16 use Getopt::Long;
17 use Pod::Usage;
18 use AnyEvent::I3;
19 use JSON::XS;
20 use List::Util qw(first);
21 use v5.10;
22 use utf8;
23 use open ':encoding(UTF-8)';
24
25 binmode STDOUT, ':utf8';
26 binmode STDERR, ':utf8';
27
28 my $workspace;
29 my $output;
30 my $result = GetOptions(
31     'workspace=s' => \$workspace,
32     'output=s' => \$output,
33     'version' => sub {
34         say "i3-save-tree 0.1 © 2013 Michael Stapelberg";
35         exit 0;
36     },
37     'help' => sub {
38         pod2usage(-exitval => 0);
39     });
40
41 die "Could not parse command line options" unless $result;
42
43 if (!defined($workspace) && !defined($output)) {
44     die "One of --workspace or --output need to be specified";
45 }
46
47 unless (defined($workspace) ^ defined($output)) {
48     die "Only one of --workspace or --output can be specified";
49 }
50
51 my $i3 = i3();
52 if (!$i3->connect->recv) {
53     die "Could not connect to i3";
54 }
55
56 sub filter_containers {
57     my ($tree, $pred) = @_;
58
59     $_ = $tree;
60     return $tree if $pred->();
61
62     for my $child (@{$tree->{nodes}}, @{$tree->{floating_nodes}}) {
63         my $result = filter_containers($child, $pred);
64         return $result if defined($result);
65     }
66
67     return undef;
68 }
69
70 sub leaf_node {
71     my ($tree) = @_;
72
73     return $tree->{type} eq 'con' &&
74            @{$tree->{nodes}} == 0 &&
75            @{$tree->{floating_nodes}} == 0;
76 }
77
78 my %allowed_keys = map { ($_, 1) } qw(
79     type
80     fullscreen_mode
81     layout
82     border
83     current_border_width
84     floating
85     percent
86     nodes
87     floating_nodes
88     name
89     geometry
90     window_properties
91 );
92
93 sub strip_containers {
94     my ($tree) = @_;
95
96     # layout is not relevant for a leaf container
97     delete $tree->{layout} if leaf_node($tree);
98
99     # fullscreen_mode conveys no state at all, it can either be 0 or 1 and the
100     # default is _always_ 0, so skip noop entries.
101     delete $tree->{fullscreen_mode} if $tree->{fullscreen_mode} == 0;
102
103     # names for non-leafs are auto-generated and useful only for i3 debugging
104     delete $tree->{name} unless leaf_node($tree);
105
106     delete $tree->{geometry} if zero_rect($tree->{geometry});
107
108     delete $tree->{current_border_width} if $tree->{current_border_width} == -1;
109
110     for my $key (keys %$tree) {
111         next if exists($allowed_keys{$key});
112
113         delete $tree->{$key};
114     }
115
116     for my $key (qw(nodes floating_nodes)) {
117         $tree->{$key} = [ map { strip_containers($_) } @{$tree->{$key}} ];
118     }
119
120     return $tree;
121 }
122
123 my $json_xs = JSON::XS->new->pretty(1)->allow_nonref->space_before(0)->canonical(1);
124
125 sub zero_rect {
126     my ($rect) = @_;
127     return $rect->{x} == 0 &&
128            $rect->{y} == 0 &&
129            $rect->{width} == 0 &&
130            $rect->{height} == 0;
131 }
132
133 # Dumps the containers in JSON, but with comments to explain the user what she
134 # needs to fix.
135 sub dump_containers {
136     my ($tree, $ws, $last) = @_;
137
138     $ws //= "";
139
140     say $ws . '{';
141
142     $ws .= (' ' x 4);
143
144     if (!leaf_node($tree)) {
145         my $desc = $tree->{layout} . ' split container';
146         if ($tree->{type} ne 'con') {
147             $desc = $tree->{type};
148         }
149         say "$ws// $desc with " . @{$tree->{nodes}} . " children";
150     }
151
152     # Turn “window_properties” into “swallows” expressions, but only for leaf
153     # nodes. It only makes sense for leaf nodes to swallow anything.
154     if (leaf_node($tree)) {
155         my $swallows = {};
156         for my $property (keys %{$tree->{window_properties}}) {
157             $swallows->{$property} = '^' . quotemeta($tree->{window_properties}->{$property}) . '$';
158         }
159         $tree->{swallows} = [ $swallows ];
160     }
161     delete $tree->{window_properties};
162
163     my @keys = sort keys %$tree;
164     for (0 .. (@keys-1)) {
165         my $key = $keys[$_];
166         # Those are handled recursively, not printed.
167         next if $key eq 'nodes' || $key eq 'floating_nodes';
168
169         # JSON::XS’s encode appends a newline
170         chomp(my $val = $json_xs->encode($tree->{$key}));
171
172         # Fix indentation. Keep in mind we are producing output to be
173         # read/modified by a human.
174         $val =~ s/^/$ws/mg;
175         $val =~ s/^\s+//;
176
177         # Comment out all swallows criteria, they are just suggestions.
178         if ($key eq 'swallows') {
179             $val =~ s,^(\s*)\s{3}",\1// ",gm;
180         }
181
182         # Append a comma unless this is the last value.
183         # Ugly, but necessary so that we can print all values before recursing.
184         my $comma = ($_ == (@keys-1) &&
185                      @{$tree->{nodes}} == 0 &&
186                      @{$tree->{floating_nodes}} == 0 ? '' : ',');
187         say qq#$ws"$key": $val$comma#;
188     }
189
190     for my $key (qw(nodes floating_nodes)) {
191         my $num = scalar @{$tree->{$key}};
192         next if !$num;
193
194         say qq#$ws"$key": [#;
195         for (0 .. ($num-1)) {
196             dump_containers(
197                 $tree->{$key}->[$_],
198                 $ws . (' ' x 4),
199                 ($_ == ($num-1)));
200         }
201         say qq#$ws]#;
202     }
203
204     $ws =~ s/\s{4}$//;
205
206     say $ws . ($last ? '}' : '},');
207 }
208
209 my $tree = $i3->get_tree->recv;
210
211 my $dump;
212 if (defined($workspace)) {
213     $dump = filter_containers($tree, sub {
214         $_->{type} eq 'workspace' && $_->{name} eq $workspace
215     });
216 } else {
217     $dump = filter_containers($tree, sub {
218         $_->{type} eq 'output' && $_->{name} eq $output
219     });
220     # Get the output’s content container (living beneath dockarea containers).
221     $dump = first { $_->{type} eq 'con' } @{$dump->{nodes}};
222 }
223
224 $dump = strip_containers($dump);
225
226 say "// vim:ts=4:sw=4:et";
227 for my $key (qw(nodes floating_nodes)) {
228     for (0 .. (@{$dump->{$key}} - 1)) {
229         dump_containers($dump->{$key}->[$_], undef, 1);
230         # Newlines separate containers so that one can use { and } in vim to
231         # jump out of the current container.
232         say '';
233     }
234 }
235
236 =encoding utf-8
237
238 =head1 NAME
239
240     i3-save-tree - save (parts of) the layout tree for restoring
241
242 =head1 SYNOPSIS
243
244     i3-save-tree [--workspace=name] [--output=name]
245
246 =head1 DESCRIPTION
247
248 Dumps a workspace (or an entire output) to stdout. The data is supposed to be
249 edited a bit by a human, then later fed to i3 via the append_layout command.
250
251 The append_layout command will create placeholder windows, arranged in the
252 layout the input file specifies. Each container should have a swallows
253 specification. When a window is mapped (made visible on the screen) that
254 matches the specification, i3 will put it into that place and kill the
255 placeholder.
256
257 =head1 OPTIONS
258
259 =over
260
261 =item B<--workspace=name>
262
263 Specifies the workspace that should be dumped, e.g. 1. Either this or --output
264 need to be specified.
265
266 =item B<--output=name>
267
268 Specifies the output that should be dumped, e.g. LVDS-1. Either this or
269 --workspace need to be specified.
270
271 =back
272
273 =head1 VERSION
274
275 Version 0.1
276
277 =head1 AUTHOR
278
279 Michael Stapelberg, C<< <michael at i3wm.org> >>
280
281 =head1 LICENSE AND COPYRIGHT
282
283 Copyright 2013 Michael Stapelberg.
284
285 This program is free software; you can redistribute it and/or modify it
286 under the terms of the BSD license.
287
288 =cut