#!/usr/bin/python
# -*- coding: utf-8 -*-
# Screen rotation script for X60 tablet (http://luke.no-ip.org/x60tablet)
# HDAPS monitoring added by by Daniel Mendler <dmendler at wurzelteiler . de>

import os, sys, re, signal, time, errno, subprocess

    # HDAPS system files
hdapsPosFile = '/sys/devices/platform/hdaps/position'
    # HDAPS calibrate seems mostly useless, use manually stored calibration instead.
hdapsCalibrateFile = '/sys/devices/platform/hdaps/calibrate'
def hasHDAPS():
    return os.path.exists(hdapsPosFile) and os.path.exists(hdapsCalibrateFile)

    # All allowed rotations
rotations = ['normal', 'right', 'inverted', 'left']

    # Rotations to pick from when no specific rotation is given on the command line
    # Also controls the order in which rotations are chosen.
preferredRotations = rotations

    # Rotation to use when switched to tablet mode. If this is set to 'monitor', the
    # script will check the hdaps orientation and automatically rotate the screen.
if hasHDAPS():
    tabletMode = "monitor"
else:
    tabletMode = "left"

    # If true, this causes the monitor daemon to check the swivel state of the tablet
    # in addition to its orientation. 
monitorObeysSwivelState = True

    # Rotation to use when switched to normal laptop mode
laptopMode = "normal"
    
    # Keycodes to use for each rotation
    # 104 = pgup, 109 = pgdn, 105 = left, 106 = right, 103 = up, 108 = down
keyCodes = {
            'normal':   {'up': 103, 'dn': 108, 'lt': 105, 'rt': 106},
            'right':    {'up': 105, 'dn': 106, 'lt': 108, 'rt': 103},
            'inverted': {'up': 108, 'dn': 103, 'lt': 106, 'rt': 105},
            'left':     {'up': 106, 'dn': 105, 'lt': 103, 'rt': 108}
           }

    # Keyboard scan codes for arrow keys (you probably don't need to change these)
scanCodes = {'up': 0x71, 'dn': 0x6f, 'lt': 0x6e, 'rt': 0x6d}

    # Pid file for hdaps monitor daemon
hdapsPidFile = '/tmp/hdaps-rotate.pid'

tabletModeFile = "/sys/devices/platform/thinkpad_acpi/hotkey_tablet_mode"

    # name of display to be rotated as reported by xrandr (if None, it will be determined automatically)
displayName = None




## If a local xsetwacom is installed, it should probably take precedent (?)
if os.path.isfile('/usr/local/bin/xsetwacom'):
    xsetwacom = '/usr/local/bin/xsetwacom'
elif os.path.isfile('/usr/bin/xsetwacom'):
    xsetwacom = '/usr/bin/xsetwacom'
else:
    ## If it's not one of those two, just hope it's in the path somewhere.
    xsetwacom = 'xsetwacom'

xrandr = '/usr/bin/xrandr'



def main():
    global displayName
    setEnv()
    
    if displayName is None:
            displayName = guessDisplayName()
    
    if len(sys.argv) < 2:     # No rotation specified, just go to the next one in the preferred list
        cr = getCurrentRotation()
        if cr in preferredRotations:
            nextIndex = (preferredRotations.index(cr) + 1) % len(preferredRotations)
        else:
            nextIndex = 0
        next = preferredRotations[nextIndex]
    else:
        next = sys.argv[1]
        if not next in rotations:
            if next == "tablet":
                next = tabletMode
                #if tabletMode == 'monitor' and not hasHDAPS():
                    #sys.stderr.write("warning: HDAPS does not appear to be installed, skipping monitor mode\n")
                    #next = noHDAPSTabletMode
            elif next == "laptop":
                next = laptopMode
            elif next == 'monitor':
                pass
            else:
                sys.stderr.write("Rotation \"%s\" not allowed (pick from %s, tablet, laptop, or monitor)\n" % (next, ', '.join(rotations)))
                sys.stderr.write("""
    monitor -- means the script should run in the background and rotate the screen based on the
                          tablet's orientation (requires HDAPS).
    tablet  -- uses watever orientation is specified in the tabletMode variable in the script
    laptop  -- uses whatever orientation is specified in the laptopMode variable in the script
                          (tabletMode and laptopMode may be edited to suit your preferences)
""")
                sys.exit(-1)
    
    if next == 'monitor':
        #if not hasHDAPS():
            #sys.stderr.write("ERROR: HDAPS does not appear to be installed, can not start orientation monitor.\n")
            #sys.exit(-1)
        startHDAPSDaemon()
    else:
        stopHDAPSDaemon()
        print "Setting rotation to %s" % next
        setRotation(next)
        cr = getCurrentRotation()
        if cr != next:
            sys.stderr.write("Failed to change rotation! (is xrandr broken?)\n")



    ## Read and parse HDAPS position
