Commits

Adam Coddington committed f001373 Merge

Merging in development line.

Comments (0)

Files changed (17)

 .*\.pyc
+build.*
+.*egg-info.*
+dist.*
+recursive-include push_to_talk_app/icons *
 # Push-to-talk for Linux
 
-This gnome panel application allows one to assign a key (by default *F12*), that will unmute one's microphone while pushed.
+This gnome application allows one to assign a key (by default *F12*) that will unmute one's microphone while pushed.
 
 ## Dependencies
 
  - pygtk
- - xlib
+ - Xlib
 
 On Ubuntu, this is as easy as running ``sudo apt-get install python-gtk2 python-xlib``.
 
 ## Installation
 
- 1. Run ``sudo ./install.sh``.
- 2. Restart gnome panel by either logging-out/logging-in, or running ``sudo killall gnome-panel`` (it will automatically restart).
- 3. Right-click on your gnome panel.
- 4. Select 'Add to panel'.
- 5. Add 'Push-to-talk'.
+ 1. Run ``sudo python setup.py install``.
+ 2. Run ``ptt``.
+ 
+If the application immediately closes with the message "You must log-out and log-in again for your system tray icon to appear.", log-out and log-back in again; a system settings change was required.
 
 ## Changing the Push-to-talk Key
 
- 1. Right-click on the 'TALK' icon in your gnome panel.
+ 1. Right-click on the microphone icon in your system tray.
  2. Click 'Set Key'.
  3. Press the key you'd like to use.
 

install.sh

-cp ./push_to_talk.py /usr/bin/
-cp ./push_to_talk.server /usr/lib/bonobo/servers/
+#!/usr/bin/python
+
+from push_to_talk_app.application import run_from_cmdline
+run_from_cmdline()

push_to_talk.py

