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 delete $tree->{$key} unless exists($allowed_keys{$key});
129 for my $key (qw(nodes floating_nodes)) {
130 $tree->{$key} = [ map { strip_containers($_) } @{$tree->{$key}} ];
136 my $json_xs = JSON::XS->new->pretty(1)->allow_nonref->space_before(0)->canonical(1);
140 return $rect->{x} == 0 &&
142 $rect->{width} == 0 &&
143 $rect->{height} == 0;
146 # Dumps the containers in JSON, but with comments to explain the user what she
148 sub dump_containers {
149 my ($tree, $ws, $last) = @_;
157 if (!leaf_node($tree)) {
158 my $desc = $tree->{layout} . ' split container';
159 if ($tree->{type} ne 'con') {
160 $desc = $tree->{type};
162 say "$ws// $desc with " . @{$tree->{nodes}} . " children";
165 # Turn “window_properties” into “swallows” expressions, but only for leaf
166 # nodes. It only makes sense for leaf nodes to swallow anything.
167 if (leaf_node($tree)) {
169 for my $property (keys %{$tree->{window_properties}}) {
170 $swallows->{$property} = '^' . quotemeta($tree->{window_properties}->{$property}) . '$'
171 if $property ne 'transient_for';
173 $tree->{swallows} = [ $swallows ];
175 delete $tree->{window_properties};
177 my @keys = sort keys %$tree;
178 for (0 .. (@keys-1)) {
180 # Those are handled recursively, not printed.
181 next if $key eq 'nodes' || $key eq 'floating_nodes';
183 # JSON::XS’s encode appends a newline
184 chomp(my $val = $json_xs->encode($tree->{$key}));
186 # Fix indentation. Keep in mind we are producing output to be
187 # read/modified by a human.
191 # Comment out all swallows criteria, they are just suggestions.
192 if ($key eq 'swallows') {
193 $val =~ s,^(\s*)\s{3}",\1// ",gm;
196 # Append a comma unless this is the last value.
197 # Ugly, but necessary so that we can print all values before recursing.
198 my $comma = ($_ == (@keys-1) &&
199 @{$tree->{nodes}} == 0 &&
200 @{$tree->{floating_nodes}} == 0 ? '' : ',');
201 say qq#$ws"$key": $val$comma#;
204 for my $key (qw(nodes floating_nodes)) {
205 my $num = scalar @{$tree->{$key}};
208 say qq#$ws"$key": [#;
209 for (0 .. ($num-1)) {
220 say $ws . ($last ? '}' : '},');
223 my $tree = $i3->get_tree->recv;
226 if (defined($workspace)) {
227 $dump = filter_containers($tree, sub {
228 $_->{type} eq 'workspace' && ($_->{name} eq $workspace || ($workspace =~ /^\d+$/ && $_->{num} eq $workspace))
231 $dump = filter_containers($tree, sub {
232 $_->{type} eq 'output' && $_->{name} eq $output
234 # Get the output’s content container (living beneath dockarea containers).
235 $dump = first { $_->{type} eq 'con' } @{$dump->{nodes}};
238 $dump = strip_containers($dump);
240 say "// vim:ts=4:sw=4:et";
241 for my $key (qw(nodes floating_nodes)) {
242 for (0 .. (@{$dump->{$key}} - 1)) {
243 dump_containers($dump->{$key}->[$_], undef, 1);
244 # Newlines separate containers so that one can use { and } in vim to
245 # jump out of the current container.
254 i3-save-tree - save (parts of) the layout tree for restoring
258 i3-save-tree [--workspace=name|number] [--output=name]
262 Dumps a workspace (or an entire output) to stdout. The data is supposed to be
263 edited a bit by a human, then later fed to i3 via the append_layout command.
265 The append_layout command will create placeholder windows, arranged in the
266 layout the input file specifies. Each container should have a swallows
267 specification. When a window is mapped (made visible on the screen) that
268 matches the specification, i3 will put it into that place and kill the
271 If neither argument is specified, the currently focused workspace will be used.
277 =item B<--workspace=name|number>
279 Specifies the workspace that should be dumped, e.g. 1. This can either be a
280 name or the number of a workspace.
282 =item B<--output=name>
284 Specifies the output that should be dumped, e.g. LVDS-1.
294 Michael Stapelberg, C<< <michael at i3wm.org> >>
296 =head1 LICENSE AND COPYRIGHT
298 Copyright 2013 Michael Stapelberg.
300 This program is free software; you can redistribute it and/or modify it
301 under the terms of the BSD license.