Sample Fn-F7 script

From ThinkWiki
Revision as of 04:56, 24 November 2007 by Ordonnateur (Talk | contribs) (configuring acpi: note re /proc/acpi/ibm/hotkey)
Jump to: navigation, search

This guide will help you configure Fn-F7 key combination to toggle between internal, mirror, external, or both screens. This was tested on ThinkPad X60s running Fedora 8, please comment if it works or does not work for you.

Works like a charm on X61s with Xubuntu 7.10. Working with R60e with some modifications noted below.

configuring the virtual screen size

Add a "Virtual" statement to your /etc/X11/xorg.conf, the total resolution should be large enough to fit all your screens in the configuration you want, for example if you have 1600x1200 monitor to the left of your internal 1024x768 monitor, your total max resolution is 2624x1200 (See Xorg RandR 1.2 for more details):

Section "Screen"
       Identifier "Screen0"
       Device     "Videocard0"
       DefaultDepth     24
        SubSection "Display"
               Viewport   0 0
               Depth     24
               Virtual   2624 1200
       EndSubSection
EndSection

Restart X server at this point (i.e. logout and login).

configuring acpi

Create /etc/acpi/events/thinkpad.conf:

event=ibm/hotkey HKEY 00000080 00001007
action=/usr/local/sbin/thinkpad-fn-f7

or you may (eg Ubuntu 7.10) already have /etc/acpi/ibm-videobtn

# /etc/acpi/events/ibmvideobtn
# This is called when the user presses the video button. It is currently
# a placeholder.

in which case modify the line 'action=/bin/true' to run the script as above.

It may also be necessary to enable acpi events as per How to get special keys to work with (in root terminal) # echo enable,0xffff > /proc/acpi/ibm/hotkey [Note this command isn't persistent. some documentation at [[1]]

identify output devices

Note the names of your output devices as you will have to change EXTERNAL_OUTPUT and INTERNAL_OUTPUT to what xrandr shows, for example VGA and LVDS in this case:

$ xrandr -q
VGA connected 1600x1200+0+0 (normal left inverted right x axis y axis) 432mm x 324mm
...
LVDS connected (normal left inverted right x axis y axis)

The bash script

Create /usr/local/sbin/thinkpad-fn-f7, you can set EXTERNAL_LOCATION to one of: left, right, above, or below.

#!/bin/bash

# External output may be "VGA" or "VGA-0" or "DVI-0"
EXTERNAL_OUTPUT="VGA"
INTERNAL_OUTPUT="LVDS"
EXTERNAL_LOCATION="left"

# Figure out which user and X11 display to work on
# TODO there has to be a better way to do this?
X_USER=$(w -h -s | grep ":[0-9]" | head -1 | awk '{print $1}')
export DISPLAY=$(w -h -s | grep ":[0-9]" | head -1 | awk '{print $3}')

# Switch to X user if necessary
if [ "$X_USER" != "$USER" ]; then
       SU="su $X_USER -c"
fi

case "$EXTERNAL_LOCATION" in
       left|LEFT)
               EXTERNAL_LOCATION="--left-of $INTERNAL_OUTPUT"
               ;;
       right|RIGHT)
               EXTERNAL_LOCATION="--right-of $INTERNAL_OUTPUT"
               ;;
       top|TOP|above|ABOVE)
               EXTERNAL_LOCATION="--above $INTERNAL_OUTPUT"
               ;;
       bottom|BOTTOM|below|BELOW)
               EXTERNAL_LOCATION="--below $INTERNAL_OUTPUT"
               ;;
       *)
               EXTERNAL_LOCATION="--left-of $INTERNAL_OUTPUT"
               ;;
esac

# Figure out current state
INTERNAL_STATE=$($SU xrandr | grep ^$INTERNAL_OUTPUT | grep con | sed "s/.*connected //" | sed "s/(.*//")
EXTERNAL_STATE=$($SU xrandr | grep ^$EXTERNAL_OUTPUT | grep con | sed "s/.*connected //" | sed "s/(.*//")

if [ -z "$INTERNAL_STATE" ]; then
       STATE="external"
elif [ -z "$EXTERNAL_STATE" ]; then
       STATE="internal"
