2 # vim:ts=4:sw=4:expandtab
4 # © 2013 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(
105 sub strip_containers {
108 # layout is not relevant for a leaf container
109 delete $tree->{layout} if leaf_node($tree);
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;
115 # names for non-leafs are auto-generated and useful only for i3 debugging
116 delete $tree->{name} unless leaf_node($tree);
118 delete $tree->{geometry} if zero_rect($tree->{geometry});
120 # Retain the rect for floating containers to keep their positions.
121 delete $tree->{rect} unless $tree->{type} eq 'floating_con';
123 delete $tree->{current_border_width} if $tree->{current_border_width} == -1;
125 for my $key (keys %$tree) {
126 next if exists($allowed_keys{$key});
128 delete $tree->{$key};
131 for my $key (qw(nodes floating_nodes)) {
132 $tree->{$key} = [ map { strip_containers($_) } @{$tree->{$key}} ];
138 my $json_xs = JSON::XS->new->pretty(1)->allow_nonref->space_before(0)->canonical(1);
142 return $rect->{x} == 0 &&
144 $rect->{width} == 0 &&
145 $rect->{height} == 0;
148 # Dumps the containers in JSON, but with comments to explain the user what she
150 sub dump_containers {
151 my ($tree, $ws, $last) = @_;
159 if (!leaf_node($tree)) {
160 my $desc = $tree->{layout} . ' split container';
161 if ($tree->{type} ne 'con') {
162 $desc = $tree->{type};
164 say "$ws// $desc with " . @{$tree->{nodes}} . " children";
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)) {
171 for my $property (keys %{$tree->{window_properties}}) {
172 $swallows->{$property} = '^' . quotemeta($tree->{window_properties}->{$property}) . '$';
174 $tree->{swallows} = [ $swallows ];
176 delete $tree->{window_properties};
178 my @keys = sort keys %$tree;
179 for (0 .. (@keys-1)) {
181 # Those are handled recursively, not printed.
182 next if $key eq 'nodes' || $key eq 'floating_nodes';
184 # JSON::XS’s encode appends a newline
185 chomp(my $val = $json_xs->encode($tree->{$key}));
187 # Fix indentation. Keep in mind we are producing output to be
188 # read/modified by a human.
192 # Comment out all swallows criteria, they are just suggestions.
193 if ($key eq 'swallows') {
194 $val =~ s,^(\s*)\s{3}",\1// ",gm;
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#;
205 for my $key (qw(nodes floating_nodes)) {
206 my $num = scalar @{$tree->{$key}};
209 say qq#$ws"$key": [#;
210 for (0 .. ($num-1)) {
221 say $ws . ($last ? '}' : '},');
224 my $tree = $i3->get_tree->recv;
227 if (defined($workspace)) {
228 $dump = filter_containers($tree, sub {
229 $_->{type} eq 'workspace' && ($_->{name} eq $workspace || ($workspace =~ /^\d+$/ && $_->{num} eq $workspace))
232 $dump = filter_containers($tree, sub {
233 $_->{type} eq 'output' && $_->{name} eq $output
235 # Get the output’s content container (living beneath dockarea containers).
236 $dump = first { $_->{type} eq 'con' } @{$dump->{nodes}};
239 $dump = strip_containers($dump);
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.
255 i3-save-tree - save (parts of) the layout tree for restoring
259 i3-save-tree [--workspace=name|number] [--output=name]
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.
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
272 If neither argument is specified, the currently focused workspace will be used.
278 =item B<--workspace=name|number>
280 Specifies the workspace that should be dumped, e.g. 1. This can either be a
281 name or the number of a workspace.
283 =item B<--output=name>
285 Specifies the output that should be dumped, e.g. LVDS-1.
295 Michael Stapelberg, C<< <michael at i3wm.org> >>
297 =head1 LICENSE AND COPYRIGHT
299 Copyright 2013 Michael Stapelberg.
301 This program is free software; you can redistribute it and/or modify it
302 under the terms of the BSD license.