2 # vim:ts=4:sw=4:expandtab
4 # © 2013-2014 Michael Stapelberg
6 # Requires perl ≥ v5.10, AnyEvent::I3 and JSON::XS
9 use warnings qw(FATAL utf8);
12 use POSIX qw(locale_h);
14 use File::Basename qw(basename);
15 use File::Temp qw(tempfile);
16 use List::Util qw(first);
21 use List::Util qw(first);
22 use Encode qw(decode);
25 use open ':encoding(UTF-8)';
27 binmode STDOUT, ':utf8';
28 binmode STDERR, ':utf8';
32 my $result = GetOptions(
33 'workspace=s' => \$workspace,
34 'output=s' => \$output,
36 say "i3-save-tree 0.1 © 2013 Michael Stapelberg";
40 pod2usage(-exitval => 0);
43 die "Could not parse command line options" unless $result;
45 if (defined($workspace) && defined($output)) {
46 die "Only one of --workspace or --output can be specified";
49 $workspace = decode('utf-8', $workspace);
50 $output = decode('utf-8', $output);
53 if (!$i3->connect->recv) {
54 die "Could not connect to i3";
57 sub get_current_workspace {
58 my $current = first { $_->{focused} } @{$i3->get_workspaces->recv};
59 return $current->{name};
62 if (!defined($workspace) && !defined($output)) {
63 $workspace = get_current_workspace();
66 sub filter_containers {
67 my ($tree, $pred) = @_;
70 return $tree if $pred->();
72 for my $child (@{$tree->{nodes}}, @{$tree->{floating_nodes}}) {
73 my $result = filter_containers($child, $pred);
74 return $result if defined($result);
83 return $tree->{type} eq 'con' &&
84 @{$tree->{nodes}} == 0 &&
85 @{$tree->{floating_nodes}} == 0;
88 my %allowed_keys = map { ($_, 1) } qw(
104 sub strip_containers {
107 # layout is not relevant for a leaf container
108 delete $tree->{layout} if leaf_node($tree);
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;
114 # names for non-leafs are auto-generated and useful only for i3 debugging
115 delete $tree->{name} unless leaf_node($tree);
117 delete $tree->{geometry} if zero_rect($tree->{geometry});
119 delete $tree->{current_border_width} if $tree->{current_border_width} == -1;
121 for my $key (keys %$tree) {
122 next if exists($allowed_keys{$key});
124 delete $tree->{$key};
127 for my $key (qw(nodes floating_nodes)) {
128 $tree->{$key} = [ map { strip_containers($_) } @{$tree->{$key}} ];
134 my $json_xs = JSON::XS->new->pretty(1)->allow_nonref->space_before(0)->canonical(1);
138 return $rect->{x} == 0 &&
140 $rect->{width} == 0 &&
141 $rect->{height} == 0;
144 # Dumps the containers in JSON, but with comments to explain the user what she
146 sub dump_containers {
147 my ($tree, $ws, $last) = @_;
155 if (!leaf_node($tree)) {
156 my $desc = $tree->{layout} . ' split container';
157 if ($tree->{type} ne 'con') {
158 $desc = $tree->{type};
160 say "$ws// $desc with " . @{$tree->{nodes}} . " children";
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)) {
167 for my $property (keys %{$tree->{window_properties}}) {
168 $swallows->{$property} = '^' . quotemeta($tree->{window_properties}->{$property}) . '$';
170 $tree->{swallows} = [ $swallows ];
172 delete $tree->{window_properties};
174 my @keys = sort keys %$tree;
175 for (0 .. (@keys-1)) {
177 # Those are handled recursively, not printed.
178 next if $key eq 'nodes' || $key eq 'floating_nodes';
180 # JSON::XS’s encode appends a newline
181 chomp(my $val = $json_xs->encode($tree->{$key}));
183 # Fix indentation. Keep in mind we are producing output to be
184 # read/modified by a human.
188 # Comment out all swallows criteria, they are just suggestions.
189 if ($key eq 'swallows') {
190 $val =~ s,^(\s*)\s{3}",\1// ",gm;
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#;
201 for my $key (qw(nodes floating_nodes)) {
202 my $num = scalar @{$tree->{$key}};
205 say qq#$ws"$key": [#;
206 for (0 .. ($num-1)) {
217 say $ws . ($last ? '}' : '},');
220 my $tree = $i3->get_tree->recv;
223 if (defined($workspace)) {
224 $dump = filter_containers($tree, sub {
225 $_->{type} eq 'workspace' && $_->{name} eq $workspace
228 $dump = filter_containers($tree, sub {
229 $_->{type} eq 'output' && $_->{name} eq $output
231 # Get the output’s content container (living beneath dockarea containers).
232 $dump = first { $_->{type} eq 'con' } @{$dump->{nodes}};
235 $dump = strip_containers($dump);
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.
251 i3-save-tree - save (parts of) the layout tree for restoring
255 i3-save-tree [--workspace=name] [--output=name]
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.
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
272 =item B<--workspace=name>
274 Specifies the workspace that should be dumped, e.g. 1. Either this or --output
275 need to be specified.
277 =item B<--output=name>
279 Specifies the output that should be dumped, e.g. LVDS-1. Either this or
280 --workspace need to be specified.
290 Michael Stapelberg, C<< <michael at i3wm.org> >>
292 =head1 LICENSE AND COPYRIGHT
294 Copyright 2013 Michael Stapelberg.
296 This program is free software; you can redistribute it and/or modify it
297 under the terms of the BSD license.