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