You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

436 lines
14 KiB

import abc
import os
import argparse
import sys
import enum
import logging
__all__ = ['AbstractControl', 'GroupedControl', 'WrappingControl', 'ActionWrapperControl', 'Button']
logger = logging.getLogger(__name__)
class AbstractControl(metaclass=abc.ABCMeta):
"""
Base class for all modules
"""
def __init__(self):
"""
Creates a new control
"""
self.args = None
self.__name = 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 respond_to_ex(self, command: str):
"""
Responds to a non-cleaned user command
Namespaced commands still contain the namespace of this class, which has to be
removed from the command before it is passed on to respond_to()
:param command: The uncleaned command from the user
:return: bool Whether the displayed information is changed by the executed operations.
"""
logger.debug('%s.respond_to_ex: %s', self.__class__.__name__, command)
if command[0] == ':':
split_command = command.split(':', 2)
if len(split_command) == 3:
if split_command[1] != self.get_namespace():
logger.error('%s.respond_to_ex: Unsollicited command (mismatch %s <-> %s)', self.__class__.__name__, split_command[1], self.get_namespace())
return False
command = ':' + split_command[2]
logger.debug('%s.respond_to: %s', self.__class__.__name__, command)
return self.respond_to(command)
logger.warning('%s.respond_to_ex: Could not split into full command.', self.__class__.__name__)
else:
logger.debug('%s.respond_to: %s', self.__class__.__name__, command)
return self.respond_to(command)
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 get_namespace(self):
"""
:return: The class-specific part of the namespace to use for namespaced commands. It cannot contain colons
"""
return self.__class__.__name__
def set_name(self, name: str):
"""
Sets the namespace to use for namespaced commands
This method may be overridden to customize the namespace used for the class
It must call parent().set_name() with the desired name
:param name: The namespace to use for namespaced commands
"""
self.__name = name
logger.debug('%s.set_name: Set name to %s', self.__class__.__name__, name)
def set_name_ex(self, name: str):
"""
Sets the namespace to use for namespaced commands
This method must not be overridden
:param name: Namespace used by the object one up the hierarchy
"""
self.set_name('%s:%s' % (name, self.get_namespace()))
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
"""
if command[0] == ':':
command = self.__name + command
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):
if command[0] != ':':
return any([m.respond_to(command) for m in self.__modules if m.enabled])
split_command = command.split(':', maxsplit=2)
if len(split_command) == 3:
index = int(split_command[1])
return self.__modules[index].respond_to_ex(':' + split_command[2])
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 set_name(self, name: str):
super().set_name(name)
[m.set_name_ex('%s:%d' % (name, i)) for i, m in enumerate(self.__modules)]
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):
logging.debug('%s.respond_to: Passing command "%s" to child', self.__class__.__name__, command)
return self.child.respond_to_ex(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):
return self.child.periodic()
def load_state_ex(self, state):
self.child.load_state_ex(state)
def set_name(self, name: str):
super().set_name(name)
self.child.set_name_ex(name)
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