]> git.sur5r.net Git - i3/i3/blob - i3-save-tree
Make the --workspace optional by defaulting to the focused workspace.
[i3/i3] / i3-save-tree
1 #!/usr/bin/env perl
2 # vim:ts=4:sw=4:expandtab
3 #
4 # © 2013-2014 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
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] [--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 =head1 OPTIONS
269
270 =over
271
272 =item B<--workspace=name>
273
274 Specifies the workspace that should be dumped, e.g. 1. Either this or --output
275 need to be specified.
276
277 =item B<--output=name>
278
279 Specifies the output that should be dumped, e.g. LVDS-1. Either this or
280 --workspace need to be specified.
281
282 =back
283
284 =head1 VERSION
285
286 Version 0.1
287
288 =head1 AUTHOR
289
290 Michael Stapelberg, C<< <michael at i3wm.org> >>
291
292 =head1 LICENSE AND COPYRIGHT
293
294 Copyright 2013 Michael Stapelberg.
295
296 This program is free software; you can redistribute it and/or modify it
297 under the terms of the BSD license.
298
299 =cut