]> git.sur5r.net Git - i3/i3/blob - i3-save-tree
Merge pull request #1669 from Airblader/feature-docs-2
[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 );
103
104 sub strip_containers {
105     my ($tree) = @_;
106
107     # layout is not relevant for a leaf container
108     delete $tree->{layout} if leaf_node($tree);
109
110     # fullscreen_mode conveys no state at all, it can either be 0 or 1 and the
111     # default is _always_ 0, so skip noop entries.
112     delete $tree->{fullscreen_mode} if $tree->{fullscreen_mode} == 0;
113
114     # names for non-leafs are auto-generated and useful only for i3 debugging
115     delete $tree->{name} unless leaf_node($tree);
116
117     delete $tree->{geometry} if zero_rect($tree->{geometry});
118
119     delete $tree->{current_border_width} if $tree->{current_border_width} == -1;
120
121     for my $key (keys %$tree) {
122         next if exists($allowed_keys{$key});
123
124         delete $tree->{$key};
125     }
126
127     for my $key (qw(nodes floating_nodes)) {
128         $tree->{$key} = [ map { strip_containers($_) } @{$tree->{$key}} ];
129     }
130
131     return $tree;
132 }
133
134 my $json_xs = JSON::XS->new->pretty(1)->allow_nonref->space_before(0)->canonical(1);
135
136 sub zero_rect {
137     my ($rect) = @_;
138     return $rect->{x} == 0 &&
139            $rect->{y} == 0 &&
140            $rect->{width} == 0 &&
141            $rect->{height} == 0;
142 }
143
144 # Dumps the containers in JSON, but with comments to explain the user what she
145 # needs to fix.
146 sub dump_containers {
147     my ($tree, $ws, $last) = @_;
148
149     $ws //= "";
150
151     say $ws . '{';
152
153     $ws .= (' ' x 4);
154
155     if (!leaf_node($tree)) {
156         my $desc = $tree->{layout} . ' split container';
157         if ($tree->{type} ne 'con') {
158             $desc = $tree->{type};
159         }
160         say "$ws// $desc with " . @{$tree->{nodes}} . " children";
161     }
162
163     # Turn “window_properties” into “swallows” expressions, but only for leaf
164     # nodes. It only makes sense for leaf nodes to swallow anything.
165     if (leaf_node($tree)) {
166         my $swallows = {};
167         for my $property (keys %{$tree->{window_properties}}) {
168             $swallows->{$property} = '^' . quotemeta($tree->{window_properties}->{$property}) . '$';
169         }
170         $tree->{swallows} = [ $swallows ];
171     }
172     delete $tree->{window_properties};
173
174     my @keys = sort keys %$tree;
175     for (0 .. (@keys-1)) {
176         my $key = $keys[$_];
177         # Those are handled recursively, not printed.
178         next if $key eq 'nodes' || $key eq 'floating_nodes';
179
180         # JSON::XS’s encode appends a newline
181         chomp(my $val = $json_xs->encode($tree->{$key}));
182
183         # Fix indentation. Keep in mind we are producing output to be
184         # read/modified by a human.
185         $val =~ s/^/$ws/mg;
186         $val =~ s/^\s+//;
187
188         # Comment out all swallows criteria, they are just suggestions.
189         if ($key eq 'swallows') {
190             $val =~ s,^(\s*)\s{3}",\1// ",gm;
191         }
192
193         # Append a comma unless this is the last value.
194         # Ugly, but necessary so that we can print all values before recursing.
195         my $comma = ($_ == (@keys-1) &&
196                      @{$tree->{nodes}} == 0 &&
197                      @{$tree->{floating_nodes}} == 0 ? '' : ',');
198         say qq#$ws"$key": $val$comma#;
199     }
200
201     for my $key (qw(nodes floating_nodes)) {
202         my $num = scalar @{$tree->{$key}};
203         next if !$num;
204
205         say qq#$ws"$key": [#;
206         for (0 .. ($num-1)) {
207             dump_containers(
208                 $tree->{$key}->[$_],
209                 $ws . (' ' x 4),
210                 ($_ == ($num-1)));
211         }
212         say qq#$ws]#;
213     }
214
215     $ws =~ s/\s{4}$//;
216
217     say $ws . ($last ? '}' : '},');
218 }
219
220 my $tree = $i3->get_tree->recv;
221
222 my $dump;
223 if (defined($workspace)) {
224     $dump = filter_containers($tree, sub {
225         $_->{type} eq 'workspace' && ($_->{name} eq $workspace || ($workspace =~ /^\d+$/ && $_->{num} eq $workspace))
226     });
227 } else {
228     $dump = filter_containers($tree, sub {
229         $_->{type} eq 'output' && $_->{name} eq $output
230     });
231     # Get the output’s content container (living beneath dockarea containers).
232     $dump = first { $_->{type} eq 'con' } @{$dump->{nodes}};
233 }
234
235 $dump = strip_containers($dump);
236
237 say "// vim:ts=4:sw=4:et";
238 for my $key (qw(nodes floating_nodes)) {
239     for (0 .. (@{$dump->{$key}} - 1)) {
240         dump_containers($dump->{$key}->[$_], undef, 1);
241         # Newlines separate containers so that one can use { and } in vim to
242         # jump out of the current container.
243         say '';
244     }
245 }
246
247 =encoding utf-8
248
249 =head1 NAME
250
251     i3-save-tree - save (parts of) the layout tree for restoring
252
253 =head1 SYNOPSIS
254
255     i3-save-tree [--workspace=name|number] [--output=name]
256
257 =head1 DESCRIPTION
258
259 Dumps a workspace (or an entire output) to stdout. The data is supposed to be
260 edited a bit by a human, then later fed to i3 via the append_layout command.
261
262 The append_layout command will create placeholder windows, arranged in the
263 layout the input file specifies. Each container should have a swallows
264 specification. When a window is mapped (made visible on the screen) that
265 matches the specification, i3 will put it into that place and kill the
266 placeholder.
267
268 If neither argument is specified, the currently focused workspace will be used.
269
270 =head1 OPTIONS
271
272 =over
273
274 =item B<--workspace=name|number>
275
276 Specifies the workspace that should be dumped, e.g. 1. This can either be a
277 name or the number of a workspace.
278
279 =item B<--output=name>
280
281 Specifies the output that should be dumped, e.g. LVDS-1.
282
283 =back
284
285 =head1 VERSION
286
287 Version 0.1
288
289 =head1 AUTHOR
290
291 Michael Stapelberg, C<< <michael at i3wm.org> >>
292
293 =head1 LICENSE AND COPYRIGHT
294
295 Copyright 2013 Michael Stapelberg.
296
297 This program is free software; you can redistribute it and/or modify it
298 under the terms of the BSD license.
299
300 =cut