Merge branches 'rscholer-online_module', 'schroeji-yaourt', 'richese-bg_commands', 'facetoe-praw_warning' and 'NiclasEriksen-master' into master

This commit is contained in:
enkore 2015-10-16 16:25:53 +02:00
7 changed files with 247 additions and 57 deletions

View File

@ -281,23 +281,94 @@ The global default action for all settings is ``None`` (do nothing),
but many modules define other defaults, which are documented in the but many modules define other defaults, which are documented in the
module reference. module reference.
The settings can be of different types, namely The values you can assign to these four settings can be divided to following
three categories:
- a list referring to a method of the module, e.g. .. rubric:: Member callbacks
These callbacks are part of the module itself and usually do some simple module
related tasks (like changing volume when scrolling, etc.). All available
callbacks are (most likely not) documented in their respective module
documentation.
For example the module :py:class:`.ALSA` has callbacks named ``switch_mute``,
``increase_volume`` and ``decrease volume``. They are already assigned by
default but you can change them to your liking when registering the module.
.. code:: python .. code:: python
status.register("clock", status.register("alsa",
on_leftclick=["scroll_format", 1]) on_leftclick = ["switch_mute"],
# or as a strings without the list
on_upscroll = "decrease_volume",
on_downscroll = "increase_volume",
# this will refresh any module by clicking on it
on_rightclick = "run",
)
``scroll_format`` is a method of the ``clock`` module, the ``1`` is Some callbacks also have additional parameters. Both ``increase_volume`` and
passed as a parameter and indicates the direction in this case. ``decrease_volume`` have an optional parameter ``delta`` which determines the
- as a special case of the above: a string referring to a method, no amount of percent to add/subtract from the current volume.
parameters are passed.
- a list where the first element is a callable and the following .. code:: python
elements are passed as arguments to the callable
- again a special case of the above: just a callable, no parameters status.register("alsa",
- a string which is run in a shell # all additional items in the list are sent to the callback as arguments
on_upscroll = ["decrease_volume", 2],
on_downscroll = ["increase_volume", 2],
)
.. rubric:: Python callbacks
These refer to to any callable Python object (most likely a function).
.. code:: python
# Note that the 'self' parameter is required and gives access to all
# variables of the module.
def change_text(self):
self.output["full_text"] = "Clicked"
status.register("text",
text = "Initial text",
on_leftclick = [change_text],
# or
on_rightclick = change_text,
)
You can also create callbacks with parameters.
.. code:: python
def change_text(self, text="Hello world!", color="#ffffff"):
self.output["full_text"] = text
self.output["color"] = color
status.register("text",
text = "Initial text",
color = "#00ff00",
on_leftclick = [change_text, "Clicked LMB", "#ff0000"],
on_rightclick = [change_text, "Clicked RMB"],
on_upscroll = change_text,
)
.. rubric:: External program callbacks
You can also use callbacks to execute external programs. Any string that does
not match any `member callback` is treated as an external command. If you want
to do anything more complex than executing a program with a few arguments,
consider creating an `python callback` or execute a script instead.
.. code:: python
status.register("text",
text = "Launcher?",
# open terminal window running htop
on_leftclick = "i3-sensible-terminal -e htop",
# open i3pystatus github page in firefox
on_rightclick = "firefox --new-window https://github.com/enkore/i3pystatus",
)
.. _hints: .. _hints:
@ -375,7 +446,7 @@ Refreshing the bar
The whole bar can be refreshed by sending SIGUSR1 signal to i3pystatus process. The whole bar can be refreshed by sending SIGUSR1 signal to i3pystatus process.
This feature is available only in standalone operation (:py:class:`.Status` was This feature is available only in standalone operation (:py:class:`.Status` was
created with `standalone=True` parameter). created with ``standalone=True`` parameter).
To find the PID of the i3pystatus process look for the ``status_command`` you To find the PID of the i3pystatus process look for the ``status_command`` you
use in your i3 config file. use in your i3 config file.

View File

