#!/usr/bin/perl # Copyright 2004, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Cascade-Pilot Software"), to deal in the Cascade-Pilot Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Cascade-Pilot Software, and to permit persons to whom the Cascade-Pilot Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Cascade-Pilot Software. THE CASCADE-PILOT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE CASCADE-PILOT SOFTWARE OR THE USE OR OTHER DEALINGS IN THE CASCADE-PILOT SOFTWARE. use strict; use warnings; =pod =head1 NAME cascade-pilot - move a window in cascade fashion =head1 SYNOPSIS cascade-pilot [options] move north|south|west|east cascade-pilot output screen cascade-pilot --help | --man =head1 DESCRIPTION I is a tool to move windows north|south through a cascade of other windows; or east|west between cascades and across screens. =cut require Getopt::Long; require Pod::Usage; use lib "$ENV{HOME}/.config", '/etc/xdg'; # for screenpager config files sub _cascade( $$ ); # ( window, -1|1 ) ret$ 0|1 # # Moves the window up or down in the cascade, if there is one. # Returns 1 if the window was moved; 0 otherwise. # *_cascade_gap = \22; # # Standard vertical gap between cascaded windows. # sub _get_frame_strut( $ ); # ( window ) # ret@ ( left, right, top, bottom ) # # Returns widths of window decorations on all 4 sides. # # my( $frame_left, $frame_right, $frame_top, $frame_bottom ) # = _get_frame_strut( $window ); # sub _get_screen( $$ ); # ( x, width ) ret@ # # Returns the screen of the specified window location, # together with the positions of its first and last columns in desktop space. # # my( $screen, $screen_x0, $screen_x1 ) = _get_screen( $x, $width ); # sub _get_window(); # ret$ window|0 # # Returns the ID of the active window, # or 0 if none is active. # sub _get_wininfo( $ ); # ( window ) ret@ # # Returns the wininfo results for the specified window. # # my( $wininfo, $x, $y, $width, $height ) = _get_wininfo( $window ); # sub _get_wm_state( $ ); # ( window ) ret@ # # Returns the _NET_WM_STATE properties for the specified window. # # my( $is_decorated, $is_fullscreen, $is_maximized_horz, $is_maximized_vert ) # = _get_wm_state( $window ); # sub _move( $$ ); # ( window, -1|1 ) ret$ 0|1 # # Moves the window to an adjacent screen, east or west, if there is one. # The window is positioned in the north-east corner of the new screen. # Returns 1 if the window was moved; 0 otherwise. # sub _reposition_window( $$$;$$$ ); # ( window, wininfo, x ; y, width, height ) # # Repositions the window using wmctrl -r -e. # Has no effect on position/size attributes that are passed in as undef. # Any position/size value of -1 is translated to 0, # because of wmctrl limitations. # sub _shift_options(); # # Shifts command line options into %option. # # ---------------------------------------------------------------------------------------- my %option; _shift_options; =pod =head1 ARGUMENTS =over 8 =cut my $argument = shift; defined $argument or $argument = ''; # prevent warnings # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( $argument eq 'move' ) { =pod =item B north|south|west|east Move the active window north|south through a cascade of other windows; or east|west between cascades and across screens. =cut my $window = _get_window; if( $window == 0 ) { warn 'aborting, no active window'; exit 2; } { my( $is_decorated, $is_fullscreen, $is_maximized_horz, $is_maximized_vert ) = _get_wm_state( $window ); if( $is_fullscreen || $is_maximized_horz || $is_maximized_vert ) { warn 'aborting, cannot pilot a maximized window'; exit 2; } if( !$is_decorated ) { warn 'aborting, cannot pilot an undecorated window'; # I could, but currently the determination of %x_block depends on decorations exit 2; } } my $sub_argument = shift; defined $sub_argument or $sub_argument = ''; # prevent warnings my $was_moved; if( $sub_argument eq 'north' ) { $was_moved = _cascade( $window, -1 ); } elsif( $sub_argument eq 'south' ) { $was_moved = _cascade( $window, 1 ); } elsif( $sub_argument eq 'west' ) { $was_moved = _move( $window, -1 ); $option{conserve} = 1; $was_moved and _cascade( $window, 1 ); } elsif( $sub_argument eq 'east' ) { $was_moved = _move( $window, 1 ); $option{conserve} = 1; $was_moved and _cascade( $window, 1 ); } else { Pod::Usage::pod2usage( -verbose => 0 ); # and exits } if( $was_moved ) # then ensure focus not lost by motion { my $command = "wmctrl -i -a $window"; # (re)activate system $command and die 'unable to execute: ' . $command; } exit; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( $argument eq 'output' ) { =pod =item B screen Outputs the screen of the active window, if any. =cut my $object = shift; defined $object or $object = ''; # prevent warnings if( $object eq 'screen' ) { my $window = _get_window; $window == 0 and exit; my( $wininfo, $x, $y, $width ) = _get_wininfo( $window ); my( $screen, $screen_x0, $screen_x1 ) = _get_screen( $x, $width ); print "$screen\n"; } else { Pod::Usage::pod2usage( -verbose => 0 ); # and exits } exit; } =pod =back =cut Pod::Usage::pod2usage( -verbose => 0 ); # and exits # - S u b - B o d y ---------------------------------------------------------------------- # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _cascade( $$ ) { my $window = shift; my $direction_sign = shift; # Identify current desktop. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - my $desktop; { my $command = 'xprop -root _NET_CURRENT_DESKTOP'; $_ = `$command`; # e.g. _NET_CURRENT_DESKTOP(CARDINAL) = 0 m'= ([0-9]+)$' or die "unrecognized output from '$command'"; $desktop = $1; # e.g. 0 } # Identify window's screen and calculate its geometry. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - my( $wininfo, $x, $y, $width, $height ) = _get_wininfo( $window ); my( $screen, $screen_x0, $screen_x1 ) = _get_screen( $x, $width ); # List all windows. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - require X11::Protocol; my $server = X11::Protocol->new(); my @full_window_list; # FIX to use Zelea::WindowManagement::net_client_list() { my $atom = $server->atom( '_NET_CLIENT_LIST' ); my $elements_to_get = 256; # at a time (actual value not critical) my $element_offset = 0; # to start for( ;; ) { my( $value, $type, $format, $bytes_after ) = $server->GetProperty ( $server->root, $atom, 'AnyPropertyType', $element_offset, $elements_to_get ); $format == 32 or die; push @full_window_list, unpack( 'I*', $value ); $bytes_after or last; $element_offset += $elements_to_get; } } # Filter window list down to a single cascade, # stored as a list of references to points, # each of which is a potential anchor for repositioning the window. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - my $cascade_offset = $::_cascade_gap * $direction_sign; my @point_ARRAY; my %x_block; # defining x positions that are blocked my $w; FILTER: foreach $w( @full_window_list ) { # Filter out self. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` $w == $window and next FILTER; # Filter by desktop. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { my $atom = $server->atom( '_NET_WM_DESKTOP' ); my( $value, $type, $format, $bytes_after ); eval { ( $value, $type, $format, $bytes_after ) = $server->GetProperty( $w, $atom, 'AnyPropertyType', 0, 1 ); }; $@ and next FILTER; # protocol error, probably because $w has closed if( $format != 32 ) { # warn "bad format ($format), assume window closed"; next FILTER; } my $w_desktop = unpack 'I', $value; # -1 (or its unsigned equivalent) for windows which appear on all desktops # so we filter those out too $w_desktop == $desktop or next FILTER; } # Filter by screen. # Only windows on the same screen are in the cascade. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` my( $w_wininfo, $w_x, $w_y, $w_width ) = _get_wininfo( $w ); { my $mid_x = $w_x + $w_width / 2; # center column of window ( $mid_x < $screen_x0 || $mid_x > $screen_x1 ) and next FILTER; } # Filter by state. # Maximized windows cannot be part of a cascade. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { my( $is_decorated, $is_fullscreen, $is_maximized_horz, $is_maximized_vert ) = _get_wm_state( $w ); $is_fullscreen || $is_maximized_horz and next FILTER; if( $is_maximized_vert ) { my $x_to_block; # may be a cascaded window, so save its place, for when it is restored from max [an alternative would be to restore these windows beforehand, and re-max them afterwards -- that is probably a simpler, more robust strategy; though would still require making up for stripped decorations, because I cannot redecorate from outside of Openbox; but it would make windows jump around alot] if( $is_decorated ) { $x_to_block = $w_x; } else # undecorated, as I usually force it to be, on my machine { my( $frame_left, $frame_right, $frame_top, $frame_bottom ) = _get_frame_strut( $window ); $x_to_block = $w_x - $frame_left; # i.e. where it will be when restored from max, and redecorated } $x_block{$x_to_block} = 1; next FILTER; } } # Filter by direction. # No repositioning points are in the opposite tail of the cascade, # because the window will not be moving in that direction. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { my $y_bound = $y - $cascade_offset; # look slightly in the other direction, on account of offset, a valid anchor may be there $w_y * $direction_sign > $y_bound * $direction_sign or next FILTER; } # ` ` ` push @point_ARRAY, [ $w_x, $w_y ]; } # Select the nearest, unblocked position for the window, among the points. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - my( $new_x, $new_y ); if( scalar @point_ARRAY ) { @point_ARRAY = sort{ $$a[1] <=> $$b[1] } @point_ARRAY; # sort vertically, top down SELECT: { my $p; # index into @point_ARRAY my $p_last; if( $direction_sign == 1 ) { $p = 0; # top point first $p_last = @point_ARRAY - 1; } else { $p = @point_ARRAY - 1; # bottom point first $p_last = 0; } # Before the first point, if there is room. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` { my $point_ARRAY = $point_ARRAY[$p]; my $leading_gap = ($$point_ARRAY[1] - $y) * $direction_sign; if( defined $option{conserve} && $leading_gap >= $::_cascade_gap && !defined $x_block{$x} ) { $new_x = $x; # let it stay put $new_y = $y; last SELECT; } elsif( $leading_gap > $::_cascade_gap ) { $new_x = $$point_ARRAY[0] + $cascade_offset; $new_y = $$point_ARRAY[1] - $cascade_offset; defined $x_block{$new_x} or last SELECT; } } # In the first available gap between windows, if any is large enough. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` while( $p != $p_last ) { my $point_0_ARRAY = $point_ARRAY[$p]; my $point_1_ARRAY = $point_ARRAY[$p+$direction_sign]; my $gap = ($$point_1_ARRAY[1] - $$point_0_ARRAY[1]) * $direction_sign; if( $gap >= $::_cascade_gap * 2 ) { $new_x = $$point_0_ARRAY[0] - $cascade_offset; $new_y = $$point_0_ARRAY[1] + $cascade_offset; defined $x_block{$new_x} or last SELECT; } $p += $direction_sign; } # Otherwise, after the last point. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` my $point_ARRAY = $point_ARRAY[$p_last]; $new_x = $$point_ARRAY[0]; $new_y = $$point_ARRAY[1]; for( ;; ) { $new_x -= $cascade_offset; $new_y += $cascade_offset; defined $x_block{$new_x} or last; } } } else # no cascade points { $new_x = $x; # no motion required, till proven otherwise $new_y = $y; # Cascade anyway, if blocked. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` if( defined $x_block{$x} ) { while( defined $x_block{$new_x} ) { $new_x -= $cascade_offset; $new_y += $cascade_offset; } } # Otherwise, jump to a screen corner. # ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` elsif( !defined $option{conserve} ) { if( $direction_sign < 0 ) # to north-east { $new_x = $screen_x1 - $width + 1; $new_y = 0; } else # to south-west { require screenpager::Config_1; my $screen_config_HASH = $screenpager::Config_1::screen[$screen]; $new_x = $screen_x0; $new_y = $$screen_config_HASH{height} - $height; } } } # Move the window to its new position, if necessary. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - $new_x == $x && $new_y == $y and return 0; # no need to move _reposition_window( $window, $wininfo, $new_x, $new_y ); return 1; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _get_frame_strut( $ ) { my $window = shift; my( $left, $right, $top, $bottom ) = ( 0,0,0,0 ); # till proven otherwise my( $is_decorated, $is_fullscreen, $is_maximized_horz, $is_maximized_vert ) = _get_wm_state( $window ); if( $is_decorated && !$is_fullscreen ) { my $command = "xprop -id $window _KDE_NET_WM_FRAME_STRUT"; # non-standard, but no choice $_ = `$command`; # e.g. _KDE_NET_WM_FRAME_STRUT(CARDINAL) = 1, 1, 24, 4 if( m'= ([0-9]+), ([0-9]+), ([0-9]+), ([0-9]+)$' ) { my( $l, $r, $t, $b ) = ( $1, $2, $3, $4 ); # e.g. 1, 1, 24, 4 if( !$is_maximized_horz ) { $left = $l; $right = $r; } if( !$is_maximized_vert ) { $top = $t; $bottom = $b; } } } # else assume it is like gkrellm2, and has no decorations anyway; or window closed return ( $left, $right, $top, $bottom ); } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _get_screen( $$ ) { my $x = shift; my $width = shift; require screenpager::Config_1; my $screen = 0; # thus far my $screen_x0 = 0; # leftmost column of screen, in desktop space # so far my $screen_x1; # rightmost { my $mid_x = $x + $width / 2; # center column of window my $last_screen = @screenpager::Config_1::screen - 1; for( ;; ) { my $screen_config_HASH = $screenpager::Config_1::screen[$screen]; $screen_x1 = $screen_x0 + $$screen_config_HASH{width} - 1; $screen >= $last_screen || $mid_x <= $screen_x1 and last; ++$screen; $screen_x0 = $screen_x1 + 1; } } return ( $screen, $screen_x0, $screen_x1 ); } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _get_window() { my $command = 'xprop -root _NET_ACTIVE_WINDOW'; $_ = `$command`; # e.g. _NET_ACTIVE_WINDOW(WINDOW): window id # 0x602f9a /id \# (0x[0-9a-f]+)$/ or die "unrecognized output from '$command'"; my $window = $1; # e.g. 0x602f9a, or 0x0 if none active $window = oct $window; # to decimal return $window; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _get_wininfo( $ ) { # use Zelea::WindowManagement qw( title_from_xwininfo ); # my $window = shift; my( $x, $y, $width, $height, $title ); my( $frame_left, $frame_right, $frame_top, $frame_bottom ) = _get_frame_strut( $window ); my $command = "xwininfo -id $window -stats"; my $wininfo = `$command`; # e.g. ~20 lines of data including these: # Absolute upper-left X: 1591 # Absolute upper-left Y: 81 # Width: 942 # Height: 619 { $wininfo =~ m'^ *Absolute upper-left X: *(-?[0-9]+)$'m or die $wininfo; my $nominal_x = $1; $x = $nominal_x - $frame_left; # "can also get this from wininfo's -geometry line, but..." see note in /usr/bin/screenpager } { $wininfo =~ m'^ *Absolute upper-left Y: *(-?[0-9]+)$'m or die $wininfo; my $nominal_y = $1; #print "\$nominal_y=$nominal_y\n"; #if( $nominal_y == -1 ) #{ # use Zelea::WindowManagement (); # print Zelea::WindowManagement::title_from_xwininfo( $wininfo ); # print "\n"; #} $y = $nominal_y - $frame_top; # " # if( title_from_xwininfo( $wininfo ) =~ /^Xfe/ ) # { # $y = $nominal_y; # Xfe bug? per lay-file-browser # } # else # { # $y = $nominal_y - $frame_top; # " # } } { $wininfo =~ m'^ *Width: *([0-9]+)$'m or die $wininfo; my $nominal_width = $1; $width = $nominal_width + $frame_left + $frame_right; } { $wininfo =~ m'^ *Height: *([0-9]+)$'m or die $wininfo; my $nominal_height = $1; $height = $nominal_height + $frame_top + $frame_bottom; } return ( $wininfo, $x, $y, $width, $height ); } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _get_wm_state( $ ) { my $window = shift; my( $is_decorated, $is_fullscreen, $is_maximized_horz, $is_maximized_vert ); my $command = "xprop -id $window _NET_WM_STATE"; $_ = `$command`; # e.g. _NET_WM_STATE(ATOM) = _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_MAXIMIZED_VERT, _OB_WM_STATE_UNDECORATED $is_decorated = $_ !~ m'\b_OB_WM_STATE_UNDECORATED\b'; # only works for Openbox $is_fullscreen = m'\b_NET_WM_STATE_FULLSCREEN\b'; $is_maximized_horz = m'\b_NET_WM_STATE_MAXIMIZED_HORZ\b'; $is_maximized_vert = m'\b_NET_WM_STATE_MAXIMIZED_VERT\b'; return ( $is_decorated, $is_fullscreen, $is_maximized_horz, $is_maximized_vert ); } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _move( $$ ) { my $window = shift; my $direction_sign = shift; my( $wininfo, $x, $y, $width, $height ) = _get_wininfo( $window ); my( $screen, $screen_x0, $screen_x1 ) = _get_screen( $x, $width ); my $new_screen = $screen + $direction_sign; my( $new_x, $new_y ); require screenpager::Config_1; if( $direction_sign == 1 ) { $new_screen >= @screenpager::Config_1::screen and return 0; my $new_screen_config_HASH = $screenpager::Config_1::screen[$new_screen]; $new_x = $screen_x1 + 1 + $$new_screen_config_HASH{width} - $width; } else # -1 { $new_screen < 0 and return 0; $new_x = $screen_x0 - $width; } _reposition_window( $window, $wininfo, $new_x, 0 ); return 1; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _corrected_wmctrl_position_value( $ ); sub _reposition_window( $$$;$$$ ) { use Zelea::WindowManagement qw( title_from_xwininfo ); my $window = shift; my $wininfo = shift; my $x = _corrected_wmctrl_position_value shift; my $y = _corrected_wmctrl_position_value shift; my $width = _corrected_wmctrl_position_value shift; my $height = _corrected_wmctrl_position_value shift; if( $y != -1 && title_from_xwininfo($wininfo) =~ /^Xfe/ ) { my( $frame_left, $frame_right, $frame_top ) = _get_frame_strut( $window ); $y += $frame_top; # Xfe bug? per lay-file-browser } my $command = "wmctrl -i -r $window -e 0,$x,$y,$width,$height"; # gravity,x,y,width,height # but $x of -24 (for example) has no effect (treated as -1), so cannot cascade off top of screen #print "\$command=$command\n"; system $command and die 'unable to execute: ' . $command; } sub _corrected_wmctrl_position_value( $ ) { my $value = shift; if( defined $value ) { $value == -1 and $value = 0; } else { $value = -1; # wmctrl 'to leave the property unchanged' } return $value; } # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sub _shift_options() { my @specification; =pod =head1 OPTIONS =over 8 =cut push @specification, 'conserve'; =pod =item --B Specifies that the window is to move only if its current position in the cascade is invalid, because it overlaps another window too closely. Applies to C. =cut push @specification, 'help|?'; =pod =item --B Outputs a brief help message and exits. =cut push @specification, 'man'; =pod =item --B Outputs the full manual page and exits. =cut Getopt::Long::GetOptions( \%option, @specification ) or Pod::Usage::pod2usage( -verbose => 0 ); # and exits =pod =back =cut Pod::Usage::pod2usage( -verbose => 1 ) if defined $option{'help'}; # and exits Pod::Usage::pod2usage( -verbose => 2 ) if defined $option{'man'}; # and exits } __END__ =pod =head1 FILES Uses the same configuration file as screenpager, q.v. =head1 AUTHOR Michael Allan =cut