-#!/usr/bin/python
-from __future__ import with_statement
-import dbus
-import gtk
-import gnomeapplet
-import gobject
-import logging
-from multiprocessing import Process, Queue
-import os
-import os.path
-import pygtk
-import subprocess
-from Xlib import display, X
-from Xlib.ext import record
-from Xlib.protocol import rq
-
-class KeyMonitor(object):
-    RELEASE = 0
-    PRESS = 1
-
-    UNMUTED = 0
-    MUTED = 1
-
-    F1_KEYCODE = 65470
-    F12_KEYCODE = 65481
-    """
-    Heavily borrowed from PyKeyLogger
-    """
-    def __init__(self, interface, pipe, return_pipe, test = False):
-        self.local_dpy = display.Display()
-        self.record_dpy = display.Display()
-        self.interface = interface
-        self.pipe = pipe
-        self.return_pipe = return_pipe
-
-        self.configured_keycode = None
-        self.state = KeyMonitor.MUTED
-
-        if test == True:
-            self.handler = self.print_action
-        else:
-            self.handler = self.interface_handler
-
-    @property
-    def configuration_file(self):
-        return os.path.expanduser("~/.push_to_talk_key")
-
-    def get_configured_keycode(self):
-        if not self.configured_keycode:
-            try:
-                with open(self.configuration_file, "r") as infile:
-                    keycode = infile.read()
-                    self.configured_keycode = int(keycode)
-            except:
-                self.configured_keycode = KeyMonitor.F12_KEYCODE
-        return self.configured_keycode
-
-    def set_configured_keycode(self, keycode):
-        logging.info("Setting keycode to %s" % keycode)
-        try:
-            with open(self.configuration_file, "w") as outfile:
-                outfile.write(str(keycode))
-                self.configured_keycode = None
-            return True
-        except Exception as e:
-            logging.exception(e)
-            return False
-
-    def set_state(self, state):
-        if self.state != state:
-            self.pipe.put(("MUTED", state, ))
-            if state == KeyMonitor.UNMUTED:
-                self.interface.unmute()
-            elif state == KeyMonitor.MUTED:
-                self.interface.mute()
-        self.state = state
-
-    def interface_handler(self, key, action):
-        configured = self.get_configured_keycode()
-        if action == KeyMonitor.PRESS and key == configured:
-            self.set_state(KeyMonitor.UNMUTED)
-        elif action == KeyMonitor.RELEASE and key == configured:
-            self.set_state(KeyMonitor.MUTED)
-
-    def print_action(self, key, action):
-        if action == KeyMonitor.RELEASE:
-            print "\n%s RELEASE" % key
-        elif action == KeyMonitor.PRESS:
-            print "\n%s PRESS" % key
-
-    def start(self):
-        self.ctx = self.record_dpy.record_create_context(
-            0,
-            [record.AllClients],
-            [{
-                    'core_requests': (0, 0),
-                    'core_replies': (0, 0),
-                    'ext_requests': (0, 0, 0, 0),
-                    'ext_replies': (0, 0, 0, 0),
-                    'delivered_events': (0, 0),
-                    'device_events': (X.KeyPress, X.KeyRelease, ),
-                    'errors': (0, 0),
-                    'client_started': False,
-                    'client_died': False,
-            }])
-
-        self.record_dpy.record_enable_context(self.ctx, self.processevents)
-        self.record_dpy.record_free_context(self.ctx)
-
-    def processevents(self, reply):
-        if reply.category != record.FromServer:
-            return
-        if reply.client_swapped:
-            return
-        if not len(reply.data) or ord(reply.data[0]) < 2:
-            # not an event
-            return
-        data = reply.data
-        while len(data):
-            event, data = rq.EventField(None).parse_binary_value(data, self.record_dpy.display, None, None)
-            if event.type == X.KeyPress:
-                self.keypressevent(event, KeyMonitor.PRESS)
-            elif event.type == X.KeyRelease:
-                self.keypressevent(event, KeyMonitor.RELEASE)
-
-    def keypressevent(self, event, action):
-        keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
-        if not self.return_pipe.empty():
-            logging.debug("Key info %s" % keysym)
-            data_object = self.return_pipe.get_nowait()
-            data_type = data_object[0]
-            logging.debug("Got data %s" % str(data_object))
-            if data_type == "SET":
-                self.set_configured_keycode(keysym)
-            self.handler(keysym, action)
-        else:
-            self.handler(keysym, action)
-
-class PulseAudioInterface(object):
-    verb = "Mute for all applications"
-
-    def mute(self):
-        index = 0
-        while True:
-            retval = subprocess.call([
-                    'pactl',
-                    'set-source-mute',
-                    str(index),
-                    '1',
-                ])
-            if retval != 0:
-                return
-            index = index + 1
-
-    def unmute(self):
-        index = 0
-        while True:
-            retval = subprocess.call([
-                    'pactl',
-                    'set-source-mute',
-                    str(index),
-                    '0',
-                ])
-            if retval != 0:
-                return
-            index = index + 1
-
-class SkypeInterface(object):
-    verb = "Mute only Skype"
-
-    def __init__(self):
-        self.bus = dbus.SessionBus()
-        self.configured = False
-
-    def configure(self):
-        try:
-            logging.debug("Configuring...")
-            self.outgoing = self.bus.get_object('com.Skype.API', '/com/Skype')
-            self.outgoing_channel = self.outgoing.get_dbus_method('Invoke')
-            self.configured = True
-
-            self.start()
-            logging.debug("Configured.")
-            return False
-        except:
-            # This happens if Skype is not available.
-            return True
-
-    def mute(self):
-        self._invoke("MUTE ON")
-
-    def unmute(self):
-        self._invoke("MUTE OFF")
-
-    def _invoke(self, message):
-        if not self.configured:
-            self.configure()
-        try:
-            self.outgoing_channel(message)
-        except:
-            self.configured = False
-
-    def start(self):
-        self._invoke('NAME PushToTalk')
-        self._invoke('PROTOCOL 5')
-
-class PushToTalk(gnomeapplet.Applet):
-    INTERVAL = 100
-    INTERFACES = [
-            PulseAudioInterface,
-            SkypeInterface,
-            ]
-
-    def __init__(self, applet, iid, get_keycode=False):
-        self.applet = applet
-        self.iid = iid
-        self.get_keycode = get_keycode
-
-        self.label = gtk.Label("...")
-        self.applet.add(self.label)
-        self.applet.show_all()
-
-        saved_interface = self.get_saved_interface()
-        self.audio_interface = saved_interface if saved_interface else self.INTERFACES[0]
-
-        self.do_setup_menu()
-        self.start()
-
-    def get_saved_interface(self):
-        try:
-            name = self.get_saved_interface_name()
-            for interface in self.INTERFACES:
-                if interface.__name__ == name:
-                    return interface
-        except:
-            pass
-        return None
-
-    @property
-    def preferences_file(self):
-        return os.path.expanduser(
-                    "~/.push_to_talk_saved",
-                )
-
-    def get_saved_interface_name(self):
-        with open(self.preferences_file, "r") as infile:
-            interface = infile.read()
-        return interface
-
-    def set_saved_interface_name(self, name):
-        with open(self.preferences_file, "w") as outfile:
-            outfile.write(name)
-        return name
-
-    def process(self, pipe, return_pipe, get_keycode):
-        monitor = KeyMonitor(
-                self.audio_interface(), 
-                pipe,
-                return_pipe,
-                test=True if get_keycode else False
-            )
-        monitor.start()
-
-    def read_incoming_pipe(self):
-        while not self.pipe.empty():
-            data_object = self.pipe.get_nowait()
-            data_type = data_object[0]
-            data = data_object[1]
-            logging.debug("Incoming Data -- %s" % str(data_object))
-            if data_type == "MUTED":
-                if data == KeyMonitor.UNMUTED:
-                    self.set_ui_talk()
-                elif data == KeyMonitor.MUTED:
-                    self.reset_ui()
-        return True
-
-    def reset_ui(self):
-        self.label.set_markup("<span>TALK</span>")
-
-    def set_ui_talk(self):
-        self.label.set_markup("<span foreground='#FF0000'>TALK</span>")
-
-    def reset_process(self):
-        logging.debug("Killing process...")
-        self.p.terminate()
-        self.start()
-
-    def start(self):
-        self.pipe = Queue()
-        self.return_pipe = Queue()
-
-        self.p = Process(
-                target=self.process,
-                args=(self.pipe, self.return_pipe, self.get_keycode, )
-            )
-        self.p.start()
-
-        logging.debug("Process spawned")
-        self.label.set_label("TALK")
-        gobject.timeout_add(PushToTalk.INTERVAL, self.read_incoming_pipe)
-
-    def set_key(self, *arguments):
-        logging.debug("Attempting to set key...")
-        self.label.set_markup("<span foreground='#00FF00'>PRESS</span>")
-        self.return_pipe.put(("SET", 1, ))
-        self.applet.show_all()
-
-    def change_interface(self, uicomponent, verb):
-        logging.debug("Setting to verb '%s'" % verb)
-        for interface in self.INTERFACES:
-            if interface.verb == verb:
-                logging.debug("Interface is set!")
-                self.set_saved_interface_name(interface.__name__)
-                self.audio_interface = interface
-        self.do_setup_menu()
-        self.reset_process()
-
-    def get_audio_xml(self):
-        xml_strings = {}
-        for interface in self.INTERFACES:
-            xml_strings[interface.verb] = "<menuitem verb=\"%s\" label=\"%s\" />" % (
-                                interface.verb,
-                                interface.verb,
-                            )
-        return xml_strings
-
-    def do_setup_menu(self):
-        verbs = [
-                ('Set Key', self.set_key, ),
-                ]
-        for interface in self.INTERFACES:
-            if self.audio_interface.verb != interface.verb:
-                verbs.append(
-                            (interface.verb, self.change_interface, ),
-                        )
-        self.applet.setup_menu(self.menu_xml, verbs, None)
-
-    @property
-    def menu_xml(self):
-        audio_xml = self.get_audio_xml()
-        start_xml = """<popup name="button3">
-            <menuitem name="SetKey" 
-                    verb="Set Key" 
-                    label="Set Key" 
-                    pixtype="stock" 
-                    pixname="gtk-preferences"/>
-            <separator />"""
-        for audio_source_verb, audio_item in audio_xml.items():
-            if self.audio_interface.verb == audio_source_verb:
-                del(audio_xml[audio_source_verb])
-        end_xml = "</popup>"
-        final_xml = start_xml + "".join(audio_xml.values()) + end_xml
-        logging.debug(final_xml)
-        return final_xml
-
-def push_to_talk_factory(applet, iid, get_keycode=False):
-    try:
-        PushToTalk(applet, iid, get_keycode)
-    except Exception as e:
-        logging.exception(e)
-    return gtk.TRUE
-
-pygtk.require('2.0')
-
-gobject.type_register(PushToTalk)
-
-logging.basicConfig(
-        filename=os.path.expanduser("~/.push_to_talk.log"),
-        level=logging.INFO
-    )
-
-if __name__ == "__main__":
-    logging.info("Starting via BonoboFactory.")
-    gnomeapplet.bonobo_factory(
-            "OAFIID:PushToTalk_Factory",
-            PushToTalk.__gtype__,
-            "hello",
-            "0",
-            push_to_talk_factory
-        )