def readHDAPSPos(file):
    try:
        f = open(file)
    except:
        raise Exception("Could not read HDAPS file %s! This is required for automatic orientation-based rotation." % file)
    l = f.read()
    f.close()
    return [int(x) for x in l[1:-2].split(',')]
    


    ## Signal handler
def quitHDAPS(a, b):
    os.unlink(hdapsPidFile)
    os._exit(0)


def getSwivelState():
    return open(tabletModeFile).read().strip()

    ## HDAPS monitoring loop
def monitorHDAPS():
    useHDAPS = hasHDAPS()
    signal.signal(signal.SIGTERM, quitHDAPS)
    
    if useHDAPS:
        centerX, centerY = readHDAPSPos(hdapsCalibrateFile)
        x = centerX
        y = centerY
        
    rot = getCurrentRotation()
    while True:
        time.sleep(0.1)
        newrot = rot
        
        ## check swivel state
        if monitorObeysSwivelState:
            swivel = getSwivelState()
            if swivel == '0':
                newrot = laptopMode
            elif swivel == '1':
                newrot = tabletMode
            
        ## Check orientation if HDAPS is available and orientation is not overridden by swivel state
        if useHDAPS and (newrot == 'monitor' or not monitorObeysSwivelState):
            newrot = tabletMode
            nx, ny = readHDAPSPos(hdapsPosFile)
            hRate = 0.1  ## Hysteresis
            x = x * (1. - hRate) + nx * hRate
            y = y * (1. - hRate) + ny * hRate
            dx = x - centerX
            dy = y - centerY
            if abs(dx) - abs(dy) > 30:
                if dx > 30:
                    newrot = 'right'
                elif dx < -30: 
                    newrot = 'left'
            elif abs(dy) - abs(dx) > 30:
                if dy > 30: 
                    newrot = 'inverted'
                elif dy < -30: 
                    newrot = 'normal'
            #print "%d, %d %s" % (dx, dy, newrot)
            
        if rot != newrot and newrot != 'monitor':
            setRotation(newrot)
            rot = newrot


    ## Start daemon by double forking
def startHDAPSDaemon():
    stopHDAPSDaemon()
    
    ## double fork
    if os.fork() > 0: os._exit(0)
    os.chdir('/')
    os.setsid()
    os.umask(0)
    pid = os.fork()
    if pid > 0:
        return
    setUID()
    
    f = open(hdapsPidFile, 'w')
    f.write('%d' % os.getpid())
    f.close()
    #os.chmod(hdapsPidFile, 0777)
    sys.stderr.write('HDAPS monitor started\n')
    monitorHDAPS()


    ## Check for pid file and stop daemon
def stopHDAPSDaemon():
  try:
      if os.path.exists(hdapsPidFile):
          f = open(hdapsPidFile)
          pid = f.read()
          f.close()
          os.kill(int(pid), signal.SIGTERM)
          sys.stderr.write('HDAPS monitor terminated\n')
  except OSError, error:
      if error.errno == errno.ESRCH:
          sys.stderr.write("Removing stale pid file\n")
          os.unlink(hdapsPidFile)
      else:
          sys.stderr.write('Failed to kill already running daemon!\n')
          print error
          sys.exit(1)


def runCmd(cmd):
    c = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    rval = c.wait()
    stdout = c.stdout.read()
    stderr = c.stderr.read()
    if rval != 0:
        sys.stderr.write("WARNING--Command failed (returned %d):\n  %s\n" % (rval, cmd))
        sys.stderr.write(stdout)
        sys.stderr.write(stderr)
    return (rval, stdout.split('\n'), stderr.split('\n'))