else
       INTERNAL_STATE=$(echo $INTERNAL_STATE | sed "s/[0-9]*x[0-9]*//")
       EXTERNAL_STATE=$(echo $EXTERNAL_STATE | sed "s/[0-9]*x[0-9]*//")
       if [ "$INTERNAL_STATE" = "$EXTERNAL_STATE" ]; then
               STATE="mirror"
       else
               STATE="both"
       fi
fi

function screen_external(){
       $SU "xrandr --output $INTERNAL_OUTPUT --off"
       $SU "xrandr --output $EXTERNAL_OUTPUT --auto"
}

function screen_internal(){
       $SU "xrandr --output $EXTERNAL_OUTPUT --off"
       $SU "xrandr --output $INTERNAL_OUTPUT --auto"
}

function screen_mirror(){
       $SU "xrandr --output $INTERNAL_OUTPUT --auto"
       $SU "xrandr --output $EXTERNAL_OUTPUT --auto --same-as $INTERNAL_OUTPUT"
}

function screen_both(){
       $SU "xrandr --output $INTERNAL_OUTPUT --auto"
       $SU "xrandr --output $EXTERNAL_OUTPUT --auto $EXTERNAL_LOCATION"
}

function screen_toggle(){
       case "$STATE" in
               internal)
                       screen_mirror
                       ;;
               mirror)
                       screen_external
                       ;;
               external)
                       screen_both
                       ;;
               both)
                       screen_internal
                       ;;
               *)
                       screen_internal
                       ;;
       esac
}

# What should we do?
DO="$1"
if [ -z "$DO" ]; then
       if [ $(basename $0) = "thinkpad-fn-f7" ]; then
               DO="toggle"
       fi
fi

case "$DO" in
       toggle)
               screen_toggle
               ;;
       internal)
               screen_internal
               ;;
       external)
               screen_external
               ;;
       mirror)
               screen_mirror
               ;;
       both)
               screen_both
               ;;
       status)
               echo "Current Fn-F7 state is: $STATE"
               echo
               echo "Attached monitors:"
               $SU xrandr | grep "\Wconnected" | sed "s/^/ /"
               ;;
       *)
               echo "usage: $0 <command>" >&2
               echo >&2
               echo "  commands:" >&2
               echo "          status" >&2
               echo "          internal" >&2
               echo "          external" >&2
               echo "          mirror" >&2
               echo "          both" >&2
               echo "          toggle" >&2
               echo >&2
               ;;
esac

set permissions and restart acpi

As root, or using sudo run the following commands,

$ sudo chmod 755 /usr/local/sbin/thinkpad-fn-f7
$ sudo service acpid restart
OR
$ sudo /etc/init.d/acpid restart

You should be ready to go, just press Fn-F7 to try.

Alternative script using .Xauthority rather than su

Use this script as an alternative

action=/usr/local/sbin/toggle-display.sh

for /etc/acpi/events/ibm-videobtn