push_to_talk.server

-<oaf_info>
-    <oaf_server iid="OAFIID:PushToTalk_Factory" type="exe" location="/usr/bin/push_to_talk.py">
-        <oaf_attribute name="repo_ids" type="stringv">
-            <item value="IDL:Bonobo/GenericFactory:1.0"/>
-            <item value="IDL:Bonobo/Unknown:1.0"/>
-        </oaf_attribute>
-        <oaf_attribute name="name" type="string" value="Push-to-talk Factory"/>
-        <oaf_attribute name="description" type="string" value="Allows one to use push-to-talk functionality in Linux"/>
-    </oaf_server>
-    <oaf_server iid="OAFIID:PushToTalk" type="factory" location="OAFIID:PushToTalk_Factory">
-        <oaf_attribute name="repo_ids" type="stringv">
-            <item value="IDL:GNOME/Vertigo/PanelAppletShell:1.0"/>
-            <item value="IDL:Bonobo/Control:1.0"/>
-            <item value="IDL:Bonobo/Unknown:1.0"/>
-        </oaf_attribute>
-        <oaf_attribute name="name" type="string" value="Push-to-talk"/>
-        <oaf_attribute name="description" type="string" value="Allows one to use push-to-talk functionality in Linux"/>
-        <oaf_attribute name="panel:category" type="string" value="Utility"/>
-        <oaf_attribute name="panel:icon" type="string" value="bug-buddy.png"/>
-    </oaf_server>
-</oaf_info>