def getCurrentRotation():
    global displayName
    try:
        rrv = randrVersion()
        if rrv < '1.2':
            l = [s for s in runCmd(xrandr)[1] if re.match('Current rotation', s)]
            r = re.sub('Current rotation - ', '', l[0])
            return r.strip()
        elif rrv >= '1.2':
            l = runCmd(xrandr)[1] #"%s | grep 'LVDS connected' | gawk '{print $4}' | sed -e 's/(//'" % xrandr)
            l = [x for x in l if re.search(displayName + ' connected', x)][0]
            l = l.split(' ')[3]
            l = re.sub(r'\(', '', l)
            cr = l.strip()
            print "Current rotation: %s" % cr
            return cr
    except:
        sys.stderr.write("Can not determine current rotation, bailing out :(\n")
        raise

def guessDisplayName():
    try:
        l = runCmd(xrandr)[1] #"%s | grep 'LVDS connected' | gawk '{print $4}' | sed -e 's/(//'" % xrandr)
        l = [x for x in l if re.search(r'(LVDS.*|default) connected', x)][0]
        n = l.split(' ')[0]
        print "Guessed display name:", n
        return n
    except:
        sys.stderr.write("Can not determine current rotation, bailing out :(\n")
        raise


    ## Calls xrandr and xsetwacom, sets new keymap.
def setRotation(o):
    global displayName, xrandr
    if o == None:
        return
    if runCmd("%s --output %s --rotate %s" % (xrandr, displayName, o))[0] != 0:
        raise Exception("xrandr rotate command failed, bailing out.")
    wacomRots = {'normal': '0', 'left': '2', 'right': '1', 'inverted': '3'}
    #wacomRots = {'normal': 'NONE', 'left': 'CCW', 'right': 'CW', 'inverted': 'HALF'}
    tabletDevs = listDevices()
    if len(tabletDevs) < 1:
        sys.stderr.write('Did not find any tablet devices, only rotating screen.\n')
    for d in tabletDevs:
        if runCmd("%s set %s Rotate %s" % (xsetwacom, d, wacomRots[o]))[0] != 0:
            raise Exception("xsetwacom rotate command failed, bailing out")
    setKeymap(o)


    ## set process UID to the same as the user logged in on :0
def setUID():
    username = getUsername()
    if username == None:
        return
    uid = int(passwdRecord(username)[2])
    if os.getuid() != uid:
        try:
            os.setuid(uid)
        except:
            sys.stderr.write('Could not set process UID :(\n')

    ## Return the /etc/passwd record for user
def passwdRecord(user):
    fd = open('/etc/passwd', 'r')
    lines = fd.readlines()
    fd.close()
    match = filter(lambda s: re.match('%s:' % user, s), lines)
    return match[0].split(':')


    ## Get username logged in on :0
def getUsername():
    who = runCmd('/usr/bin/who')[1]
    
    ## Search for any line that looks like it mentions display :0
    l = filter(lambda s: re.search(r'\S+.+\:0(\.0)?\D*', s), who)
    if len(l) > 1:
        ## try to pick out the user logged in on the tty (there should be only one on :0)
        l2 = filter(lambda s: re.search(r'\btty\d+\b', s), l)
        if len(l2) < 1:
            sys.stderr.write("WARNING: Guessing X session user is [%s]\n" % l[0])
        else:
            l = l2
    if len(l) < 1:
        sys.stderr.write("Can not determine current X session username\n")
        return None
    
    ustr = (l[0].strip().split(' '))[0]
    return ustr


    ## Set up the X environmental variables needed for xrandr and xsetwacom
def setEnv():
    if os.environ.has_key('DISPLAY'):
        return  # DISPLAY is already set, don't mess with it.
    
    username = getUsername()
    if username == None:
        return
    print "Rotating screen for user %s" % username
    home = passwdRecord(username)[5]
    xauth = '%s/.Xauthority' % home
    os.environ['DISPLAY'] = ':0.0'
    os.environ['XAUTHORITY'] = xauth

    
def setKeymap(o):
    for sc in scanCodes.keys():
        os.system('sudo setkeycodes %x %d' % (scanCodes[sc], keyCodes[o][sc]))


def randrVersion():
    xrv = runCmd('%s -v' % xrandr)[1][0]
    xrv = re.sub(r'.*version ', '', xrv).strip()
    if len(xrv) < 1:
        raise Exception('Could not determine xrandr version!')
    return xrv

def listDevices():
    dev = runCmd("%s list dev" % xsetwacom)[1]
    dev = [re.sub(r'\s.*', '', s).strip() for s in dev]
    dev.remove('')
    return dev
      
main()