@ -9,6 +9,21 @@ core Package
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`color` Module
-------------------
.. automodule:: i3pystatus.core.color
:members:
:undoc-members:
:show-inheritance:
:mod:`command` Module
---------------------
.. automodule:: i3pystatus.core.command
:members:
:undoc-members:
:show-inheritance:
:mod:`desktop` Module :mod:`desktop` Module
--------------------- ---------------------
@ -74,11 +89,4 @@ core Package
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
:mod:`color` Module
-------------------
.. automodule:: i3pystatus.core.color
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,23 +1,38 @@
# from subprocess import CalledProcessError import logging
from collections import namedtuple import shlex
import subprocess import subprocess
from collections import namedtuple
CommandResult = namedtuple("Result", ['rc', 'out', 'err']) CommandResult = namedtuple("Result", ['rc', 'out', 'err'])
def run_through_shell(command, enable_shell=False): def run_through_shell(command, enable_shell=False):
""" """
Retrieves output of command Retrieve output of a command.
Returns tuple success (boolean)/ stdout(string) / stderr (string) Returns a named tuple with three elements:
Don't use this function with programs that outputs lots of data since the output is saved * ``rc`` (integer) Return code of command.
in one variable * ``out`` (string) Everything that was printed to stdout.
* ``err`` (string) Everything that was printed to stderr.
Don't use this function with programs that outputs lots of data since the
output is saved in one variable.
:param command: A string or a list of strings containing the name and
arguments of the program.
:param enable_shell: If set ot `True` users default shell will be invoked
and given ``command`` to execute. The ``command`` should obviously be a
string since shell does all the parsing.
""" """
if not enable_shell and isinstance(command, str):
command = shlex.split(command)
returncode = None returncode = None
stderr = None stderr = None
try: try:
proc = subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=enable_shell) proc = subprocess.Popen(command, stderr=subprocess.PIPE,
stdout=subprocess.PIPE, shell=enable_shell)
out, stderr = proc.communicate() out, stderr = proc.communicate()
out = out.decode("UTF-8") out = out.decode("UTF-8")
stderr = stderr.decode("UTF-8") stderr = stderr.decode("UTF-8")
@ -27,7 +42,42 @@ def run_through_shell(command, enable_shell=False):
except OSError as e: except OSError as e:
out = e.strerror out = e.strerror
stderr = e.strerror stderr = e.strerror
logging.getLogger("i3pystatus.core.command").exception("")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
out = e.output out = e.output
logging.getLogger("i3pystatus.core.command").exception("")
return CommandResult(returncode, out, stderr) return CommandResult(returncode, out, stderr)
def execute(command, detach=False):
"""
Runs a command in background. No output is retrieved. Useful for running GUI
applications that would block click events.
:param command: A string or a list of strings containing the name and
arguments of the program.
:param detach: If set to `True` the program will be executed using the
`i3-msg` command. As a result the program is executed independent of
i3pystatus as a child of i3 process. Because of how i3-msg parses its
arguments the type of `command` is limited to string in this mode.
"""
if detach:
if not isinstance(command, str):
msg = "Detached mode expects a string as command, not {}".format(
command)
logging.getLogger("i3pystatus.core.command").error(msg)
raise AttributeError(msg)
command = ["i3-msg", "exec", command]
else:
if isinstance(command, str):
command = shlex.split(command)
try:
subprocess.Popen(command, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except OSError:
logging.getLogger("i3pystatus.core.command").exception("")
except subprocess.CalledProcessError:
logging.getLogger("i3pystatus.core.command").exception("")

View File

@ -1,6 +1,7 @@
from i3pystatus.core.settings import SettingsBase from i3pystatus.core.settings import SettingsBase
from i3pystatus.core.threading import Manager from i3pystatus.core.threading import Manager
from i3pystatus.core.util import convert_position from i3pystatus.core.util import convert_position
from i3pystatus.core.command import execute
from i3pystatus.core.command import run_through_shell from i3pystatus.core.command import run_through_shell
@ -47,33 +48,46 @@ class Module(SettingsBase):
def on_click(self, button): def on_click(self, button):
""" """
Maps a click event (include mousewheel events) with its associated callback. Maps a click event 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', Currently implemented events are:
'on_rightclick', 'on_upscroll', 'on_downscroll'.
For instance, you can test with: =========== ================ =========
:: Event Callback setting Button ID
=========== ================ =========
Left click on_leftclick 1
Right click on_rightclick 3
Scroll up on_upscroll 4
Scroll down on_downscroll 5
=========== ================ =========
The action is determined by the nature (type and value) of the callback
setting in the following order:
1. If null callback (``None``), no action is taken.
2. If it's a `python function`, call it and pass any additional
arguments.
3. If it's name of a `member method` of current module (string), call it
and pass any additional arguments.
4. If the name does not match with `member method` name execute program
with such name.
.. seealso:: :ref:`callbacks` for more information about
callback settings and examples.
:param button: The ID of button event received from i3bar.
:type button: int
:return: Returns ``True`` if a valid callback action was executed.
``False`` otherwise.
:rtype: bool
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 log_event(name, button, cb, args, action):
msg = "{}: button={}, cb='{}', args={}, type='{}'".format(
name, button, cb, args, action)
self.logger.debug(msg)
def split_callback_and_args(cb): def split_callback_and_args(cb):
if isinstance(cb, list): if isinstance(cb, list):
return cb[0], cb[1:] return cb[0], cb[1:]
@ -90,23 +104,25 @@ class Module(SettingsBase):
elif button == 5: # mouse wheel down elif button == 5: # mouse wheel down
cb = self.on_downscroll cb = self.on_downscroll
else: else:
self.logger.info("Button '%d' not handled yet." % (button)) log_event(self.__name__, button, None, None, "Unhandled button")
return False return False
if not cb: if not cb:
self.logger.info("no cb attached") log_event(self.__name__, button, None, None, "No callback attached")
return False return False
else: else:
cb, args = split_callback_and_args(cb) cb, args = split_callback_and_args(cb)
self.logger.debug("cb=%s args=%s" % (cb, args))
if callable(cb): if callable(cb):
cb(self) log_event(self.__name__, button, cb, args, "Python callback")
cb(self, *args)
elif hasattr(self, cb): elif hasattr(self, cb):
if cb is not "run": if cb is not "run":
log_event(self.__name__, button, cb, args, "Member callback")
getattr(self, cb)(*args) getattr(self, cb)(*args)
else: else:
run_through_shell(cb, *args) log_event(self.__name__, button, cb, args, "External command")
execute(cb, detach=True)
return True return True
def move(self, position): def move(self, position):

View File

@ -99,7 +99,10 @@ class SettingsBase(metaclass=SettingsBaseMeta):
raise ConfigMissingError( raise ConfigMissingError(
type(self).__name__, missing=exc.keys) from exc type(self).__name__, missing=exc.keys) from exc
if self.__name__.startswith("i3pystatus"):
self.logger = logging.getLogger(self.__name__) self.logger = logging.getLogger(self.__name__)
else:
self.logger = logging.getLogger("i3pystatus." + self.__name__)
self.logger.setLevel(self.log_level) self.logger.setLevel(self.log_level)
self.init() self.init()

View File

@ -28,6 +28,8 @@ class Reddit(IntervalModule):
* {message_author} * {message_author}
* {message_subject} * {message_subject}
* {message_body} * {message_body}
* {link_karma}
* {comment_karma}
""" """
@ -73,7 +75,7 @@ class Reddit(IntervalModule):
r = praw.Reddit(user_agent='i3pystatus') r = praw.Reddit(user_agent='i3pystatus')
if self.password: if self.password:
r.login(self.username, self.password) r.login(self.username, self.password, disable_warning=True)
unread_messages = sum(1 for i in r.get_unread()) unread_messages = sum(1 for i in r.get_unread())
if unread_messages: if unread_messages:
d = vars(next(r.get_unread())) d = vars(next(r.get_unread()))
@ -131,6 +133,14 @@ class Reddit(IntervalModule):
title = fdict["submission_title"][:(self.title_maxlen - 3)] + "..." title = fdict["submission_title"][:(self.title_maxlen - 3)] + "..."
fdict["submission_title"] = title fdict["submission_title"] = title
if self.username:
u = r.get_redditor(self.username)
fdict["link_karma"] = u.link_karma
fdict["comment_karma"] = u.comment_karma
else:
fdict["link_karma"] = ""
fdict["comment_karma"] = ""
full_text = self.format.format(**fdict) full_text = self.format.format(**fdict)
self.output = { self.output = {
"full_text": full_text, "full_text": full_text,

View File

@ -0,0 +1,32 @@
import re
from i3pystatus.core.command import run_through_shell
from i3pystatus.updates import Backend
class Yaourt(Backend):
"""
This module counts the available updates using yaourt.
By default it will only count aur packages. Thus it can be used with the pacman backend like this:
from i3pystatus.updates import pacman, yaourt
status.register("updates", backends = [pacman.Pacman(), yaourt.Yaourt()])
If you want to count both pacman and aur packages with this module you can set the variable
count_only_aur = False like this:
from i3pystatus.updates import yaourt
status.register("updates", backends = [yaourt.Yaourt(False)])
"""
def __init__(self, aur_only=True):
self.aur_only = aur_only
@property
def updates(self):
command = ["yaourt", "-Qua"]
checkupdates = run_through_shell(command)
if(self.aur_only):
return len(re.findall("^aur/", checkupdates.out, flags=re.M))
return checkupdates.out.count("\n")
Backend = Yaourt