push_to_talk_app/__init__.py

+# Copyright (c) 2012 Adam Coddington
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE 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
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+__author__ = 'Adam Coddington <me@adamcoddington.net>'
+__version__ = (1, 0, 1)
+
+def get_version():
+    return '.'.join(str(bit) for bit in __version__)

push_to_talk_app/application.py

+#!/usr/bin/python
+
+# Copyright (c) 2012 Adam Coddington
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE 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
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import logging
+from multiprocessing import Process, Queue
+from optparse import OptionParser
+import os
+import os.path
+
+from gi.repository import GObject, Gio, GLib
+import gtk
+
+from interfaces import SkypeInterface, PulseAudioInterface
+from key_monitor import KeyMonitor
+
+class PushToTalk(gtk.StatusIcon):
+    INTERVAL = 100
+    INTERFACES = [
+            PulseAudioInterface,
+            SkypeInterface,
+            ]
+
+    def __init__(self):
+        self.logger = logging.getLogger('push_to_talk_app')
+
+        gtk.StatusIcon.__init__(self)
+
+        self.configure_unity()
+
+        saved_interface = self.get_saved_interface()
+        self.audio_interface = saved_interface if saved_interface else self.INTERFACES[0]
+
+        self.do_setup_menu()
+        
+        self.reset_ui()
+        self.set_tooltip('Test')
+        self.set_visible(True)
+        self.start()
+
+    def configure_unity(self):
+        application_name = 'ptt'
+        schema = 'com.canonical.Unity.Panel'
+        key = 'systray-whitelist'
+        settings = Gio.Settings(schema)
+        value = settings.get_value(key)
+        if value:
+            if 'all' not in value and application_name not in value:
+                unpacked = value.unpack()
+                unpacked.append(application_name)
+                updated = GLib.Variant('as', unpacked)
+                settings.set_value(key, updated)
+                raise Exception("You must log-out and log-in again for your system tray icon to appear.")
+
+    def get_saved_interface(self):
+        try:
+            name = self.get_saved_interface_name()
+            for interface in self.INTERFACES:
+                if interface.__name__ == name:
+                    return interface
+        except:
+            pass
+        return None
+
+    @property
+    def preferences_file(self):
+        return os.path.expanduser(
+                    "~/.push_to_talk_saved",
+                )
+
+    def get_saved_interface_name(self):
+        with open(self.preferences_file, "r") as infile:
+            interface = infile.read()
+        return interface
+
+    def set_saved_interface_name(self, name):
+        with open(self.preferences_file, "w") as outfile:
+            outfile.write(name)
+        return name
+
+    def process(self, pipe, return_pipe):
+        monitor = KeyMonitor(
+                self.audio_interface(), 
+                pipe,
+                return_pipe,
+                test=False
+            )
+        monitor.start()
+
+    def read_incoming_pipe(self):
+        while not self.pipe.empty():
+            data_object = self.pipe.get_nowait()
+            data_type = data_object[0]
+            data = data_object[1]
+            self.logger.debug("Incoming Data -- %s" % str(data_object))
+            if data_type == "MUTED":
+                if data == KeyMonitor.UNMUTED:
+                    self.set_ui_talk()
+                elif data == KeyMonitor.MUTED:
+                    self.reset_ui()
+        return True
+
+    def reset_ui(self):
+        self.set_from_file(os.path.join(
+                os.path.dirname(__file__),
+                'icons/mute.png'
+            ))
+
+    def set_ui_talk(self):
+        self.set_from_file(os.path.join(
+                os.path.dirname(__file__),
+                'icons/talk.png'
+            ))
+
+    def set_ui_setkey(self):
+        self.set_from_file(os.path.join(
+                os.path.dirname(__file__),
+                'icons/setkey.png'
+            ))
+
+    def reset_process(self):
+        self.logger.debug("Killing process...")
+        self.p.terminate()
+        self.start()
+
+    def start(self):
+        self.pipe = Queue()
+        self.return_pipe = Queue()
+
+        self.p = Process(
+                target=self.process,
+                args=(self.pipe, self.return_pipe, )
+            )
+        self.p.start()
+
+        self.logger.debug("Process spawned")
+        GObject.timeout_add(PushToTalk.INTERVAL, self.read_incoming_pipe)
+
+    def set_key(self, *arguments):
+        self.logger.debug("Attempting to set key...")
+        self.set_ui_setkey()
+        self.return_pipe.put(("SET", 1, ))
+
+    def change_interface(self, action):
+        verb = action.get_name()
+        self.logger.debug("Setting to verb '%s'" % verb)
+        for interface in self.INTERFACES:
+            if interface.verb == verb:
+                self.logger.debug("Interface is set!")
+                self.set_saved_interface_name(interface.__name__)
+                self.audio_interface = interface
+        self.do_setup_menu()
+        self.reset_process()
+
+    def get_audio_xml(self):
+        xml_strings = {}
+        for interface in self.INTERFACES:
+            xml_strings[interface.verb] = "<menuitem action=\"%s\" />" % (
+                                interface.verb,
+                            )
+        return xml_strings
+
+    def do_setup_menu(self):
+        verbs = [(
+                'Menu',
+                None,
+                'Menu', 
+                ),
+                (
+                'SetKey', 
+                None, 
+                'Set Key', 
+                None, 
+                'Set key to use for push-to-talk', 
+                self.set_key, 
+            ),]
+        for interface in self.INTERFACES:
+            if self.audio_interface.verb != interface.verb:
+                verbs.append((
+                                interface.verb, 
+                                None, 
+                                interface.verb, 
+                                None, 
+                                '', 
+                                self.change_interface, 
+                        ),)
+
+        action_group = gtk.ActionGroup('Actions')
+        action_group.add_actions(verbs)
+
+        self.manager = gtk.UIManager()
+        self.manager.insert_action_group(action_group, 0)
+        self.manager.add_ui_from_string(self.menu_xml)
+        self.menu = self.manager.get_widget('/Menubar/Menu/SetKey').props.parent
+        self.connect('popup-menu', self.on_popup_menu)
+
+    def on_popup_menu(self, status, button, time):
+        self.menu.popup(None, None, None, button, time)
+
+    @property
+    def menu_xml(self):
+        audio_xml = self.get_audio_xml()
+        start_xml = """
+            <ui>
+                <menubar name="Menubar">
+                    <menu action="Menu">
+                        <menuitem action="SetKey"/>
+                        <separator/>
+            """
+        for audio_source_verb, audio_item in audio_xml.items():
+            if self.audio_interface.verb == audio_source_verb:
+                del(audio_xml[audio_source_verb])
+        end_xml = """
+                    </menu>
+                </menubar>
+            </ui>"""
+        final_xml = start_xml + "".join(audio_xml.values()) + end_xml
+        self.logger.debug(final_xml)
+        return final_xml
+
+def run_from_cmdline():
+    parser = OptionParser()
+    parser.add_option('-v', '--verbose', dest='verbose', action='store_true', default=False)
+    (opts, args, ) = parser.parse_args()
+
+    logging.basicConfig(
+            level=logging.DEBUG if opts.verbose else logging.WARNING
+        )
+
+    PushToTalk()
+    gtk.main()

