From 8a4d508aa0726d69217e736433bc42116540c5fb Mon Sep 17 00:00:00 2001 From: Lars Vierbergen Date: Wed, 5 Oct 2016 09:00:14 +0200 Subject: [PATCH] Rename pulsevolume manager to action manager, and add redshift and state saving support --- .gitignore | 1 + .xmobarrc | 5 +- action-manager/actions.py | 207 ++++++++++++++++++ action-manager/command.sh | 5 + action-manager/daemon.py | 73 ++++++ .../functions.py | 8 +- action-manager/start.sh | 36 +++ pulsevolume-manager/daemon.py | 93 -------- toggle-redshift.sh | 7 - xmonad-session-rc | 1 - xmonad.hs | 8 +- 11 files changed, 332 insertions(+), 112 deletions(-) create mode 100644 action-manager/actions.py create mode 100755 action-manager/command.sh create mode 100755 action-manager/daemon.py rename {pulsevolume-manager => action-manager}/functions.py (69%) create mode 100755 action-manager/start.sh delete mode 100755 pulsevolume-manager/daemon.py delete mode 100755 toggle-redshift.sh diff --git a/.gitignore b/.gitignore index 57ab12c..93e59b7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ /*.o /xmonad.errors /xmonad-x86_64-linux +/actionstate diff --git a/.xmobarrc b/.xmobarrc index ff7e5a6..1b93b5b 100644 --- a/.xmobarrc +++ b/.xmobarrc @@ -8,11 +8,10 @@ Config { font = "-*-Fixed-Bold-R-Normal-*-13-*-*-*-*-*-*-*" , Run Date "%a %b %_d %H:%M" "date" 10 , Run Battery ["-t", "", "-L", "10", "-H", "80", "-l", "red", "-h", "green", "--", "-O", "%", "-i", "%", "-o", "Bat: % / "] 10 , Run Locks - , Run PipeReader "/home/lars/.xmonad/volumepipe" "vol" - , Run Com "bash" ["-c", "if pidof redshift > /dev/null; then echo R; else echo r; fi"] "redshift" 1 + , Run PipeReader "/home/lars/.xmonad/actiondisplay" "action" , Run StdinReader ] , sepChar = "%" , alignSep = "}{" - , template = "%StdinReader% }{ %redshift% | %vol% | %locks% | %battery% | %cpu% | %memory% * %swap% | %date%" + , template = "%StdinReader% }{ %action% | %locks% | %battery% | %cpu% | %memory% * %swap% | %date%" } diff --git a/action-manager/actions.py b/action-manager/actions.py new file mode 100644 index 0000000..249d51a --- /dev/null +++ b/action-manager/actions.py @@ -0,0 +1,207 @@ +import functions +import subprocess +import abc +import sys +import os.path + +class AbstractControl(metaclass=abc.ABCMeta): + def __init__(self): + self.args = None + + @property + def visible(self): + return self.enabled + + @property + def enabled(self): + return True + + def configure(self, argument_parser): + pass + + def bind_arguments(self, args): + self.args = args + + def periodic(self): + pass + + def cleanup(self): + pass + + def respond_to(self, command): + return False + + def load_state(self, state): + pass + + def dump_state(self): + return None + + def create_pipe_command(self, command): + return 'echo {} | {}/command.sh {}'.format(command, os.path.abspath(sys.path[0]), os.path.abspath(self.args.command_pipe.name)) + + + +class VolumeControl(AbstractControl): + + @property + def muted(self): + return self._muted + + @muted.setter + def muted(self, muted): + if self._muted != muted: + try: + self._muted = muted + self._pactl('set-sink-mute', str(int(muted))) + except subprocess.CalledProcessError as e: + pass + + @property + def volume(self): + return self._volume + + @volume.setter + def volume(self, volume): + if self.muted: + self.muted = False + if self._volume != volume: + try: + self._volume = volume + self._pactl('set-sink-volume', str(volume)) + except subprocess.CalledProcessError as e: + pass + + def _pactl(self, command, arg): + for i in range(6): + subprocess.check_call(["pactl", command, str(i), arg]) + + + 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 self.action_bars(functions.create_bars(self.volume) if not self.muted else ' (mute) ') + + def action_bars(self, bars): + return ''.join([functions.action(self.create_pipe_command('=%d'%(i+1)), c, button=1) 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) + +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 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): + 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: + return self._redshift_proc.communicate()[1].replace("\n", ' ') + return None + + + @redshift_enabled.setter + def redshift_enabled(self, value): + if value == self.redshift_enabled: + return + if value: + self._redshift_proc = subprocess.Popen(['redshift', '-l', self.args.redshift_location, '-t', self.args.redshift_temperature], ) + else: + 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 functions.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} + +class ChildReaperControl(AbstractControl): + @property + def visible(self): + return False + + def periodic(self): + try: + os.wait3(os.WNOHANG) + except: + pass diff --git a/action-manager/command.sh b/action-manager/command.sh new file mode 100755 index 0000000..fff33ed --- /dev/null +++ b/action-manager/command.sh @@ -0,0 +1,5 @@ +#!/bin/bash +cat /dev/stdin | $(cat /dev/stdin > "$1")& +quit_proc="$!" +sleep 1 +kill "$quit_proc" diff --git a/action-manager/daemon.py b/action-manager/daemon.py new file mode 100755 index 0000000..93c0c2f --- /dev/null +++ b/action-manager/daemon.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +import argparse +import time +import actions +import pickle + +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) + + + +parser = argparse.ArgumentParser(description='Pulseaudio volume manager for xmobar') +parser.add_argument('output_pipe', type=argparse.FileType('w',bufsize=1)) +parser.add_argument('command_pipe', type=argparse.FileType('r',bufsize=1)) +parser.add_argument('--state-file', type=CreateFileType('r+b')) + +modules = [ + actions.QuitControl(), + actions.ChildReaperControl(), + actions.RedshiftControl(), + actions.VolumeControl(), +] + +[m.configure(parser.add_argument_group(m.__class__.__name__)) for m in modules] +args = parser.parse_args() +[m.bind_arguments(args) for m in modules] + +if args.state_file is not None and args.state_file.readable(): + try: + state = pickle.load(args.state_file) + print(state) + [m.load_state(state[m.__class__.__name__]) for m in modules if m.enabled and m.__class__.__name__ in state] + except Exception as e: + print(e) + + +def output_pipe(fd): + bars=' | '.join([str(m) for m in modules if m.visible]) + fd.writelines(bars+"\n") + +def command_pipe(fd): + command = str.rstrip(fd.readline()) + if len(command) == 0: + return False + return any([m.respond_to(command) for m in modules if m.enabled]) + + +try: + while True: + if command_pipe(args.command_pipe) or any([m.periodic() for m in modules if m.enabled]): + output_pipe(args.output_pipe) + else: + time.sleep(1) +finally: + if args.state_file is not None: + args.state_file.seek(0) + args.state_file.truncate() + state = {m.__class__.__name__: m.dump_state() for m in modules if m.enabled} + pickle.dump(state, args.state_file) + args.state_file.close() + + [m.cleanup() for m in modules if m.enabled] diff --git a/pulsevolume-manager/functions.py b/action-manager/functions.py similarity index 69% rename from pulsevolume-manager/functions.py rename to action-manager/functions.py index 796b4d7..c43c5e0 100644 --- a/pulsevolume-manager/functions.py +++ b/action-manager/functions.py @@ -1,5 +1,9 @@ import itertools import math + +def action(command, text, **kwargs): + return '{}'.format(command,' '+(' '.join(['{}={}'.format(k, v) for k,v in kwargs.items()])) if len(kwargs) else '', text) + 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))) @@ -15,7 +19,3 @@ def partial_bar(bar_size): return '-' return '/' -def action_bars(bars, control_pipe_name): - return ''.join([' '+control_pipe_name+'"`>'+c+'' for i,c in zip(range(len(bars)), bars)]) - - diff --git a/action-manager/start.sh b/action-manager/start.sh new file mode 100755 index 0000000..ec301ad --- /dev/null +++ b/action-manager/start.sh @@ -0,0 +1,36 @@ +#!/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 + $(echo "q" > $commandpipe)& + quit_proc=$! + sleep 1 + kill $quit_proc + rm "$commandpipe" +else + rm -f "$commandpipe" +fi + +mkfifo "$commandpipe" +$(sleep 1; echo "refresh" > $commandpipe)& +eval set -- "$ARGS" +exec "$(dirname "${BASH_SOURCE[0]}")/daemon.py" "$@" diff --git a/pulsevolume-manager/daemon.py b/pulsevolume-manager/daemon.py deleted file mode 100755 index 1b36d3b..0000000 --- a/pulsevolume-manager/daemon.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import time -import selectors -import enum -import math -import functions -import subprocess -parser = argparse.ArgumentParser(description='Pulseaudio volume manager for xmobar') -parser.add_argument('output_pipe', type=argparse.FileType('w',bufsize=1)) -parser.add_argument('command_pipe', type=argparse.FileType('r',bufsize=1)) -args = parser.parse_args() - -class AudioState: - _muted = False - _volume = 0 - - @property - def muted(self): - return self._muted - - @muted.setter - def muted(self, muted): - if self._muted != muted: - try: - self._muted = muted - self._pactl('set-sink-mute', str(int(muted))) - except subprocess.CalledProcessError as e: - pass - - @property - def volume(self): - return self._volume - - @volume.setter - def volume(self, volume): - if self.muted: - self.muted = False - if self._volume != volume: - try: - self._volume = volume - self._pactl('set-sink-volume', str(volume)) - except subprocess.CalledProcessError as e: - pass - - - def _pactl(self, command, arg): - for i in range(6): - subprocess.check_call(["pactl", command, str(i), arg]) - - - -audio_state = AudioState() - - -def output_pipe(fd): - bars = functions.action_bars(functions.create_bars(audio_state.volume) if not audio_state.muted else ' (mute) ', args.command_pipe.name) - fd.writelines(bars+"\n") - -def command_pipe(fd): - command = str.rstrip(fd.readline()) - if len(command) == 0: - return False - if command[0] == '=': - audio_state.volume = int(command[1:])*9000 - elif command == 'm1': - audio_state.muted = True - elif command == 'm0': - audio_state.muted = False - elif command == 'mt': - audio_state.muted = not audio_state.muted - elif command == '+': - audio_state.volume+=3000 - elif command == '-': - audio_state.volume-=3000 - elif command == 'r': - audio_state.volume=30000 - return True - - - -#sel = selectors.DefaultSelector() -#sel.register(args.output_pipe, selectors.EVENT_WRITE, data=output_pipe) -#sel.register(args.command_pipe, selectors.EVENT_READ, data=command_pipe) - -while True: - if command_pipe(args.command_pipe): - output_pipe(args.output_pipe) - else: - time.sleep(1) - - #for key, events in sel.select(): - # key.data(key.fileobj, events) diff --git a/toggle-redshift.sh b/toggle-redshift.sh deleted file mode 100755 index 408a3e7..0000000 --- a/toggle-redshift.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -if pidof redshift; then - killall redshift -else - redshift -l 51:5 -t 6500:3000& - disown -fi diff --git a/xmonad-session-rc b/xmonad-session-rc index 01b0b1f..bc50b7f 100644 --- a/xmonad-session-rc +++ b/xmonad-session-rc @@ -18,7 +18,6 @@ eval $(ssh-agent) gajim& dropbox start& keepass2 ~/Documents/passwords.kdbx& -redshift -l 51:5 -t 6500:3000 & icedove& mate-terminal -e ssh-add ~/.ssh/id_rsa& bash ~/.xmonad/battery-monitor.sh& diff --git a/xmonad.hs b/xmonad.hs index 881bde5..c726aab 100644 --- a/xmonad.hs +++ b/xmonad.hs @@ -56,7 +56,7 @@ workspaceManageHook = composeAll [ main = do xmobar_proc <- spawnPipe "xmobar" - pactl_proc <- spawn "for i in ~/.xmonad/volumepipe ~/.xmonad/volumecontrol; do rm -f $i && mkfifo $i;done && exec ~/.xmonad/pulsevolume-manager/daemon.py ~/.xmonad/volumepipe ~/.xmonad/volumecontrol" + spawn "exec ~/.xmonad/action-manager/start.sh ~/.xmonad/actiondisplay ~/.xmonad/actioncontrol --state-file ~/.xmonad/actionstate --redshift-enabled --redshift-location 51:5 --redshift-temperature 6000:2000" xmonad $ baseConfig { manageHook = (scratchpadManageHook $ W.RationalRect 0.0 0.0 1.0 0.5) <+> workspaceManageHook <+> fullscreenManageHook <+> manageDocks , logHook = dynamicLogWithPP xmobarPP @@ -96,11 +96,11 @@ main = do -- Azerty support , ((modMask baseConfig, xK_semicolon), sendMessage (IncMasterN (-1))) -- mute button - , ((0, 0x1008FF12), spawn "bash -c 'echo mt > ~/.xmonad/volumecontrol'") + , ((0, 0x1008FF12), spawn "echo mt | ~/.xmonad/action-manager/command.sh ~/.xmonad/actioncontrol") -- volumeup button - , ((0, 0x1008FF13), spawn "bash -c 'echo + > ~/.xmonad/volumecontrol'") + , ((0, 0x1008FF13), spawn "echo + | ~/.xmonad/action-manager/command.sh ~/.xmonad/actioncontrol") -- volumedown button - , ((0, 0x1008FF11), spawn "bash -c 'echo - > ~/.xmonad/volumecontrol'") + , ((0, 0x1008FF11), spawn "echo - | ~/.xmonad/action-manager/command.sh ~/.xmonad/actioncontrol") -- Media buttons , ((0, 0x1008ff14), spawn "clementine --play-pause") , ((0, 0x1008ff15), spawn "clementine --stop")