#!/bin/bash
# usr/local/sbin/toggle-display.sh 
# based on /etc/acpi/screenblank.sh (Ubuntu 7.10)
#
# . /usr/share/acpi-support/power-funcs         # for getXuser
umask 022;
PATH="$PATH:/usr/bin/X11"
getXuser() {
       user=`finger| grep -m1 ":$displaynum " | awk '{print $1}'`
       if [ x"$user" = x"" ]; then
               user=`finger| grep -m1 ":$displaynum" | awk '{print $1}'`
       fi
       if [ x"$user" != x"" ]; then
               userhome=`getent passwd $user | cut -d: -f6`
               export XAUTHORITY=$userhome/.Xauthority
       else
               export XAUTHORITY=""
       fi
}
# end of getXuser from /usr/share/acpi-support/power-funcs
#
for x in /tmp/.X11-unix/*; do
   displaynum=`echo $x | sed s#/tmp/.X11-unix/X##`
   getXuser;
   if [ x"$XAUTHORITY" != x"" ]; then
       export DISPLAY=":$displaynum"
##     . /usr/share/acpi-support/screenblank.sh
       /usr/local/bin/toggle.py
   fi
done

Each user has an ~/.Xauthority file with one line for each X display containing a 'magic cookie' (Run $ xauth list to see the contents). The Xserver reads the record in ~/.Xauthority matching its display. When an X client application starts it also looks for that record and passes the magic cookie to the server. If it matches, the connection to the Xserver is allowed. The above example runs the python script below but could run a modified version of /usr/local/sbin/thinkpad-fn-f7 without the USER, and DISPLAY detection SU commands.

A Python Toggle script

This is a somewhat over-elaborate script which could be cut down when run by the /usr/local/sbin/toggle-display.sh script above. It was written to explore all the possibilities rather than economy of execution. The functions 'toggle_full', 'position', 'toggle_limited' and the OptionParser in 'main' may be omitted with suitable changes to function7. The appropriate outputs can be specified on the command line and it is not necessary to call it via acpi. eg you can run it as $ /usr/local/bin/toggle.py --help

#! /usr/bin/python
# -*- coding: utf-8 -*-
#
# stinkpad@blueyonder.co.uk	2007-11-23
"""Toggle internal and external displays (equivalent to ThinkPad Fn7)
Simple; cloned: on+off, on+on, off+on. 
Full; as simple plus xinerama: right, below,  left, above. 
Limited; as full but display will overlap if virtual screen is too small. 
Use 'xrandr -q' to determine output names. 
Further information: http://www.thinkwiki.org/wiki/Xorg_RandR_1.2 
"""
__usage__ = "usage: %prog [--help]|[[-i internal][-e external][-d displays]]"
__version__ = "toggle [djclark.co.uk 2007-11-23]"
#
# Output names; Intel: LVDS VGA TV TMDS-1 TMDS-2
#               ATI:   LVDS VGA-0 S-video DVI-0
LAPTOP = 'LVDS' 
MONITOR = 'VGA'
SEQUENCE = 'simple'
#
import sys
import os
import re
# "LVDS connected 1024x768+0+0 (normal left inverted right) 304mm x 228mm"
REGEX_OUTPUT = re.compile(r
	(?x)					# ignore whitespace
	^					# start of string
	(?P<output>[A-Za-z0-9\-]*)[ ] 		# LVDS VGA etc
	(?P<connect>(dis)?connected)[ ]		# dis/connected
	((					# a group
 	(?P<width>\d+)x 			# either 1024x768+0+0
	(?P<height>\d+)[+]  
 	(?P<horizontal>\d+)[+]
	(?P<vertical>\d+)
	)|[\D])					# or not a digit
	.*					# ignore rest of line
	)

# "Screen 0: minimum 320 x 200, current 1024 x 768, maximum 2624 x 1968"
REGEX_SCREEN = re.compile(r
	(?x) 				# ignore whitespace
	^				# start of string
	Screen[ ]			
	(?P<screen>\d)[: ]+
	minimum[ ]
	(?P<minWidth>\d+)[ x]+
	(?P<minHeight>\d+)[, ]+
	current[ ]+
	(?P<curWidth>\d+)[ x]+
	(?P<curHeight>\d+)[, ]+
	maximum[ ]+
	(?P<maxWidth>\d+)[ x]+
	(?P<maxHeight>\d+)
	)	
def toggle_simple(d0, d1):
   """Toggle display states: on+off, on+on, off+on"""
   if d1['connect'] == 'disconnected': 	# external unplugged
       return ('auto','off',) 		#     switch off external
   if d1['width'] is 0: 			# external off
       return ('auto','auto',) 		#     both on
   if d0['width'] is 0: 			# laptop off
       return ('auto','off',) 		#    laptop on
   return ('off','auto',) 			# both on, laptop off
def toggle_full(d0, d1):
   """Toggle display states: 1+0, 1+1, 0+1, 1+E, 1+S, 1+W, 1+N""" 
   if d1['connect'] == 'disconnected': 	# external unplugged
       return ('auto','off',) 		#     switch off external
   place = '--%s ' + d0['output']
   if d1['width'] == 0: 			# external off
       return ('auto','auto',place%'same-as') 	#     external on
   if d0['width'] == 0: 			# laptop off
       return ('auto','off',) 		#    laptop on
   if d1['horizontal'] > 0: 			# external to right
       return ('auto','auto',place%'below') 	#     make below
   if d1['vertical'] > 0: 			# external below
       return ('auto','auto',place%'left-of') 	#     make left
   if d0['horizontal'] > 0: 			# external left
       return ('auto','auto',place%'above') 	#     make above
   if d0['vertical'] > 0: 			# external above
       return ('off','auto',)  		#     laptop off
   return ('auto','auto',place%'right-of') 	# is same, make right
def position(orientation, da, db, screen):
   """Calculate offset position of second display"""
   p = 'auto --pos %sx%s'
   if orientation == 'V':
       if (da['height'] + db['height']) <= screen['maxHeight']:
           return p%(0, da['height'])
       return p%(0, screen['maxHeight'] - db['height'])
   else:
       if (da['width'] + db['width']) <= screen['maxWidth']:
           return p%(da['width'],0)
       return p%(screen['maxWidth'] - db['width'],0)
def toggle_limited(d0, d1, sz):
   """Toggle display states (overlapped): 1+0,1+1,0+1,1+E,1+S,1+W,1+N"""
   if d1['connect'] == 'disconnected': 	# external unplugged
       return ('auto','off') 			#     switch off external
   if d1['width'] == 0: 				# external off
       return ('auto --pos 0x0','auto --pos 0x0') 	#     both on
   if d0['width'] == 0: 				# laptop off
       return ('auto --pos 0x0','off') 		#     laptop on
   if d1['horizontal'] > 0: 				# external to right
       return ('auto --pos 0x0',position('V',d0,d1,sz)) #     put *below
   if d1['vertical'] > 0: 				# external below
       return (position('H',d1,d0,sz),'auto --pos 0x0') #     put *left
   if d0['horizontal'] > 0: 				# external left
       return (position('V',d1,d0,sz),'auto --pos 0x0') #     put *above
   if d0['vertical'] > 0: 				# external above
       return ('off','auto --pos 0x0')  		#     laptop off
   return ('auto --pos 0x0',position('H',d0,d1,sz)) 	# both, put*right
def function7(disp0=LAPTOP, disp1=MONITOR, seq=SEQUENCE):
   """Use xrandr to read current display state and change state"""
   for line in os.popen('xrandr -q').read().splitlines():
       if line.startswith(disp0,0) :
           d0_state = REGEX_OUTPUT.match(line).groupdict()
       elif line.startswith(disp1,0):
           d1_state = REGEX_OUTPUT.match(line).groupdict()
       elif line.startswith('Screen',0):
           screen_size = REGEX_SCREEN.match(line).groupdict()
       else:
           pass
   for i in ('width','height','horizontal','vertical'):
       try:
           d0_state[i] = int(d0_state[i])
       except TypeError:
           d0_state[i] = 0
       except UnboundLocalError:
           return 'Internal Display output %s not found'% disp0
       try:
           d1_state[i] = int(d1_state[i])
       except TypeError:
           d1_state[i] = 0
       except UnboundLocalError:
           return 'External Display output %s not found'% disp1
   for i in screen_size.keys():
       try:
           screen_size[i] = int(screen_size[i])
       except TypeError:
           screen_size[i] = 0
   #
   toggle = toggle_simple
   xrandr ='xrandr --output '+disp0+' --%s --output '+disp1+' --%s %s'
   if seq == 'full':
       toggle = toggle_full
   if seq == 'limited':
       toggle = toggle_limited
       xrandr ='xrandr --output '+disp0+' --%s --output '+disp1+' --%s'
       os.popen(xrandr % toggle(d0_state, d1_state, screen_size))
   else:
       os.popen(xrandr % toggle(d0_state, d1_state))
   return None
def main():
   """ Command line options """
   global LAPTOP,MONITOR,SEQUENCE
   from optparse import OptionParser
   p = OptionParser(usage=__usage__, version=__version__, description=__doc__)  
   p.set_defaults(internal=LAPTOP, external=MONITOR, displays=SEQUENCE)
   p.set_defaults(separator='comma', column=0)
   p.add_option('-i','--internal', dest="internal", metavar=LAPTOP,
	help="internal display")
   p.add_option('-e','--external', dest="external", metavar=MONITOR,
 	help="external display")
   p.add_option('-d','--displays', dest="displays", action="store",
	choices=('simple', 'limited', 'full'), metavar=SEQUENCE,
	help='simple/limited/full')
   (opt, args) = p.parse_args()
   failed = function7(opt.internal, opt.external, opt.displays)
   if failed:
       print '\n'+failed+'\n'
       print os.popen('xrandr -q').read()
#
if __name__ == '__main__': 	#only when run from cmd line
   main()