push_to_talk_app/icons/mute.png

Added
New image

push_to_talk_app/icons/setkey.png

Added
New image

push_to_talk_app/icons/talk.png

Added
New image

push_to_talk_app/interfaces/__init__.py

+from skype import *
+from pulse_audio import *

push_to_talk_app/interfaces/pulse_audio.py

+# Copyright (c) 2012 Adam Coddington
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE 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
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import logging
+import subprocess
+
+__all__ = ['PulseAudioInterface', ]
+
+class PulseAudioInterface(object):
+    verb = "Mute for all applications"
+
+    INPUTS = {}
+
+    def __init__(self):
+        self.logger = logging.getLogger('push_to_talk_app.interfaces.pulse_audio')
+        super(PulseAudioInterface, self).__init__()
+        self.update_input_list()
+
+    def update_input_list(self):
+        self.INPUTS = {}
+
+        proc = subprocess.Popen(
+                [
+                    'pactl',
+                    'list',
+                    'short',
+                    'sources',
+                ], 
+                stdout = subprocess.PIPE
+            )
+        out, err = proc.communicate()
+        input_lines = out.split('\n')
+        for input_line in input_lines:
+            input_line = input_line.strip()
+            if not input_line:
+                break
+
+            details = input_line.split('\t')
+
+            index = details[0]
+            parsed = {
+                        'name': details[1],
+                        'module': details[2],
+                        'sound': details[3],
+                        'status': details[4],
+                    }
+            self.logger.debug("Found device %s" % parsed['name'])
+            self.INPUTS[index] = parsed
+
+    def mute(self):
+        for index in self.INPUTS.keys():
+            subprocess.call([
+                    'pactl',
+                    'set-source-mute',
+                    str(index),
+                    '1',
+                ])
+
+    def unmute(self):
+        for index in self.INPUTS.keys():
+            retval = subprocess.call([
+                    'pactl',
+                    'set-source-mute',
+                    str(index),
+                    '0',
+                ])

