File Coverage

lib/Term/Tmux/Layout.pm
Criterion Covered Total %
statement 127 191 66.4
branch 21 48 43.7
condition n/a
subroutine 12 15 80.0
pod 4 4 100.0
total 164 258 63.5


line stmt bran cond sub pod time code
1             #
2             # Copyright (C) 2015-2019 Joelle Maslak
3             # All Rights Reserved - See License
4             #
5              
6             package Term::Tmux::Layout;
7             $Term::Tmux::Layout::VERSION = '1.192431';
8 2     2   80162 use v5.8;
  2         6  
9              
10             # ABSTRACT: Create tmux layout strings programmatically
11              
12 2     2   9 use strict;
  2         2  
  2         36  
13 2     2   7 use warnings;
  2         3  
  2         59  
14 2     2   590 use autodie;
  2         13940  
  2         12  
15              
16 2     2   10843 use Carp;
  2         4  
  2         159  
17 2     2   1346 use Moose;
  2         885822  
  2         15  
18 2     2   15169 use namespace::autoclean;
  2         15453  
  2         10  
19              
20              
21              
22             sub set_layout {
23 0 0   0 1 0 if ( $#_ < 1 ) { confess 'invalid call' }
  0         0  
24 0         0 my ( $self, @def ) = @_;
25              
26 0         0 my ( $x, $y ) = $self->get_window_size();
27 0 0       0 if ( !defined($y) ) { die "Cannot get the current tmux window size"; }
  0         0  
28              
29 0         0 $self->hsize($x);
30 0         0 $self->vsize($y);
31              
32 0         0 my $layout = $self->layout(@def);
33 0         0 system( 'tmux', 'select-layout', $layout );
34 0 0       0 if ($@) {
35 0         0 die('Could not set layout');
36             }
37              
38 0         0 return $layout;
39             }
40              
41              
42             sub layout {
43 11 50   11 1 6016 if ( $#_ < 1 ) { confess 'invalid call' }
  0         0  
44 11         26 my ( $self, @desc ) = @_;
45              
46 11         62 my @rows = split /[\n|]/, join( '|', @desc );
47 11         22 my $width = length( $rows[0] );
48 11         22 foreach (@rows) {
49 17 50       38 if ( $width != length($_) ) {
50 0         0 croak 'All rows must be the same length';
51             }
52             }
53              
54 11         22 my $desc = join '|', @desc;
55              
56             # Where are my divisions?
57 11         378 my $hdiv = $self->hsize / ( $width * 1.0 );
58 11         290 my $vdiv = $self->vsize / ( scalar(@rows) * 1.0 );
59              
60 11         35 my @v_grid;
61 11         29 for ( my $i = 0; $i < scalar(@rows); $i++ ) {
62 17         56 $v_grid[$i] = int( $vdiv * $i + .5 );
63             }
64 11         17 my @h_grid;
65 11         27 for ( my $i = 0; $i < length( $rows[0] ); $i++ ) {
66 29         60 $h_grid[$i] = int( $hdiv * $i + .5 );
67             }
68 11         286 push @h_grid, $self->hsize + 1;
69 11         271 push @v_grid, $self->vsize + 1;
70              
71 11         68 my %gridstruct = (
72             hgrid => \@h_grid, # H Start positions for each pane
73             vgrid => \@v_grid, # V Start positions for each pane
74             hparent => 0, # absolute start x position of enclosing window
75             vparent => 0, # absolute start y position of enclosing window
76             hoffset => 0, # We are drawing division at child relative grid location x
77             voffset => 0, # We are drawing division at child relative location x
78             hsize => $#h_grid, # Child grid size X
79             vsize => $#v_grid, # Child grid size Y
80             layout => $desc
81             );
82 11         27 my $result = $self->_divide( \%gridstruct );
83 11         29 return $self->checksum($result) . ",$result";
84             }
85              
86             sub _divide {
87 27 50   27   60 if ( $#_ != 1 ) { confess 'invalid call' }
  0         0  
88 27         51 my ( $self, $gridstruct ) = @_;
89              
90 27         56 my (@map) = $self->_make_map( $gridstruct->{layout} );
91              
92             # Check 1: Are we done (I.E. only one pane left)?
93 27         35 my %panes;
94 27         52 foreach my $r (@map) {
95 41         54 foreach my $c (@$r) {
96 84         141 $panes{$c} = 1;
97             }
98             }
99              
100             # Absolute Location, in grid units, of H and V of parent
101 27         44 my $h_grid_parent_b = $gridstruct->{hparent};
102 27         35 my $v_grid_parent_b = $gridstruct->{vparent};
103              
104             # Absolute Location, in colrow of start of parent division
105 27         41 my $h_char_parent_b = $gridstruct->{hgrid}->[$h_grid_parent_b];
106 27         39 my $v_char_parent_b = $gridstruct->{vgrid}->[$v_grid_parent_b];
107              
108             # Absolute Grid location of H and V start of this division
109 27         43 my $h_grid_abs_b = $gridstruct->{hparent} + $gridstruct->{hoffset};
110 27         39 my $v_grid_abs_b = $gridstruct->{vparent} + $gridstruct->{voffset};
111              
112             # Absolute Locations, in grid units, of end+1 of this division
113 27         42 my $h_grid_abs_n = $h_grid_abs_b + $gridstruct->{hsize};
114 27         37 my $v_grid_abs_n = $v_grid_abs_b + $gridstruct->{vsize};
115              
116             # Absolute Location, in col/row, of start of this division
117 27         39 my $h_char_abs_b = $gridstruct->{hgrid}->[$h_grid_abs_b];
118 27         39 my $v_char_abs_b = $gridstruct->{vgrid}->[$v_grid_abs_b];
119 27 100       52 if ( $h_char_abs_b > 0 ) { $h_char_abs_b++; } # Adjust for pane border
  9         11  
120 27 50       51 if ( $v_char_abs_b > 0 ) { $v_char_abs_b++; } # Adjust for pane border
  0         0  
121              
122             # Absolute Location, in col/row of end+1 of this division
123 27         42 my $h_char_abs_n = $gridstruct->{hgrid}->[$h_grid_abs_n];
124 27         36 my $v_char_abs_n = $gridstruct->{vgrid}->[$v_grid_abs_n];
125              
126             # Relative Position (to parent) of start of this division
127 27         47 my $h_char_rel_b = $h_char_abs_b - $h_char_parent_b;
128 27         40 my $v_char_rel_b = $v_char_abs_b - $v_char_parent_b;
129              
130             # Relative Position (to parent) of next division
131 27         35 my $h_char_rel_n = $h_char_abs_n - $h_char_parent_b;
132 27         37 my $v_char_rel_n = $v_char_abs_n - $v_char_parent_b;
133              
134             # Division width/height in col/rows
135 27         35 my $h_size = $h_char_rel_n - $h_char_rel_b;
136 27         1220 my $v_size = $v_char_rel_n - $v_char_rel_b;
137 27 100       58 if ( $h_char_abs_b == 0 ) { $h_size--; }
  18         37  
138 27 50       46 if ( $v_char_abs_b == 0 ) { $v_size--; }
  27         35  
139              
140 27         68 my $result = "${h_size}x${v_size},${h_char_abs_b},${v_char_abs_b}";
141              
142 27 100       61 if ( scalar( keys %panes ) == 1 ) {
143             # We throw in a bogus pane value because it is ignroed anyhow
144 19         84 return "$result,100";
145             }
146              
147             # Check 2: Can we do a vertical split?
148             NEXTV:
149 8         16 for ( my $i = 1; $i < scalar( @{ $map[0] } ); $i++ ) {
  12         27  
150 12         26 for ( my $j = 0; $j < scalar(@map); $j++ ) {
151 16 100       76 if ( $map[$j]->[ $i - 1 ] eq $map[$j]->[$i] ) {
152              
153             # Can't split here
154 4         10 next NEXTV;
155             }
156             }
157              
158             # We can split here!
159              
160             # TODO: We should check that we aren't allowing things
161             # that are 0xY or Xx0
162 8         24 my (@vfield) = $self->_vsplit_field( $gridstruct->{layout}, $i );
163              
164             my %left = (
165             hgrid => $gridstruct->{hgrid},
166             vgrid => $gridstruct->{vgrid},
167             hparent => $h_grid_abs_b,
168             vparent => $v_grid_abs_b,
169             hoffset => 0,
170             voffset => 0,
171             hsize => $i,
172             vsize => $gridstruct->{vsize},
173 8         38 layout => $vfield[0]
174             );
175             my %right = (
176             hgrid => $gridstruct->{hgrid},
177             vgrid => $gridstruct->{vgrid},
178             hparent => $h_grid_abs_b,
179             vparent => $v_grid_abs_b,
180             hoffset => $i,
181             voffset => 0,
182             hsize => $gridstruct->{hsize} - $i,
183             vsize => $gridstruct->{vsize},
184 8         36 layout => $vfield[1]
185             );
186              
187 8         38 $result .= '{' . $self->_divide( \%left ) . ',' . $self->_divide( \%right ) . '}';
188              
189 8         43 return $result;
190             }
191              
192             # Check 3: Can we do a horizontal split?
193             NEXTH:
194 0         0 for ( my $j = 1; $j < scalar(@map); $j++ ) {
195 0         0 for ( my $i = 0; $i < scalar( @{ $map[0] } ); $i++ ) {
  0         0  
196 0 0       0 if ( $map[ $j - 1 ]->[$i] eq $map[$j]->[$i] ) {
197              
198             # Can't split here
199 0         0 next NEXTH;
200             }
201             }
202              
203 0         0 my (@hfield) = $self->_hsplit_field( $gridstruct->{layout}, $j );
204              
205             my %left = (
206             hgrid => $gridstruct->{hgrid},
207             vgrid => $gridstruct->{vgrid},
208             hparent => $h_grid_abs_b,
209             vparent => $v_grid_abs_b,
210             hoffset => 0,
211             voffset => 0,
212             hsize => $gridstruct->{hsize},
213 0         0 vsize => $j,
214             layout => $hfield[0]
215             );
216             my %right = (
217             hgrid => $gridstruct->{hgrid},
218             vgrid => $gridstruct->{vgrid},
219             hparent => $h_grid_abs_b,
220             vparent => $v_grid_abs_b,
221             hoffset => 0,
222             voffset => $j,
223             hsize => $gridstruct->{hsize},
224 0         0 vsize => $gridstruct->{vsize} - $j,
225             layout => $hfield[1]
226             );
227             # We can split here!
228              
229             # TODO: We should check that we aren't allowing things
230             # that are 0xY or Xx0
231              
232 0         0 $result .= '[' . $self->_divide( \%left ) . ',' . $self->_divide( \%right ) . ']';
233              
234 0         0 return $result;
235             }
236              
237 0         0 die("Can't split");
238             }
239              
240             sub _hsplit_field {
241 0 0   0   0 if ( $#_ != 2 ) { confess 'invalid call'; }
  0         0  
242 0         0 my ( $self, $field, $spos ) = @_;
243              
244 0         0 my (@map) = $self->_make_map($field);
245              
246 0         0 my (@split) = ( [], [] );
247 0         0 for ( my $i = 0; $i < scalar( @{ $map[0] } ); $i++ ) {
  0         0  
248 0         0 for ( my $j = 0; $j < scalar(@map); $j++ ) {
249              
250             # Create the row
251 0 0       0 if ( $i == 0 ) {
252 0         0 $split[0]->[$j] = [];
253 0         0 $split[1]->[$j] = [];
254             }
255              
256 0 0       0 if ( $j < $spos ) {
257              
258             # First map
259 0         0 $split[0]->[$j]->[$i] = $map[$j]->[$i];
260             } else {
261              
262             # Second map
263 0         0 $split[1]->[ $j - $spos ]->[$i] = $map[$j]->[$i];
264             }
265             }
266             }
267              
268 0         0 my $field1 = join "\n", map { join '', @$_ } @{ $split[0] };
  0         0  
  0         0  
269 0         0 my $field2 = join "\n", map { join '', @$_ } @{ $split[1] };
  0         0  
  0         0  
270              
271 0         0 return ( $field1, $field2 );
272             }
273              
274             sub _vsplit_field {
275 8 50   8   18 if ( $#_ != 2 ) { confess 'invalid call'; }
  0         0  
276 8         19 my ( $self, $field, $spos ) = @_;
277              
278 8         17 my (@map) = $self->_make_map($field);
279              
280 8         17 my (@split) = ( [], [] );
281 8         12 for ( my $i = 0; $i < scalar( @{ $map[0] } ); $i++ ) {
  34         64  
282 26         49 for ( my $j = 0; $j < scalar(@map); $j++ ) {
283              
284             # Create the row
285 39 100       63 if ( $i == 0 ) {
286 12         20 $split[0]->[$j] = [];
287 12         24 $split[1]->[$j] = [];
288             }
289              
290 39 100       64 if ( $i < $spos ) {
291              
292             # First map
293 18         48 $split[0]->[$j]->[$i] = $map[$j]->[$i];
294             } else {
295              
296             # Second map
297 21         51 $split[1]->[$j]->[ $i - $spos ] = $map[$j]->[$i];
298             }
299             }
300             }
301              
302 8         15 my $field1 = join "\n", map { join '', @$_ } @{ $split[0] };
  12         33  
  8         17  
303 8         29 my $field2 = join "\n", map { join '', @$_ } @{ $split[1] };
  12         27  
  8         11  
304              
305 8         31 return ( $field1, $field2 );
306             }
307              
308             sub _make_map {
309 35 50   35   63 if ( $#_ != 1 ) { confess 'invalid call' }
  0         0  
310 35         64 my ( $self, $field ) = @_;
311              
312 35 50       78 if ( !defined($field) ) { confess 'Empty field!' }
  0         0  
313              
314 35         46 my @map;
315 35         44 my $rpos = 0;
316 35         108 foreach my $row ( split /[\n|]/, $field ) {
317 53         69 my $cpos = 0;
318 53         82 $map[$rpos] = [];
319 53         105 foreach my $col ( split //, $row ) {
320 123         196 $map[$rpos]->[$cpos] = $col;
321 123         491 $cpos++;
322             }
323 53         91 $rpos++;
324             }
325              
326 35         78 return @map;
327             }
328              
329              
330             sub checksum {
331 14 50   14 1 1567 if ( $#_ != 1 ) { confess 'invalid call'; }
  0         0  
332 14         29 my ( $self, $str ) = @_;
333              
334             # We silently discard a newline if it appears.
335 14         25 chomp($str);
336              
337 14         21 my $csum = 0;
338 14         105 foreach my $c ( split //, $str ) {
339 613         830 $csum = ( $csum >> 1 ) + ( ( $csum & 1 ) << 15 ) % 65536;
340 613         751 $csum += ord($c);
341 613         811 $csum %= 65536;
342             }
343              
344 14         130 return sprintf( "%04x", $csum );
345             }
346              
347              
348             sub get_window_size {
349 0 0   0 1   if ( scalar(@_) != 1 ) { confess 'invalid call' }
  0            
350              
351 0           my (@windows) = `tmux list-windows`;
352 0           @windows = grep { /\(active\)$/ } map { chomp; $_ } @windows;
  0            
  0            
  0            
353              
354 0 0         if ( scalar(@windows) ) {
355 0           my ( $x, $y ) = $windows[0] =~ / \[([0-9]+)x([0-9]+)\] /;
356 0           return ( $x, $y );
357             }
358              
359 0           return;
360             }
361              
362              
363             has 'hsize' => (
364             is => 'rw',
365             isa => 'Int',
366             default => 80
367             );
368              
369              
370             has 'vsize' => (
371             is => 'rw',
372             isa => 'Int',
373             default => 24
374             );
375              
376              
377             __PACKAGE__->meta->make_immutable;
378              
379             1;
380              
381             __END__
382              
383             =pod
384              
385             =encoding UTF-8
386              
387             =head1 NAME
388              
389             Term::Tmux::Layout - Create tmux layout strings programmatically
390              
391             =head1 VERSION
392              
393             version 1.192431
394              
395             =head1 SYNOPSIS
396              
397             my $layout = Term::Tmux::Layout->new();
398             my $checksum = $layout->set_layout('abc|def');
399              
400             =head1 DESCRIPTION
401              
402             Set tmux pane layouts using via a simpler interface. See also L<tmuxlayout>
403             which wraps this module in a command-line script.
404              
405             =head1 ATTRIBUTES
406              
407             =head2 hsize
408              
409             Defines the width of the terminal window (the entire canvas),
410             with a default of 80.
411              
412             =head2 vsize
413              
414             Defines the height of the terminal window tmux canvas (does not
415             include the status line and command line at the bottom, so this
416             should be one line smaller than the actual terminal emulator
417             window size). This defaults to 24.
418              
419             =head1 METHODS
420              
421             =head2 set_layout( $definition )
422              
423             This option sets the layout to the string definition provided. The string
424             provided must follow the requirements of C<layout()> described elsewhere
425             in this document.
426              
427             This command will determine the current tmux window size (using
428             C<get_window_size()>) and then calls C<layout()> to get the layout string
429             in proper tmux format. Finally, it executes tmux to select that layout
430             as the active layout.
431              
432             You can only run this method from a tmux window. C<tmuxlayout> is a thin
433             wrapper around this function.
434              
435             =head2 layout ( $layout )
436              
437             This method takes a "layout" in a text format, and outputs
438             the proper output.
439              
440             The layout format consists of a text field of numbers or other
441             characters, separated by new lines. Each character reflects a
442             single pane on the screen, defining its' size in rows and
443             columns.
444              
445             Some sample layouts:
446              
447             11123
448             11124
449              
450             This would create a layout with 4 panes. The panes would be
451             arranged such that pane 1 takes up the entire vertical canvas,
452             but only 3/5ths of the horizontal canvas. Pane 2 also takes up
453             the entire vertical canvas, but only 1/5 of the horizontal
454             canvas. Pane 3 and 4 are stacked, taking 1/5 of the horizontal
455             canvas, evenly splitting the vertical canvas.
456              
457             Note that some layouts cannot be displayed by tmux. For example,
458             the following would be invalid:
459              
460             1122
461             1134
462             5556
463              
464             Tmux divides the entire screen up either horizontally or vertically.
465             However, there is no single horizontal or vertical split that would
466             allow this screen to be divided.
467              
468             This layout can be passed a single scalar, where the rows are
469             seperated by pipe characters C<|> or new lines.
470              
471             If this function is passed an array in the place of the definition,
472             each element starts its own row. Each element can also contain pipe
473             or newlines, and these are also interpreted as row deliminators.
474              
475             Thus, the following are all valid calls to layout:
476              
477             $obj->layout('abc|def|ghi');
478              
479             $obj->layout("abc\ndef\nghi");
480              
481             $obj->layout('abc', 'def', 'ghi');
482              
483             $obj->layout('abc|def', 'ghi');
484              
485             =head2 checksum( $str )
486              
487             This method performs the tmux checksum, as described in the tmux
488             source code in C<layout_checksum()>. The input value is the string
489             without the checksum on the front. The output is the checksum
490             value as a string (four hex characters).
491              
492             =head2 get_window_size( )
493              
494             This method fetches the window size for the currently active tmux
495             window. If tmux is not running, it instead returns C<undef>.
496              
497             =head2 new
498              
499             my $layout = Term::Tmux::Layout( hsize => 80, vsize => 23 );
500              
501             Create a new layout class. Optionally takes named parameters
502             for the C<hsize> and C<vsize>.
503              
504             =head1 TODO
505              
506             =over 4
507              
508             =item * Break out command execution
509              
510             There probably should be a Term::Tmux::Command module to execute tmux
511             commands, rather than having the window size commands executed directly
512             by this module.
513              
514             =back
515              
516             =head1 REPOSITORY
517              
518             L<https://github.com/jmaslak/Term-Tmux-Layout>
519              
520             =head1 SEE ALSO
521              
522             See L<tmuxlayout> for a command line utility that wraps this module.
523              
524             =head1 BUGS
525              
526             Check the issue tracker at:
527             L<https://github.com/jmaslak/Term-Tmux-Layout/issues>
528              
529             =head1 AUTHOR
530              
531             Joelle Maslak <jmaslak@antelope.net>
532              
533             =head1 COPYRIGHT AND LICENSE
534              
535             This software is copyright (c) 2015-2018 by Joelle Maslak.
536              
537             This is free software; you can redistribute it and/or modify it under
538             the same terms as the Perl 5 programming language system itself.
539              
540             =cut