Initial commit

master
Lars Vierbergen 8 years ago
commit 8a899f53ea
  1. 1
      .gitignore
  2. 5
      command.sh
  3. 21
      daemon.py
  4. 5
      modules/__init__.py
  5. 153
      modules/application.py
  6. 56
      modules/caffeine.py
  7. 366
      modules/core.py
  8. 81
      modules/redshift.py
  9. 28
      modules/util.py
  10. 125
      modules/volume.py
  11. 29
      start.sh

1
.gitignore vendored

@ -0,0 +1 @@
__pycache__/

@ -0,0 +1,5 @@
#!/bin/bash
$(echo "$1" > "$2")&
quit_proc="$!"
sleep 1
kill -9 "$quit_proc"

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import logging
import modules
import modules.core
#logging.basicConfig(level=logging.DEBUG)
modules.Application(
modules.GroupedControl(
modules.CaffeineControl(),
modules.RedshiftControl(),
separator=''
),
modules.ActionWrapperControl(
modules.VolumeControl(),
action='pavucontrol',
buttons=modules.core.Button.RIGHT
),
).run()

@ -0,0 +1,5 @@
from .application import Application
from .core import GroupedControl, ActionWrapperControl
from .caffeine import CaffeineControl
from .redshift import RedshiftControl
from .volume import VolumeControl

@ -0,0 +1,153 @@
import argparse
import pickle
import signal
import logging
import time
import traceback
from .core import GroupedControl
from .util import QuitControl, ChildReaperControl
import os
import stat
logger = logging.getLogger(__name__)
class CreateFileType(argparse.FileType):
def __init__(self, mode='r', bufsize=-1, encoding=None, errors=None):
super().__init__(mode, bufsize, encoding, errors)
self.__mode = mode
self.__bufsize = bufsize
self.__encoding = encoding
self.__errors = errors
def __call__(self, string):
try:
return super().__call__(string)
except argparse.ArgumentTypeError as e:
return open(string, 'wb' if 'b' in self.__mode else 'w', self.__bufsize, self.__encoding, self.__errors)
class PipeFileType(argparse.FileType):
def __init__(self, *args, lazy=False, **kwargs):
super().__init__(*args, **kwargs)
self.lazy = lazy
def __call__(self, string):
try:
mode = os.stat(string).st_mode
if not stat.S_ISFIFO(mode):
raise argparse.ArgumentTypeError('%s is not a fifo' % strign)
except FileNotFoundError:
pass
if not self.lazy or string == '-':
return super().__call__(string)
else:
return LazyFile(string, self._mode, self._bufsize, self._encoding, self._errors)
class LazyFile:
def __init__(self, name, *args):
self.name = name
self.__args = args
self.__file = None
def __open(self):
if self.__file is None:
self.__file = open(self.name, *self.__args)
return self.__file
def close(self):
return self.__open().close()
@property
def closed(self):
return self.__open().closed
def fileno(self):
return self.__open().fileno()
def flush(self):
return self.__open().flush()
def isatty(self):
return self.__open().isatty()
def read(self, *a):
return self.__open().read(*a)
def readable(self):
return self.__open().readable()
def readline(self):
return self.__open().readline()
def readlines(self):
return self.__open().readlines()
def seek(self):
return self.__open().seek()
def seekable(self):
return self.__open().seekable()
def tell(self):
return self.__open().tell()
def truncate(self):
return self.__open().truncate()
def writable(self):
return self.__open().writable()
def write(self, *a):
return self.__open().write(*a)
def writelines(self, *a):
return self.__open().writelines(*a)
class Application(GroupedControl):
def __init__(self, *modules, **kwargs):
super().__init__(ChildReaperControl(), *modules, **kwargs)
def configure(self, argument_parser):
argument_parser.add_argument('output_pipe', type=PipeFileType('w', bufsize=1, lazy=True))
argument_parser.add_argument('command_pipe', type=PipeFileType('r', bufsize=1, lazy=True))
argument_parser.add_argument('--state-file', type=CreateFileType('r+b'))
super().configure(argument_parser)
def bind_arguments(self, args):
super().bind_arguments(args)
if args.state_file is not None and args.state_file.readable():
try:
state = pickle.load(args.state_file)
logger.info("Loaded state: %r" % state)
self.load_state_ex(state)
except:
traceback.print_exc()
def cleanup(self):
if self.args.state_file is not None:
self.args.state_file.seek(0)
self.args.state_file.truncate()
state = self.dump_state_ex()
logger.info("Dumped state: %r" % state)
pickle.dump(state, self.args.state_file)
self.args.state_file.close()
super().cleanup()
def respond_to(self, command):
if command == '':
return False
logger.info('Received command %s', command)
return super().respond_to(command)
def handle_signal(self, signal, tb):
raise Exception("Received signal %s"%signal)
def run(self):
parser = argparse.ArgumentParser(description='Action manager for xmobar')
self.configure(parser)
self.bind_arguments(parser.parse_args())
for sig in {signal.SIGHUP, signal.SIGINT, signal.SIGQUIT, signal.SIGTERM}:
signal.signal(sig, self.handle_signal)
try:
self.args.output_pipe.writelines(str(self)+"\n")
while True:
if self.respond_to(str.rstrip(self.args.command_pipe.readline())) or self.periodic():
self.args.output_pipe.writelines(str(self) + "\n")
else:
time.sleep(1)
except BaseException as e:
logger.exception('Received exception, shutting down')
finally:
self.cleanup()

