2013-12-15 15:29:21 -05:00
|
|
|
|
#!/usr/bin/env perl
|
|
|
|
|
# vim:ts=4:sw=4:expandtab
|
|
|
|
|
#
|
2014-01-05 14:10:24 -05:00
|
|
|
|
# © 2013-2014 Michael Stapelberg
|
2013-12-15 15:29:21 -05:00
|
|
|
|
#
|
|
|
|
|
# Requires perl ≥ v5.10, AnyEvent::I3 and JSON::XS
|
|
|
|
|
|
|
|
|
|
use strict;
|
|
|
|
|
use warnings qw(FATAL utf8);
|
|
|
|
|
use Data::Dumper;
|
|
|
|
|
use IPC::Open2;
|
|
|
|
|
use POSIX qw(locale_h);
|
|
|
|
|
use File::Find;
|
|
|
|
|
use File::Basename qw(basename);
|
|
|
|
|
use File::Temp qw(tempfile);
|
2015-03-18 16:37:37 -04:00
|
|
|
|
use List::Util qw(first);
|
2013-12-15 15:29:21 -05:00
|
|
|
|
use Getopt::Long;
|
|
|
|
|
use Pod::Usage;
|
|
|
|
|
use AnyEvent::I3;
|
|
|
|
|
use JSON::XS;
|
|
|
|
|
use List::Util qw(first);
|
2014-06-25 04:00:23 -04:00
|
|
|
|
use Encode qw(decode);
|
2013-12-15 15:29:21 -05:00
|
|
|
|
use v5.10;
|
|
|
|
|
use utf8;
|
|
|
|
|
use open ':encoding(UTF-8)';
|
|
|
|
|
|
|
|
|
|
binmode STDOUT, ':utf8';
|
|
|
|
|
binmode STDERR, ':utf8';
|
|
|
|
|
|
|
|
|
|
my $workspace;
|
|
|
|
|
my $output;
|
|
|
|
|
my $result = GetOptions(
|
|
|
|
|
'workspace=s' => \$workspace,
|
|
|
|
|
'output=s' => \$output,
|
|
|
|
|
'version' => sub {
|
|
|
|
|
say "i3-save-tree 0.1 © 2013 Michael Stapelberg";
|
|
|
|
|
exit 0;
|
|
|
|
|
},
|
|
|
|
|
'help' => sub {
|
|
|
|
|
pod2usage(-exitval => 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
die "Could not parse command line options" unless $result;
|
|
|
|
|
|
2015-03-18 16:37:37 -04:00
|
|
|
|
if (defined($workspace) && defined($output)) {
|
2013-12-15 15:29:21 -05:00
|
|
|
|
die "Only one of --workspace or --output can be specified";
|
|
|
|
|
}
|
|
|
|
|
|
2014-06-25 04:00:23 -04:00
|
|
|
|
$workspace = decode('utf-8', $workspace);
|
|
|
|
|
$output = decode('utf-8', $output);
|
|
|
|
|
|
2013-12-15 15:29:21 -05:00
|
|
|
|
my $i3 = i3();
|
|
|
|
|
if (!$i3->connect->recv) {
|
|
|
|
|
die "Could not connect to i3";
|
|
|
|
|
}
|
|
|
|
|
|
2015-03-18 16:37:37 -04:00
|
|
|
|
sub get_current_workspace {
|
|
|
|
|
my $current = first { $_->{focused} } @{$i3->get_workspaces->recv};
|
|
|
|
|
return $current->{name};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!defined($workspace) && !defined($output)) {
|
|
|
|
|
$workspace = get_current_workspace();
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-15 15:29:21 -05:00
|
|
|
|
sub filter_containers {
|
|
|
|
|
my ($tree, $pred) = @_;
|
|
|
|
|
|
|
|
|
|
$_ = $tree;
|
|
|
|
|
return $tree if $pred->();
|
|
|
|
|
|
|
|
|
|
for my $child (@{$tree->{nodes}}, @{$tree->{floating_nodes}}) {
|
|
|
|
|
my $result = filter_containers($child, $pred);
|
|
|
|
|
return $result if defined($result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undef;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sub leaf_node {
|
|
|
|
|
my ($tree) = @_;
|
|
|
|
|
|
|
|
|
|
return $tree->{type} eq 'con' &&
|
|
|
|
|
@{$tree->{nodes}} == 0 &&
|
|
|
|
|
@{$tree->{floating_nodes}} == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my %allowed_keys = map { ($_, 1) } qw(
|
|
|
|
|
type
|
|
|
|
|
fullscreen_mode
|
|
|
|
|
layout
|
|
|
|
|
border
|
|
|
|
|
current_border_width
|
|
|
|
|
floating
|
|
|
|
|
percent
|
|
|
|
|
nodes
|
|
|
|
|
floating_nodes
|
|
|
|
|
name
|
|
|
|
|
geometry
|
|
|
|
|
window_properties
|
2015-01-18 14:48:34 -05:00
|
|
|
|
mark
|
2013-12-15 15:29:21 -05:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
sub strip_containers {
|
|
|
|
|
my ($tree) = @_;
|
|
|
|
|
|
|
|
|
|
# layout is not relevant for a leaf container
|
|
|
|
|
delete $tree->{layout} if leaf_node($tree);
|
|
|
|
|
|
|
|
|
|
# fullscreen_mode conveys no state at all, it can either be 0 or 1 and the
|
|
|
|
|
# default is _always_ 0, so skip noop entries.
|
|
|
|
|
delete $tree->{fullscreen_mode} if $tree->{fullscreen_mode} == 0;
|
|
|
|
|
|
|
|
|
|
# names for non-leafs are auto-generated and useful only for i3 debugging
|
|
|
|
|
delete $tree->{name} unless leaf_node($tree);
|
|
|
|
|
|
|
|
|
|
delete $tree->{geometry} if zero_rect($tree->{geometry});
|
|
|
|
|
|
|
|
|
|
delete $tree->{current_border_width} if $tree->{current_border_width} == -1;
|
|
|
|
|
|
|
|
|
|
for my $key (keys %$tree) {
|
|
|
|
|
next if exists($allowed_keys{$key});
|
|
|
|
|
|
|
|
|
|
delete $tree->{$key};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for my $key (qw(nodes floating_nodes)) {
|
|
|
|
|
$tree->{$key} = [ map { strip_containers($_) } @{$tree->{$key}} ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $tree;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $json_xs = JSON::XS->new->pretty(1)->allow_nonref->space_before(0)->canonical(1);
|
|
|
|
|
|
|
|
|
|
sub zero_rect {
|
|
|
|
|
my ($rect) = @_;
|
|
|
|
|
return $rect->{x} == 0 &&
|
|
|
|
|
$rect->{y} == 0 &&
|
|
|
|
|
$rect->{width} == 0 &&
|
|
|
|
|
$rect->{height} == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Dumps the containers in JSON, but with comments to explain the user what she
|
|
|
|
|
# needs to fix.
|
|
|
|
|
sub dump_containers {
|
|
|
|
|
my ($tree, $ws, $last) = @_;
|
|
|
|
|
|
|
|
|
|
$ws //= "";
|
|
|
|
|
|
|
|
|
|
say $ws . '{';
|
|
|
|
|
|
|
|
|
|
$ws .= (' ' x 4);
|
|
|
|
|
|
|
|
|
|
if (!leaf_node($tree)) {
|
|
|
|
|
my $desc = $tree->{layout} . ' split container';
|
|
|
|
|
if ($tree->{type} ne 'con') {
|
|
|
|
|
$desc = $tree->{type};
|
|
|
|
|
}
|
|
|
|
|
say "$ws// $desc with " . @{$tree->{nodes}} . " children";
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-05 14:10:24 -05:00
|
|
|
|
# Turn “window_properties” into “swallows” expressions, but only for leaf
|
|
|
|
|
# nodes. It only makes sense for leaf nodes to swallow anything.
|
|
|
|
|
if (leaf_node($tree)) {
|
|
|
|
|
my $swallows = {};
|
|
|
|
|
for my $property (keys %{$tree->{window_properties}}) {
|
|
|
|
|
$swallows->{$property} = '^' . quotemeta($tree->{window_properties}->{$property}) . '$';
|
|
|
|
|
}
|
|
|
|
|
$tree->{swallows} = [ $swallows ];
|
2013-12-15 15:29:21 -05:00
|
|
|
|
}
|
|
|
|
|
delete $tree->{window_properties};
|
|
|
|
|
|
|
|
|
|
my @keys = sort keys %$tree;
|
|
|
|
|
for (0 .. (@keys-1)) {
|
|
|
|
|
my $key = $keys[$_];
|
|
|
|
|
# Those are handled recursively, not printed.
|
|
|
|
|
next if $key eq 'nodes' || $key eq 'floating_nodes';
|
|
|
|
|
|
|
|
|
|
# JSON::XS’s encode appends a newline
|
|
|
|
|
chomp(my $val = $json_xs->encode($tree->{$key}));
|
|
|
|
|
|
|
|
|
|
# Fix indentation. Keep in mind we are producing output to be
|
|
|
|
|
# read/modified by a human.
|
|
|
|
|
$val =~ s/^/$ws/mg;
|
|
|
|
|
$val =~ s/^\s+//;
|
|
|
|
|
|
|
|
|
|
# Comment out all swallows criteria, they are just suggestions.
|
|
|
|
|
if ($key eq 'swallows') {
|
|
|
|
|
$val =~ s,^(\s*)\s{3}",\1// ",gm;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Append a comma unless this is the last value.
|
|
|
|
|
# Ugly, but necessary so that we can print all values before recursing.
|
|
|
|
|
my $comma = ($_ == (@keys-1) &&
|
|
|
|
|
@{$tree->{nodes}} == 0 &&
|
|
|
|
|
@{$tree->{floating_nodes}} == 0 ? '' : ',');
|
|
|
|
|
say qq#$ws"$key": $val$comma#;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for my $key (qw(nodes floating_nodes)) {
|
|
|
|
|
my $num = scalar @{$tree->{$key}};
|
|
|
|
|
next if !$num;
|
|
|
|
|
|
|
|
|
|
say qq#$ws"$key": [#;
|
|
|
|
|
for (0 .. ($num-1)) {
|
|
|
|
|
dump_containers(
|
|
|
|
|
$tree->{$key}->[$_],
|
|
|
|
|
$ws . (' ' x 4),
|
|
|
|
|
($_ == ($num-1)));
|
|
|
|
|
}
|
|
|
|
|
say qq#$ws]#;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ws =~ s/\s{4}$//;
|
|
|
|
|
|
|
|
|
|
say $ws . ($last ? '}' : '},');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
my $tree = $i3->get_tree->recv;
|
|
|
|
|
|
|
|
|
|
my $dump;
|
|
|
|
|
if (defined($workspace)) {
|
|
|
|
|
$dump = filter_containers($tree, sub {
|
|
|
|
|
$_->{type} eq 'workspace' && $_->{name} eq $workspace
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
$dump = filter_containers($tree, sub {
|
|
|
|
|
$_->{type} eq 'output' && $_->{name} eq $output
|
|
|
|
|
});
|
|
|
|
|
# Get the output’s content container (living beneath dockarea containers).
|
|
|
|
|
$dump = first { $_->{type} eq 'con' } @{$dump->{nodes}};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$dump = strip_containers($dump);
|
|
|
|
|
|
|
|
|
|
say "// vim:ts=4:sw=4:et";
|
|
|
|
|
for my $key (qw(nodes floating_nodes)) {
|
|
|
|
|
for (0 .. (@{$dump->{$key}} - 1)) {
|
|
|
|
|
dump_containers($dump->{$key}->[$_], undef, 1);
|
|
|
|
|
# Newlines separate containers so that one can use { and } in vim to
|
|
|
|
|
# jump out of the current container.
|
|
|
|
|
say '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
=encoding utf-8
|
|
|
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
|
|
|
|
|
i3-save-tree - save (parts of) the layout tree for restoring
|
|
|
|
|
|
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
|
|
|
|
|
|
i3-save-tree [--workspace=name] [--output=name]
|
|
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
|
|
|
|
|
Dumps a workspace (or an entire output) to stdout. The data is supposed to be
|
|
|
|
|
edited a bit by a human, then later fed to i3 via the append_layout command.
|
|
|
|
|
|
|
|
|
|
The append_layout command will create placeholder windows, arranged in the
|
|
|
|
|
layout the input file specifies. Each container should have a swallows
|
|
|
|
|
specification. When a window is mapped (made visible on the screen) that
|
|
|
|
|
matches the specification, i3 will put it into that place and kill the
|
|
|
|
|
placeholder.
|
|
|
|
|
|
2015-03-19 13:59:25 -04:00
|
|
|
|
If neither argument is specified, the currently focused workspace will be used.
|
|
|
|
|
|
2013-12-15 15:29:21 -05:00
|
|
|
|
=head1 OPTIONS
|
|
|
|
|
|
|
|
|
|
=over
|
|
|
|
|
|
|
|
|
|
=item B<--workspace=name>
|
|
|
|
|
|
2015-03-19 13:59:25 -04:00
|
|
|
|
Specifies the workspace that should be dumped, e.g. 1.
|
2013-12-15 15:29:21 -05:00
|
|
|
|
|
|
|
|
|
=item B<--output=name>
|
|
|
|
|
|
2015-03-19 13:59:25 -04:00
|
|
|
|
Specifies the output that should be dumped, e.g. LVDS-1.
|
2013-12-15 15:29:21 -05:00
|
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
|
|
=head1 VERSION
|
|
|
|
|
|
|
|
|
|
Version 0.1
|
|
|
|
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
|
|
|
|
|
|
Michael Stapelberg, C<< <michael at i3wm.org> >>
|
|
|
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT
|
|
|
|
|
|
|
|
|
|
Copyright 2013 Michael Stapelberg.
|
|
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it
|
|
|
|
|
under the terms of the BSD license.
|
|
|
|
|
|
|
|
|
|
=cut
|