diff --git a/i3pystatus/alsa.py b/i3pystatus/alsa.py index 1c37753..f53815b 100644 --- a/i3pystatus/alsa.py +++ b/i3pystatus/alsa.py @@ -46,6 +46,11 @@ class ALSA(IntervalModule): alsamixer = None has_mute = True + on_upscroll = "increase_volume" + on_downscroll = "decrease_volume" + on_leftclick = "switch_mute" + on_rightclick = on_leftclick + def init(self): self.create_mixer() try: @@ -82,18 +87,15 @@ class ALSA(IntervalModule): "color": self.color_muted if muted else self.color, } - def on_leftclick(self): - self.on_rightclick() - - def on_rightclick(self): + def switch_mute(self): if self.has_mute: muted = self.alsamixer.getmute()[self.channel] self.alsamixer.setmute(not muted) - def on_upscroll(self): + def increase_volume(self, delta=None): vol = self.alsamixer.getvolume()[self.channel] - self.alsamixer.setvolume(min(100, vol + self.increment)) + self.alsamixer.setvolume(min(100, vol + (delta if delta else self.increment))) - def on_downscroll(self): + def decrease_volume(self, delta=None): vol = self.alsamixer.getvolume()[self.channel] - self.alsamixer.setvolume(max(0, vol - self.increment)) + self.alsamixer.setvolume(max(0, vol - (delta if delta else self.increment))) diff --git a/i3pystatus/bitcoin.py b/i3pystatus/bitcoin.py index 592ac25..d412830 100644 --- a/i3pystatus/bitcoin.py +++ b/i3pystatus/bitcoin.py @@ -59,6 +59,9 @@ class Bitcoin(IntervalModule): "price_down": "▼", } + on_leftclick = "handle_leftclick" + on_rightclick = "handle_rightclick" + _price_prev = 0 def _fetch_price_data(self): @@ -122,8 +125,8 @@ class Bitcoin(IntervalModule): "color": color, } - def on_leftclick(self): + def handle_leftclick(self): user_open(self.leftclick) - def on_rightclick(self): + def handle_rightclick(self): user_open(self.rightclick) diff --git a/i3pystatus/clock.py b/i3pystatus/clock.py index dc03bee..ba95678 100644 --- a/i3pystatus/clock.py +++ b/i3pystatus/clock.py @@ -28,6 +28,8 @@ class Clock(IntervalModule): color = "#ffffff" interval = 1 current_format_id = 0 + on_upscroll = ["scroll_format", 1] + on_downscroll = ["scroll_format", -1] def init(self): if self.format is None: @@ -78,8 +80,5 @@ class Clock(IntervalModule): if self.color != "i3Bar": self.output["color"] = self.color - def on_upscroll(self): - self.current_format_id = (self.current_format_id + 1) % len(self.format) - - def on_downscroll(self): - self.current_format_id = (self.current_format_id - 1) % len(self.format) + def scroll_format(self, step=1): + self.current_format_id = (self.current_format_id + step) % len(self.format) diff --git a/i3pystatus/cmus.py b/i3pystatus/cmus.py index c3b8490..60f13ee 100644 --- a/i3pystatus/cmus.py +++ b/i3pystatus/cmus.py @@ -49,6 +49,11 @@ class Cmus(IntervalModule): "stopped": "◾", } + on_leftclick = "playpause" + on_rightclick = "next_song" + on_upscroll = "next_song" + on_downscroll = "previous_song" + def _cmus_command(self, command): p = subprocess.Popen('cmus-remote --{command}'.format(command=command), shell=True, stdout=subprocess.PIPE, @@ -108,7 +113,7 @@ class Cmus(IntervalModule): "color": self.color } - def on_leftclick(self): + def playpause(self): status = self._query_cmus().get('status', '') if status == 'playing': self._cmus_command('pause') @@ -117,11 +122,8 @@ class Cmus(IntervalModule): if status == 'stopped': self._cmus_command('play') - def on_rightclick(self): + def next_song(self): self._cmus_command("next") - def on_upscroll(self): - self._cmus_command("next") - - def on_downscroll(self): + def previous_song(self): self._cmus_command("prev") diff --git a/i3pystatus/core/modules.py b/i3pystatus/core/modules.py index fc1e569..c9b66d9 100644 --- a/i3pystatus/core/modules.py +++ b/i3pystatus/core/modules.py @@ -1,12 +1,24 @@ from i3pystatus.core.settings import SettingsBase from i3pystatus.core.threading import Manager from i3pystatus.core.util import convert_position +from i3pystatus.core.command import run_through_shell class Module(SettingsBase): output = None position = 0 + settings = ('on_leftclick', "Callback called on left click (string)", + 'on_rightclick', "Callback called on right click (string)", + 'on_upscroll', "Callback called on scrolling up (string)", + 'on_downscroll', "Callback called on scrolling down (string)", + ) + + on_leftclick = None + on_rightclick = None + on_upscroll = None + on_downscroll = None + def registered(self, status_handler): """Called when this module is registered with a status handler""" @@ -23,31 +35,71 @@ class Module(SettingsBase): pass def on_click(self, button): + """ + Maps a click event (include mousewheel events) with its associated callback. + It then triggers the callback depending on the nature (ie type) of + the callback variable: + 1. if null callback, do nothing + 2. if it's a python function () + 3. if it's the name of a method of the current module (string) + + To setup the callbacks, you can set the settings 'on_leftclick', + 'on_rightclick', 'on_upscroll', 'on_downscroll'. + + For instance, you can test with: + :: + + status.register("clock", + format=[ + ("Format 0",'Europe/London'), + ("%a %-d Format 1",'Europe/Dublin'), + "%a %-d %b %X format 2", + ("%a %-d %b %X format 3", 'Europe/Paris'), + ], + on_leftclick= ["urxvtc"] , # launch urxvtc on left click + on_rightclick= ["scroll_format", 2] , # update format by steps of 2 + on_upscroll= [print, "hello world"] , # call python function print + log_level=logging.DEBUG, + ) + """ + + def split_callback_and_args(cb): + if isinstance(cb, list): + return cb[0], cb[1:] + else: + return cb, [] + + cb = None if button == 1: # Left mouse button - self.on_leftclick() + cb = self.on_leftclick elif button == 3: # Right mouse button - self.on_rightclick() + cb = self.on_rightclick elif button == 4: # mouse wheel up - self.on_upscroll() + cb = self.on_upscroll elif button == 5: # mouse wheel down - self.on_downscroll() + cb = self.on_downscroll + else: + self.logger.info("Button '%d' not handled yet." % (button)) + return + + if not cb: + self.logger.info("no cb attached") + return + else: + cb, args = split_callback_and_args(cb) + self.logger.debug("cb=%s args=%s" % (cb, args)) + + if callable(cb): + return cb(self) + elif hasattr(self, cb): + return getattr(self, cb)(*args) + else: + return run_through_shell(cb, *args) def move(self, position): self.position = position return self - def on_leftclick(self): - pass - - def on_rightclick(self): - pass - - def on_upscroll(self): - pass - - def on_downscroll(self): - pass - class IntervalModuleMeta(type): """Add interval setting to `settings` attribute if it does not exist.""" diff --git a/i3pystatus/core/util.py b/i3pystatus/core/util.py index 0f6de1a..c3b093e 100644 --- a/i3pystatus/core/util.py +++ b/i3pystatus/core/util.py @@ -431,7 +431,7 @@ def make_bar(percentage): def user_open(url_or_command): """Open the specified paramater in the web browser if a URL is detected, othewrise pass the paramater to the shell as a subprocess. This function - is inteded to bu used in on_leftclick()/on_rightclick() events. + is inteded to bu used in on_leftclick/on_rightclick callbacks. :param url_or_command: String containing URL or command """ diff --git a/i3pystatus/mail/__init__.py b/i3pystatus/mail/__init__.py index 330494c..ff69b50 100644 --- a/i3pystatus/mail/__init__.py +++ b/i3pystatus/mail/__init__.py @@ -38,6 +38,8 @@ class Mail(IntervalModule): hide_if_null = True email_client = None + on_leftclick = "open_client" + def init(self): for backend in self.backends: pass @@ -69,11 +71,8 @@ class Mail(IntervalModule): "color": color, } - def on_leftclick(self): + def open_client(self): if self.email_client: retcode, _, stderr = run_through_shell(self.email_client) if retcode != 0 and stderr: self.logger.error(stderr) - - def on_rightclick(self): - self.run() diff --git a/i3pystatus/modsde.py b/i3pystatus/modsde.py index b524c7c..ff5f9b0 100644 --- a/i3pystatus/modsde.py +++ b/i3pystatus/modsde.py @@ -35,6 +35,8 @@ class ModsDeChecker(IntervalModule): cj = None logged_in = False + on_leftclick = "open_browser" + def init(self): self.cj = http.cookiejar.CookieJar() self.opener = urllib.request.build_opener( @@ -94,5 +96,5 @@ class ModsDeChecker(IntervalModule): return True return False - def on_leftclick(self): + def open_browser(self): webbrowser.open_new_tab("http://forum.mods.de/bb/") diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index 441c3dd..8946f19 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -50,6 +50,10 @@ class MPD(IntervalModule): color = "#FFFFFF" text_len = 25 truncate_fields = ("title", "album", "artist") + on_leftclick = "switch_playpause" + on_rightclick = "next_song" + on_upscroll = on_rightclick + on_downscroll = "previous_song" def _mpd_command(self, sock, command): try: @@ -101,26 +105,20 @@ class MPD(IntervalModule): "color": self.color, } - def on_leftclick(self): + def switch_playpause(self): try: self._mpd_command(self.s, "%s" % ("play" if self._mpd_command(self.s, "status")["state"] in ["pause", "stop"] else "pause")) except Exception as e: pass - def on_rightclick(self): + def next_song(self): try: self._mpd_command(self.s, "next") except Exception as e: pass - def on_upscroll(self): - try: - self._mpd_command(self.s, "next") - except Exception as e: - pass - - def on_downscroll(self): + def previous_song(self): try: self._mpd_command(self.s, "previous") except Exception as e: diff --git a/i3pystatus/network.py b/i3pystatus/network.py index e476b7c..a20cc38 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -105,6 +105,7 @@ class Network(IntervalModule): color_down = "#FF0000" detached_down = True unknown_up = False + on_leftclick = "nm-connection-editor" def init(self): if self.interface not in netifaces.interfaces() and not self.detached_down: @@ -174,6 +175,3 @@ class Network(IntervalModule): "color": color, "instance": self.interface } - - def on_leftclick(self): - subprocess.Popen(["nm-connection-editor"]) diff --git a/i3pystatus/now_playing.py b/i3pystatus/now_playing.py index 40b1654..fbbccdf 100644 --- a/i3pystatus/now_playing.py +++ b/i3pystatus/now_playing.py @@ -54,6 +54,8 @@ class NowPlaying(IntervalModule): color = "#FFFFFF" old_player = None + on_leftclick = "playpause" + on_rightclick = "next_song" def find_player(self): players = [a for a in dbus.SessionBus().get_object("org.freedesktop.DBus", "/org/freedesktop/DBus").ListNames() if a.startswith("org.mpris.MediaPlayer2.")] @@ -111,8 +113,8 @@ class NowPlaying(IntervalModule): "color": self.color, } - def on_leftclick(self): + def playpause(self): self.get_player().PlayPause() - def on_rightclick(self): + def next_song(self): self.get_player().Next() diff --git a/i3pystatus/parcel.py b/i3pystatus/parcel.py index ea81331..f98e818 100644 --- a/i3pystatus/parcel.py +++ b/i3pystatus/parcel.py @@ -153,6 +153,7 @@ class ParcelTracker(IntervalModule): required = ("instance",) format = "{name}:{progress}" + on_leftclick = "open_browser" @require(internet) def run(self): @@ -166,5 +167,5 @@ class ParcelTracker(IntervalModule): "instance": self.name, } - def on_leftclick(self): + def open_browser(self): webbrowser.open_new_tab(self.instance.get_url()) diff --git a/i3pystatus/pianobar.py b/i3pystatus/pianobar.py index 5314ee9..2f24850 100644 --- a/i3pystatus/pianobar.py +++ b/i3pystatus/pianobar.py @@ -27,6 +27,11 @@ class Pianobar(IntervalModule): required = ("format", "songfile", "ctlfile") color = "#FFFFFF" + on_leftclick = "playpause" + on_rightclick = "next_song" + on_upscroll = "increase_volume" + on_downscroll = "decrease_volume" + def run(self): with open(self.songfile, "r") as f: contents = f.readlines() @@ -39,14 +44,14 @@ class Pianobar(IntervalModule): "color": self.color } - def on_leftclick(self): + def playpause(self): open(self.ctlfile, "w").write("p") - def on_rightclick(self): + def next_song(self): open(self.ctlfile, "w").write("n") - def on_upscroll(self): + def increase_volume(self): open(self.ctlfile, "w").write(")") - def on_downscroll(self): + def decrease_volume(self): open(self.ctlfile, "w").write("(") diff --git a/i3pystatus/pomodoro.py b/i3pystatus/pomodoro.py index 1d4317c..3755612 100644 --- a/i3pystatus/pomodoro.py +++ b/i3pystatus/pomodoro.py @@ -42,6 +42,9 @@ class Pomodoro(IntervalModule): break_duration = 5 * 60 long_break_duration = 15 * 60 + on_rightclick = "stop" + on_leftclick = "start" + def init(self): # state could be either running/break or stopped self.state = 'stopped' @@ -90,12 +93,12 @@ class Pomodoro(IntervalModule): 'color': color } - def on_leftclick(self): + def start(self): self.state = 'running' self.time = datetime.now() + timedelta(seconds=self.pomodoro_duration) self.breaks = 0 - def on_rightclick(self): + def stop(self): self.state = 'stopped' self.time = None diff --git a/i3pystatus/pulseaudio/__init__.py b/i3pystatus/pulseaudio/__init__.py index 669ce72..390caa2 100644 --- a/i3pystatus/pulseaudio/__init__.py +++ b/i3pystatus/pulseaudio/__init__.py @@ -50,6 +50,11 @@ class PulseAudio(Module, ColorRangeModule): bar_type = 'vertical' vertical_bar_width = 2 + on_rightclick = "switch_mute" + on_leftclick = "pavucontrol" + on_upscroll = "increase_volume" + on_downscroll = "decrease_volume" + def init(self): """Creates context, when context is ready context_notify_cb is called""" # Wrap callback methods in appropriate ctypefunc instances so @@ -155,10 +160,7 @@ class PulseAudio(Module, ColorRangeModule): volume_bar=volume_bar), } - def on_leftclick(self): - subprocess.Popen(["pavucontrol"]) - - def on_rightclick(self): + def switch_mute(self): if self.has_amixer: command = "amixer -q -D pulse sset Master " if self.currently_muted: @@ -167,12 +169,12 @@ class PulseAudio(Module, ColorRangeModule): command += 'mute' subprocess.Popen(command.split()) - def on_upscroll(self): + def increase_volume(self): if self.has_amixer: command = "amixer -q -D pulse sset Master %s%%+" % self.step subprocess.Popen(command.split()) - def on_downscroll(self): + def decrease_volume(self): if self.has_amixer: command = "amixer -q -D pulse sset Master %s%%-" % self.step subprocess.Popen(command.split()) diff --git a/i3pystatus/pyload.py b/i3pystatus/pyload.py index 7e58650..f960114 100644 --- a/i3pystatus/pyload.py +++ b/i3pystatus/pyload.py @@ -39,6 +39,7 @@ class pyLoad(IntervalModule): captcha_false = "" download_true = "Downloads enabled" download_false = "Downloads disabled" + on_leftclick = "open_webbrowser" def _rpc_call(self, method, data=None): if not data: @@ -83,5 +84,5 @@ class pyLoad(IntervalModule): "instance": self.address, } - def on_leftclick(self): + def open_webbrowser(self): webbrowser.open_new_tab(self.address) diff --git a/i3pystatus/reddit.py b/i3pystatus/reddit.py index f875de1..b5a0f73 100644 --- a/i3pystatus/reddit.py +++ b/i3pystatus/reddit.py @@ -61,6 +61,9 @@ class Reddit(IntervalModule): "no_mail": "", } + on_leftclick = "open_permalink" + on_click = "open_link" + _permalink = "" _url = "" @@ -133,8 +136,8 @@ class Reddit(IntervalModule): "color": color, } - def on_leftclick(self): + def open_permalink(self): user_open(self._permalink) - def on_rightclick(self): + def open_link(self): user_open(self._url) diff --git a/i3pystatus/spotify.py b/i3pystatus/spotify.py index 0d7d760..8e26435 100644 --- a/i3pystatus/spotify.py +++ b/i3pystatus/spotify.py @@ -23,6 +23,9 @@ class Spotify(Module): ("color", "color of the output"), ) + on_leftclick = "switch_playpause" + on_rightclick = "next_song" + def main_loop(self): """ Mainloop blocks so we thread it.""" self.player = Playerctl.Player() @@ -62,8 +65,8 @@ class Spotify(Module): "color": self.color } - def on_leftclick(self): + def switch_playpause(self): self.player.play_pause() - def on_rightclick(self): + def next_song(self): self.player.next() diff --git a/i3pystatus/text.py b/i3pystatus/text.py index 214337f..44b964b 100644 --- a/i3pystatus/text.py +++ b/i3pystatus/text.py @@ -1,5 +1,3 @@ -import subprocess - from i3pystatus import Module @@ -11,14 +9,10 @@ class Text(Module): settings = ( "text", ("color", "HTML color code #RRGGBB"), - ("cmd_leftclick", "Shell command to execute on left click"), - ("cmd_rightclick", "Shell command to execute on right click"), ) required = ("text",) color = None - cmd_leftclick = "test" - cmd_rightclick = "test" def init(self): self.output = { @@ -26,9 +20,3 @@ class Text(Module): } if self.color: self.output["color"] = self.color - - def on_leftclick(self): - subprocess.call(self.cmd_leftclick, shell=True) - - def on_rightclick(self): - subprocess.call(self.cmd_rightclick, shell=True) diff --git a/tests/test_callbacks.py b/tests/test_callbacks.py new file mode 100644 index 0000000..5b2fbd0 --- /dev/null +++ b/tests/test_callbacks.py @@ -0,0 +1,68 @@ +import unittest + + +from i3pystatus.core.modules import Module + + +class CallbacksMetaTest(unittest.TestCase): + + valid_msg_count = "hello world" + notmuch_config = "notmuch-config" + + @staticmethod + def send_clicks(module): + # simulate click events for buttons 1 to 10 + for button in range(1, 10): + module.on_click(button) + + class NoDefaultCounter(Module): + + counter = 0 + + def update_counter(self, step=1): + self.counter = self.counter + step + + class WithDefaultsCounter(NoDefaultCounter): + on_leftclick = "update_counter" + on_rightclick = ["update_counter", 2] + on_upscroll = ["callable_callback", "i3 is", "awesome"] + on_downscroll = ["update_counter", -1] + + def test_count_is_correct(self): + """Checks module works in the intended way, ie increase correctly + a counter""" + counter = self.NoDefaultCounter() + self.assertEqual(counter.counter, 0) + counter.update_counter(1) + self.assertEqual(counter.counter, 1) + counter.update_counter(2) + self.assertEqual(counter.counter, 3) + counter.update_counter(-2) + self.assertEqual(counter.counter, 1) + + CallbacksMetaTest.send_clicks(counter) + + # retcode, out, err = run_through_shell("notmuch --config=%s new" % (self.notmuch_config), enable_shell=True) + + # self.assertEqual(out.strip(), self.valid_output) + def test_callback_set_post_instanciation(self): + + counter = self.NoDefaultCounter() + + # set callbacks + counter.on_leftclick = "update_counter" + counter.on_rightclick = ["update_counter", 2] + counter.on_upscroll = [print, "i3 is", "awesome"] + counter.on_downscroll = ["update_counter", -1] + + self.assertEqual(counter.counter, 0) + counter.on_click(1) # left_click + self.assertEqual(counter.counter, 1) + counter.on_click(2) # middle button + self.assertEqual(counter.counter, 1) + counter.on_click(3) # right click + self.assertEqual(counter.counter, 3) + counter.on_click(4) # upscroll + self.assertEqual(counter.counter, 3) + counter.on_click(5) # downscroll + self.assertEqual(counter.counter, 2)