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.223320';
8 2     2   84255 use v5.8;
  2         7  
9              
10             # ABSTRACT: Create tmux layout strings programmatically
11              
12 2     2   10 use strict;
  2         4  
  2         37  
13 2     2   12 use warnings;
  2         4  
  2         45  
14 2     2   609 use autodie;
  2         16084  
  2         9  
15              
16 2     2   12472 use Carp;
  2         5  
  2         160  
17 2     2   1429 use Moose;
  2         940568  
  2         13  
18 2     2   17027 use namespace::autoclean;
  2         17253  
  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 5908 if ( $#_ < 1 ) { confess 'invalid call' }
  0         0  
44 11         28 my ( $self, @desc ) = @_;
45              
46 11         61 my @rows = split /[\n|]/, join( '|', @desc );
47 11         24 my $width = length( $rows[0] );
48 11         17 foreach (@rows) {
49 17 50       39 if ( $width != length($_) ) {
50 0         0 croak 'All rows must be the same length';
51             }
52             }
53              
54 11         19 my $desc = join '|', @desc;
55              
56             # Where are my divisions?
57 11         381 my $hdiv = $self->hsize / ( $width * 1.0 );
58 11         293 my $vdiv = $self->vsize / ( scalar(@rows) * 1.0 );
59              
60 11         17 my @v_grid;
61 11         31 for ( my $i = 0; $i < scalar(@rows); $i++ ) {
62 17         56 $v_grid[$i] = int( $vdiv * $i + .5 );
63             }
64 11         19 my @h_grid;
65 11         26 for ( my $i = 0; $i < length( $rows[0] ); $i++ ) {
66 29         64 $h_grid[$i] = int( $hdiv * $i + .5 );
67             }
68 11         282 push @h_grid, $self->hsize + 1;
69 11         293 push @v_grid, $self->vsize + 1;
70              
71 11         62 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         29 my $result = $self->_divide( \%gridstruct );
83 11         28 return $self->checksum($result) . ",$result";
84             }
85              
86             sub _divide {
87 27 50   27   62 if ( $#_ != 1 ) { confess 'invalid call' }
  0         0  
88 27         49 my ( $self, $gridstruct ) = @_;
89              
90 27         57 my (@map) = $self->_make_map( $gridstruct->{layout} );
91              
92             # Check 1: Are we done (I.E. only one pane left)?
93 27         39 my %panes;
94 27         42 foreach my $r (@map) {
95 41         59 foreach my $c (@$r) {
96 84         145 $panes{$c} = 1;
97             }
98             }
99              
100             # Absolute Location, in grid units, of H and V of parent
101 27         38 my $h_grid_parent_b = $gridstruct->{hparent};
102 27         44 my $v_grid_parent_b = $gridstruct->{vparent};
103              
104             # Absolute Location, in colrow of start of parent division
105 27         34 my $h_char_parent_b = $gridstruct->{hgrid}->[$h_grid_parent_b];
106 27         36 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         46 my $h_grid_abs_b = $gridstruct->{hparent} + $gridstruct->{hoffset};
110 27         40 my $v_grid_abs_b = $gridstruct->{vparent} + $gridstruct->{voffset};
111              
112             # Absolute Locations, in grid units, of end+1 of this division
113 27         1026 my $h_grid_abs_n = $h_grid_abs_b + $gridstruct->{hsize};
114 27         36 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         38 my $h_char_abs_b = $gridstruct->{hgrid}->[$h_grid_abs_b];
118 27         43 my $v_char_abs_b = $gridstruct->{vgrid}->[$v_grid_abs_b];
119 27 100       50 if ( $h_char_abs_b > 0 ) { $h_char_abs_b++; } # Adjust for pane border
  9         15  
120 27 50       42 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         43 my $h_char_abs_n = $gridstruct->{hgrid}->[$h_grid_abs_n];
124 27         32 my $v_char_abs_n = $gridstruct->{vgrid}->[$v_grid_abs_n];
125              
126             # Relative Position (to parent) of start of this division
127 27         57 my $h_char_rel_b = $h_char_abs_b - $h_char_parent_b;
128 27         48 my $v_char_rel_b = $v_char_abs_b - $v_char_parent_b;
129              
130             # Relative Position (to parent) of next division
131 27         34 my $h_char_rel_n = $h_char_abs_n - $h_char_parent_b;
132 27         35 my $v_char_rel_n = $v_char_abs_n - $v_char_parent_b;
133              
134             # Division width/height in col/rows
135 27         40 my $h_size = $h_char_rel_n - $h_char_rel_b;
136 27         34 my $v_size = $v_char_rel_n - $v_char_rel_b;
137 27 100       48 if ( $h_char_abs_b == 0 ) { $h_size--; }
  18         24  
138 27 50       42 if ( $v_char_abs_b == 0 ) { $v_size--; }
  27         37  
139              
140 27         68 my $result = "${h_size}x${v_size},${h_char_abs_b},${v_char_abs_b}";
141              
142 27 100       58 if ( scalar( keys %panes ) == 1 ) {
143             # We throw in a bogus pane value because it is ignroed anyhow
144 19         104 return "$result,100";
145             }
146              
147             # Check 2: Can we do a vertical split?
148             NEXTV:
149 8         13 for ( my $i = 1; $i < scalar( @{ $map[0] } ); $i++ ) {
  12         24  
150 12         35 for ( my $j = 0; $j < scalar(@map); $j++ ) {
151 16 100       47 if ( $map[$j]->[ $i - 1 ] eq $map[$j]->[$i] ) {
152              
153             # Can't split here
154 4         12 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         20 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         36 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         34 layout => $vfield[1]
185             );
186              
187 8         39 $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   19 if ( $#_ != 2 ) { confess 'invalid call'; }
  0         0  
276 8         16 my ( $self, $field, $spos ) = @_;
277              
278 8         15 my (@map) = $self->_make_map($field);
279              
280 8         18 my (@split) = ( [], [] );
281 8         12 for ( my $i = 0; $i < scalar( @{ $map[0] } ); $i++ ) {
  34         66  
282 26         49 for ( my $j = 0; $j < scalar(@map); $j++ ) {
283              
284             # Create the row
285 39 100       59 if ( $i == 0 ) {
286 12         25 $split[0]->[$j] = [];
287 12         18 $split[1]->[$j] = [];
288             }
289              
290 39 100       67 if ( $i < $spos ) {
291              
292             # First map
293 18         44 $split[0]->[$j]->[$i] = $map[$j]->[$i];
294             } else {
295              
296             # Second map
297 21         54 $split[1]->[$j]->[ $i - $spos ] = $map[$j]->[$i];
298             }
299             }
300             }
301              
302 8         13 my $field1 = join "\n", map { join '', @$_ } @{ $split[0] };
  12         35  
  8         17  
303 8         26 my $field2 = join "\n", map { join '', @$_ } @{ $split[1] };
  12         25  
  8         14  
304              
305 8         33 return ( $field1, $field2 );
306             }
307              
308             sub _make_map {
309 35 50   35   69 if ( $#_ != 1 ) { confess 'invalid call' }
  0         0  
310 35         62 my ( $self, $field ) = @_;
311              
312 35 50       61 if ( !defined($field) ) { confess 'Empty field!' }
  0         0  
313              
314 35         46 my @map;
315 35         51 my $rpos = 0;
316 35         100 foreach my $row ( split /[\n|]/, $field ) {
317 53         73 my $cpos = 0;
318 53         85 $map[$rpos] = [];
319 53         103 foreach my $col ( split //, $row ) {
320 123         205 $map[$rpos]->[$cpos] = $col;
321 123         161 $cpos++;
322             }
323 53         94 $rpos++;
324             }
325              
326 35         78 return @map;
327             }
328              
329              
330             sub checksum {
331 14 50   14 1 1505 if ( $#_ != 1 ) { confess 'invalid call'; }
  0         0  
332 14         28 my ( $self, $str ) = @_;
333              
334             # We silently discard a newline if it appears.
335 14         26 chomp($str);
336              
337 14         20 my $csum = 0;
338 14         109 foreach my $c ( split //, $str ) {
339 613         900 $csum = ( $csum >> 1 ) + ( ( $csum & 1 ) << 15 ) % 65536;
340 613         804 $csum += ord($c);
341 613         825 $csum %= 65536;
342             }
343              
344 14         127 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.223320
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             5554
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-2022 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