push_to_talk_app/interfaces/skype.py

+# Copyright (c) 2012 Adam Coddington
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE 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
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import dbus
+import logging
+
+__all__ = ['SkypeInterface', ]
+
+class SkypeInterface(object):
+    verb = "Mute only Skype"
+
+    def __init__(self):
+        self.logger = logging.getLogger('push_to_talk_app.interfaces.skype')
+        self.bus = dbus.SessionBus()
+        self.configured = False
+
+    def configure(self):
+        try:
+            self.logger.debug("Configuring...")
+            self.outgoing = self.bus.get_object('com.Skype.API', '/com/Skype')
+            self.outgoing_channel = self.outgoing.get_dbus_method('Invoke')
+            self.configured = True
+
+            self.start()
+            self.logger.debug("Configured.")
+            return False
+        except:
+            # This happens if Skype is not available.
+            return True
+
+    def mute(self):
+        self._invoke("MUTE ON")
+
+    def unmute(self):
+        self._invoke("MUTE OFF")
+
+    def _invoke(self, message):
+        if not self.configured:
+            self.configure()
+        try:
+            self.outgoing_channel(message)
+        except:
+            self.configured = False
+
+    def start(self):
+        self._invoke('NAME PushToTalk')
+        self._invoke('PROTOCOL 5')

push_to_talk_app/key_monitor.py