@ -0,0 +1,56 @@
import subprocess
import logging
import time
from .core import AbstractControl, action
logger = logging.getLogger(__name__)
class CaffeineControl(AbstractControl):
def __init__(self):
super().__init__()
self.caffeine_enabled = False
self._activity_proc = None
self._activity_proc_lastrun = 0
@property
def enabled(self):
return self.args.caffeine_enabled
def configure(self, argument_parser):
argument_parser.add_argument('--caffeine-enabled', help='Use the caffeine module', action='store_true')
argument_parser.add_argument('--caffeine-timeout',
help='Time between user activity reports to xscreensaver (in seconds)',
type=int,
default=10)
def respond_to(self, command):
if command == 'caffeine':
self.caffeine_enabled = not self.caffeine_enabled
logger.info("Set caffeine enabled %r", self.caffeine_enabled)
return True
def __str__(self):
return action(self.create_pipe_command('caffeine'), 'C' if self.caffeine_enabled else 'c')
def periodic(self):
if self._activity_proc is not None:
if self._activity_proc.returncode is None:
self._activity_proc.poll()
else:
logger.debug("Reaped subprocess: %s", self._activity_proc)
self._activity_proc = None
if self.caffeine_enabled and self._activity_proc is None:
if self._activity_proc_lastrun + self.args.caffeine_timeout < time.time():
logger.debug("Poking screensaver")
self._activity_proc = subprocess.Popen(['xscreensaver-command', '-deactivate'], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
self._activity_proc_lastrun = time.time()
def dump_state(self):
return dict(caffeine_enabled=self.caffeine_enabled)
def load_state(self, state):
self.caffeine_enabled = state['caffeine_enabled']

@ -0,0 +1,366 @@
import abc
import os
import argparse
import sys
import enum
import logging
logger = logging.getLogger(__name__)
class AbstractControl(metaclass=abc.ABCMeta):
"""
Base class for all modules
"""
def __init__(self):
"""
Creates a new control
"""
self.args = None
@property
def visible(self):
"""
Determines if the module is visible in the command bar
:return: bool
"""
return self.enabled
@property
def enabled(self):
"""
Determines if the module is enabled.
Disabled modules do not receive any signals.
:return: bool
"""
return True
def configure(self, argument_parser: argparse.ArgumentParser):
"""
Configures the argument parser to accept extra arguments handled by this module
:param argument_parser: The argument parser to add arguments to
:return: void
"""
pass
def bind_arguments(self, args: argparse.Namespace):
"""
Binds arguments delivered by the argument parser to this module.
When overriding this method, the parent function always has to be called.
:param args: The Namespace object returned by ArgumentParser.parse_args()
:return: void
"""
self.args = args
def periodic(self):
"""
Called periodically during the runtime of the daemon.
Actions that are independent of user input should be handled here,
use respond_to() to handle user input.
Will only be called for modules that report to be enabled
:return: bool Whether the displayed information is changed by the executed operations.
"""
return False
def cleanup(self):
"""
Called during the shutdown of the daemon to clean up the module's resources.
Will only be called for modules that report to be enabled
:return: void
"""
pass
def respond_to(self, command: str):
"""
Responds to a user command
Actions that are dependent on user commands should be handled here,
use periodic() to handle periodic actions independent of user input
Will only be called for modules that report to be enabled
:param command: The command received from the user
:return: bool Whether the displayed information is changed by the executed operations.
"""
return False
def __str__(self):
"""
Creates the string representation of the module to show on the action bar
Will only be called for modules that report to be visible
:return: str
"""
return super().__str__()
def load_state(self, state):
"""
Loads state previously saved by this module
State is loaded once at daemon startup, if there is a statefile present.
Will only be called for modules that report to be enabled
:param state: Whatever was returned by dump_state() on the previous run
:return: void
"""
pass
def load_state_ex(self, state: dict):
"""
Loads all state previously saved
Usually this method filters the state of this module out of the global state to pass to load_state()
Controls that wrap other controls typically want to override this function to pass the whole
state to their children to avoid putting state in containers named after te wrapper.
:param state: Dictionary containing the saved state
:return: void
"""
if self.__class__.__name__ in state:
self.load_state(state[self.__class__.__name__])
def dump_state(self):
"""
Dumps current state of this module
State is dumped once at daemon shutdown, if there is a statefile present.
Will only be called for modules that report to be enabled
:return: any pickleable object representing the state of this module
"""
return None
def dump_state_ex(self):
"""
Dumps state of this module including unique identifying information for this module
Controls that wrap other controls typically want to override this function to dump the whole
state of their children to avoid putting state in containers named after te wrapper.
Will only be called for modules that report to be enabled
:return: dict
"""
return {self.__class__.__name__: self.dump_state()}
def create_pipe_command(self, command: str):
"""
Creates a shell command that will pass :command to the daemon through the controlpipe
:param command: The command to pass
:return: str Shell command that will pass the given command through the controlpipe
"""
return '{}/command.sh {} {}'.format(os.path.abspath(sys.path[0]), command,
os.path.abspath(self.args.command_pipe.name))
class GroupedControl(AbstractControl):
"""
Groups a set of modules into one module, separated by a given string
"""
def __init__(self, *modules, separator=' | '):
"""
Creates a new grouped control
:param modules: Modules to group in this module, in the order they should be displayed
:param separator: Separator string used inbetween two modules
"""
super().__init__()
self.__modules = modules
self.__separator = separator
def bind_arguments(self, args):
super().bind_arguments(args)
[m.bind_arguments(args) for m in self.__modules]
@property
def enabled(self):
return any([m.enabled for m in self.__modules])
@property
def visible(self):
return any([m.visible for m in self.__modules if m.enabled])
def configure(self, argument_parser):
[m.configure(argument_parser) for m in self.__modules]
def load_state_ex(self, state):
[m.load_state_ex(state) for m in self.__modules if m.enabled]
def cleanup(self):
[m.cleanup() for m in self.__modules if m.enabled]
def respond_to(self, command):
return any([m.respond_to(command) for m in self.__modules if m.enabled])
def periodic(self):
return any([m.periodic() for m in self.__modules if m.enabled])
def dump_state_ex(self):
data = dict()
for m in self.__modules:
if m.enabled:
data.update(m.dump_state_ex())
return data
def __passthrough_log(self, fn, s):
logger.debug('%s.%s(): %s', self.__class__.__name__, fn, s)
return s
def __str__(self):
return self.__separator.join([self.__passthrough_log('__str__', str(m)) for m in self.__modules if m.visible])
def action(command, text, **kwargs):
"""
Creates an xmobar action tag
:param command: The action (command to execute on click)
:param text: The text where the action is applied upon
:param kwargs: Extra parameters for the action tag (known case: button)
:return: str The xmobar action tag
"""
return '<action=`{}`{}>{}</action>'.format(command, ' ' + (
' '.join(['{}={}'.format(k, v) for k, v in kwargs.items()])) if len(kwargs) else '', text)
class WrappingControl(AbstractControl):
"""
Generic wrapper for a module
All method calls are passed through to the child module.
This wrapper does not affect the state, it is passed through cleanly
"""
def __init__(self, child_control: AbstractControl) -> None:
"""
Creates a new module wrapper
:param child_control: The child module to wrap
"""
super().__init__()
self.child = child_control
def cleanup(self):
self.child.cleanup()
def dump_state_ex(self):
return self.child.dump_state_ex()
def load_state(self, state):
self.child.load_state(state)
def respond_to(self, command):
return self.child.respond_to(command)
@property
def enabled(self):
return self.child.enabled
def dump_state(self):
self.child.dump_state()
def configure(self, argument_parser):
self.child.configure(argument_parser)
def bind_arguments(self, args):
super().bind_arguments(args)
self.child.bind_arguments(args)
@property
def visible(self):
return self.child.visible
def periodic(self):
self.child.periodic()
def load_state_ex(self, state):
self.child.load_state_ex(state)
def __str__(self):
return self.child.__str__()
class ActionWrapperControl(WrappingControl):
"""
Wraps a module output in an additional action
"""
def __init__(self, control: AbstractControl, action: str, buttons: str = None) -> None:
"""
Creates an action wrapper
:param control: The module to wrap
:param action: The shell command to execute when :buttons are pressed on the child module
:param buttons: Optionally, which buttons will trigger the shell command (May be a Button, or a number of OR-ed buttons)
"""
super().__init__(control)
self.__action = action
self.__buttons = buttons
def __str__(self):
if self.__buttons:
return action(self.__action, super().__str__(), button=self.__buttons)
else:
return action(self.__action, super().__str__())
@enum.unique
class Button(enum.Enum):
"""
A mouse button
A set of multiple mouse buttons can be constructed by OR-ing buttons together
e.g.: Button.LEFT|Button.RIGHT
"""
LEFT = '1'
MIDDLE = '2'
RIGHT = '3'
SCROLL_UP = '4'
SCROLL_DOWN = '5'
def __str__(self):
return self.value
def __or__(self, other):
"""
Add multiple buttons together
:param other: Button to add to this one
:return: An set of multiple buttons
"""
if isinstance(other, self.__class__):
return _Buttons([self, other])
return NotImplemented
class _Buttons(frozenset):
"""
Internal class that represents a set of Button enums and that can be further chained with itself of another Button
"""
def __str__(self):
return ''.join([str(b) for b in self])
def __or__(self, other):
"""
Add another Button of _Buttons set to the set
:param other: Button or _Buttons to add to this set of Buttons
:return: The superset of this set of buttons and the other set of buttons
"""
if isinstance(other, self.__class__):
return _Buttons(self.union(other))
elif isinstance(other, Button):
return _Buttons(self.union({other}))
return NotImplemented

@ -0,0 +1,81 @@
import subprocess
import logging
from .core import AbstractControl, action
logger = logging.getLogger(__name__)
class RedshiftControl(AbstractControl):
def __init__(self):
super().__init__()
self._redshift_proc = None
def configure(self, argument_parser):
argument_parser.add_argument('--redshift-enabled', help='Use the redshift module', action='store_true')
argument_parser.add_argument('--redshift-location', help='LAT:LON Your current location', type=str)
argument_parser.add_argument('--redshift-temperature',
help='DAY:NIGHT Color temperature to set at daytime/night', type=str)
def bind_arguments(self, args):
super().bind_arguments(args)
if self.enabled and not self.redshift_error_message:
self.redshift_enabled = True
@property
def enabled(self):
return self.args.redshift_enabled
@property
def redshift_enabled(self) -> bool:
return bool(self._redshift_proc)
@property
def redshift_error_message(self):
if self.enabled and not (self.args.redshift_location or self.args.redshift_temperature):
return "Missing parameter(s) --redshift-location and/or --redshift-temperature"
if self._redshift_proc is not None and self._redshift_proc.returncode is not None and self._redshift_proc.returncode != 0:
logger.error("Redshift process died unexpectedly: %s", self._redshift_proc.communicate())
return self._redshift_proc.communicate()[1].replace("\n", ' ')
return None
@redshift_enabled.setter
def redshift_enabled(self, value: bool) -> None:
if value == self.redshift_enabled:
return
if value:
logger.info("Starting redshift: -l %s -t %s", self.args.redshift_location, self.args.redshift_temperature)
self._redshift_proc = subprocess.Popen(
['redshift', '-l', self.args.redshift_location, '-t', self.args.redshift_temperature], stdin=subprocess.DEVNULL, stderr=subprocess.STDOUT)
else:
logger.info("Terminating running redshift process")
self._redshift_proc.terminate()
def periodic(self):
if self._redshift_proc:
self._redshift_proc.poll()
if self._redshift_proc.returncode is not None:
self._redshift_proc = None
return True
def respond_to(self, command):
if command == 'redshift':
self.redshift_enabled = not self.redshift_enabled
return True
return False
def cleanup(self):
self.redshift_enabled = False
if self._redshift_proc:
self._redshift_proc.wait()
def __str__(self):
if not self.redshift_error_message:
return action(self.create_pipe_command('redshift'), 'R' if self.redshift_enabled else 'r')
return 'E: ' + self.redshift_error_message
def load_state(self, state):
self.redshift_enabled = state['redshift_enabled']
def dump_state(self):
return {'redshift_enabled': self.redshift_enabled}

@ -0,0 +1,28 @@
import sys
import os.path
from .core import AbstractControl
class QuitControl(AbstractControl):
@property
def visible(self):
return False
def respond_to(self, command):
if command == 'q':
sys.exit(0)
elif command == 'refresh':
return True
class ChildReaperControl(AbstractControl):
@property
def visible(self):
return False
def periodic(self):
try:
os.wait3(os.WNOHANG)
except:
pass

@ -0,0 +1,125 @@
import subprocess
import logging
import math
from .core import AbstractControl, action, Button
logger = logging.getLogger(__name__)
class VolumeControl(AbstractControl):
def configure(self, argument_parser):
argument_parser.add_argument('--volume-enabled', help='Enable volume control', action='store_true')
@property
def enabled(self):
return self.args.volume_enabled
@property
def muted(self):
return self._muted
@muted.setter
def muted(self, muted):
if self._muted != muted:
logger.info("Setting muted to %s", muted)
try:
self._muted = muted
self._pactl('set-sink-mute', str(int(muted)))
except subprocess.CalledProcessError as e:
logger.exception("Error setting mute")
@property
def volume(self):
return self._volume
@volume.setter
def volume(self, volume):
if volume < 0:
logger.warning("Cannot set volume to %d, clamping to zero", volume)
volume = 0
if volume > 90000:
logger.warning("Cannot set volume to %d, clamping to 90000", volume)
volume = 90000
if self.muted:
self.muted = False
if self._volume != volume:
logger.info("Setting volume to %s", volume)
try:
self._volume = volume
self._pactl('set-sink-volume', str(volume))
except subprocess.CalledProcessError as e:
logger.exception("Error setting volume")
def _pa_get_sinks(self):
return [l.split(b'\t')[0].decode() for l in subprocess.check_output(["pactl", "list", "short", "sinks"], stdin=subprocess.DEVNULL,stderr=subprocess.DEVNULL).split(b'\n') if len(l) > 0]
def _pactl(self, command, arg):
for i in self._pa_get_sinks():
logger.debug("Calling pactl: %s %s %s", command, i, arg)
subprocess.check_call(["pactl", command, i, arg], stdin=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def __init__(self):
super().__init__()
self._muted = False
self._volume = 0
def respond_to(self, command):
if command[0] == '=':
self.volume = int(command[1:]) * 9000
elif command == 'm1':
self.muted = True
elif command == 'm0':
self.muted = False
elif command == 'mt':
self.muted = not self.muted
elif command == '+':
self.volume += 3000
elif command == '-':
self.volume -= 3000
elif command == 'r':
self.volume = 30000
else:
return False
return True
def __str__(self):
return action(
self.create_pipe_command('+'),
action(
self.create_pipe_command('-'),
self.action_bars(create_bars(self.volume) if not self.muted else ' (mute) '),
button=Button.SCROLL_DOWN
),
button=Button.SCROLL_UP
)
def action_bars(self, bars):
return ''.join([action(self.create_pipe_command('=%d' % (i + 1)), c, button=Button.LEFT) for i, c in
zip(range(len(bars)), bars)])
def load_state(self, state):
self.volume = state['volume']
self.muted = state['muted']
def dump_state(self):
return dict(volume=self.volume, muted=self.muted)
def create_bars(volume):
num_bars = float(volume) / 9000.0
return ('/' * math.floor(num_bars)) + partial_bar(num_bars - math.floor(num_bars)) + (
' ' * (10 - math.ceil(num_bars)))
def partial_bar(bar_size):
if bar_size == 0.0:
return ''
elif bar_size < 0.3:
return ' '
elif bar_size < 0.6:
return '.'
elif bar_size < 0.9:
return '-'
return '/'

@ -0,0 +1,29 @@
#!/bin/bash
ARGS="$@"
while [[ -n "$@" ]]; do
if [[ "${1:0:1}" != "-" ]]; then
if [[ -z "$volumepipe" ]]; then
volumepipe="$1"
elif [[ -z "$commandpipe" ]]; then
commandpipe="$1"
fi
fi
shift;
done
# volumepipe
if ! [[ -p "$volumepipe" ]]; then
rm -rf "$volumepipe"
mkfifo "$volumepipe"
fi
# volumecontrol
if ! [[ -p "$commandpipe" ]]; then
rm -rf "$commandpipe"
mkfifo "$commandpipe"
fi
eval set -- "$ARGS"
exec "$(dirname "${BASH_SOURCE[0]}")/daemon.py" "$@"
Loading…
Cancel
Save