+#!/usr/bin/python
+
+# Copyright (c) 2012 Adam Coddington
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE 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
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+import os.path
+import logging
+
+from Xlib import display, X
+from Xlib.ext import record
+from Xlib.protocol import rq
+
+__all__ = ['KeyMonitor', ]
+
+class KeyMonitor(object):
+    RELEASE = 0
+    PRESS = 1
+
+    UNMUTED = 0
+    MUTED = 1
+
+    F1_KEYCODE = 65470
+    F12_KEYCODE = 65481
+    """
+    Heavily borrowed from PyKeyLogger
+    """
+    def __init__(self, interface, pipe, return_pipe, test = False):
+        self.logger = logging.getLogger('push_to_talk_app.key_monitor')
+        self.local_dpy = display.Display()
+        self.record_dpy = display.Display()
+        self.interface = interface
+        self.pipe = pipe
+        self.return_pipe = return_pipe
+
+        self.configured_keycode = None
+        self.state = KeyMonitor.MUTED
+
+        if test == True:
+            self.handler = self.print_action
+        else:
+            self.handler = self.interface_handler
+
+    @property
+    def configuration_file(self):
+        return os.path.expanduser("~/.push_to_talk_key")
+
+    def get_configured_keycode(self):
+        if not self.configured_keycode:
+            try:
+                with open(self.configuration_file, "r") as infile:
+                    keycode = infile.read()
+                    self.configured_keycode = int(keycode)
+            except:
+                self.configured_keycode = KeyMonitor.F12_KEYCODE
+        return self.configured_keycode
+
+    def set_configured_keycode(self, keycode):
+        self.logger.info("Setting keycode to %s" % keycode)
+        try:
+            with open(self.configuration_file, "w") as outfile:
+                outfile.write(str(keycode))
+                self.configured_keycode = None
+            return True
+        except Exception as e:
+            self.logger.exception(e)
+            return False
+
+    def set_state(self, state):
+        if self.state != state:
+            self.pipe.put(("MUTED", state, ))
+            if state == KeyMonitor.UNMUTED:
+                self.interface.unmute()
+            elif state == KeyMonitor.MUTED:
+                self.interface.mute()
+        self.state = state
+
+    def interface_handler(self, key, action):
+        configured = self.get_configured_keycode()
+        if action == KeyMonitor.PRESS and key == configured:
+            self.set_state(KeyMonitor.UNMUTED)
+        elif action == KeyMonitor.RELEASE and key == configured:
+            self.set_state(KeyMonitor.MUTED)
+
+    def print_action(self, key, action):
+        if action == KeyMonitor.RELEASE:
+            print "\n%s RELEASE" % key
+        elif action == KeyMonitor.PRESS:
+            print "\n%s PRESS" % key
+
+    def start(self):
+        self.ctx = self.record_dpy.record_create_context(
+            0,
+            [record.AllClients],
+            [{
+                    'core_requests': (0, 0),
+                    'core_replies': (0, 0),
+                    'ext_requests': (0, 0, 0, 0),
+                    'ext_replies': (0, 0, 0, 0),
+                    'delivered_events': (0, 0),
+                    'device_events': (X.KeyPress, X.KeyRelease, ),
+                    'errors': (0, 0),
+                    'client_started': False,
+                    'client_died': False,
+            }])
+
+        self.record_dpy.record_enable_context(self.ctx, self.processevents)
+        self.record_dpy.record_free_context(self.ctx)
+
+    def processevents(self, reply):
+        if reply.category != record.FromServer:
+            return
+        if reply.client_swapped:
+            return
+        if not len(reply.data) or ord(reply.data[0]) < 2:
+            # not an event
+            return
+        data = reply.data
+        while len(data):
+            event, data = rq.EventField(None).parse_binary_value(data, self.record_dpy.display, None, None)
+            if event.type == X.KeyPress:
+                self.keypressevent(event, KeyMonitor.PRESS)
+            elif event.type == X.KeyRelease:
+                self.keypressevent(event, KeyMonitor.RELEASE)
+
+    def keypressevent(self, event, action):
+        keysym = self.local_dpy.keycode_to_keysym(event.detail, 0)
+        if not self.return_pipe.empty():
+            self.logger.debug("Key info %s" % keysym)
+            data_object = self.return_pipe.get_nowait()
+            data_type = data_object[0]
+            self.logger.debug("Got data %s" % str(data_object))
+            if data_type == "SET":
+                self.set_configured_keycode(keysym)
+            self.handler(keysym, action)
+        else:
+            self.handler(keysym, action)
+from setuptools import setup
+
+from push_to_talk_app import get_version
+
+setup(
+    name='push_to_talk',
+    version=get_version(),
+    url='http://bitbucket.org/latestrevision/linux-push-to-talk/',
+    description='Push-to-talk functionality for Linux',
+    author='Adam Coddington',
+    author_email='me@adamcoddington.net',
+    classifiers=[
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Utilities',
+    ],
+    packages=['push_to_talk_app', 'push_to_talk_app.interfaces', ],
+    entry_points={
+            'console_scripts': [
+                'ptt = push_to_talk_app.application:run_from_cmdline',
+                ],
+        },
+    install_requires = [
+            #'pygtk',
+            'python-xlib',
+        ],
+    include_package_data=True
+)