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
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
.. code:: python
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.
status.register("clock",
on_leftclick=["scroll_format", 1])
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.
``scroll_format`` is a method of the ``clock`` module, the ``1`` is
passed as a parameter and indicates the direction in this case.
- as a special case of the above: a string referring to a method, no
parameters are passed.
- a list where the first element is a callable and the following
elements are passed as arguments to the callable
- again a special case of the above: just a callable, no parameters
- a string which is run in a shell
.. code:: python
status.register("alsa",
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",
)
Some callbacks also have additional parameters. Both ``increase_volume`` and
``decrease_volume`` have an optional parameter ``delta`` which determines the
amount of percent to add/subtract from the current volume.
.. code:: python
status.register("alsa",
# 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:
@ -375,7 +446,7 @@ Refreshing the bar
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
created with `standalone=True` parameter).
created with ``standalone=True`` parameter).
To find the PID of the i3pystatus process look for the ``status_command`` you
use in your i3 config file.

View File

@ -9,6 +9,21 @@ core Package
:undoc-members:
: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
---------------------
@ -74,11 +89,4 @@ core Package
:undoc-members:
:show-inheritance:
:mod:`color` Module
-------------------
.. automodule:: i3pystatus.core.color
:members:
:undoc-members:
:show-inheritance:

View File

@ -1,23 +1,38 @@
# from subprocess import CalledProcessError
from collections import namedtuple
import logging
import shlex
import subprocess
from collections import namedtuple
CommandResult = namedtuple("Result", ['rc', 'out', 'err'])
def run_through_shell(command, enable_shell=False):
"""
Retrieves output of command
Returns tuple success (boolean)/ stdout(string) / stderr (string)
Retrieve output of a command.
Returns a named tuple with three elements:
Don't use this function with programs that outputs lots of data since the output is saved
in one variable
* ``rc`` (integer) Return code of command.
* ``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
stderr = None
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 = out.decode("UTF-8")
stderr = stderr.decode("UTF-8")
@ -27,7 +42,42 @@ def run_through_shell(command, enable_shell=False):
except OSError as e:
out = e.strerror
stderr = e.strerror
logging.getLogger("i3pystatus.core.command").exception("")
except subprocess.CalledProcessError as e:
out = e.output
logging.getLogger("i3pystatus.core.command").exception("")
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.threading import Manager
from i3pystatus.core.util import convert_position
from i3pystatus.core.command import execute
from i3pystatus.core.command import run_through_shell
@ -47,33 +48,46 @@ class Module(SettingsBase):
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)
Maps a click event with its associated callback.
To setup the callbacks, you can set the settings 'on_leftclick',
'on_rightclick', 'on_upscroll', 'on_downscroll'.
Currently implemented events are:
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):
if isinstance(cb, list):
return cb[0], cb[1:]
@ -90,23 +104,25 @@ class Module(SettingsBase):
elif button == 5: # mouse wheel down
cb = self.on_downscroll
else:
self.logger.info("Button '%d' not handled yet." % (button))
log_event(self.__name__, button, None, None, "Unhandled button")
return False
if not cb:
self.logger.info("no cb attached")
log_event(self.__name__, button, None, None, "No callback attached")
return False
else:
cb, args = split_callback_and_args(cb)
self.logger.debug("cb=%s args=%s" % (cb, args))
if callable(cb):
cb(self)
log_event(self.__name__, button, cb, args, "Python callback")
cb(self, *args)
elif hasattr(self, cb):
if cb is not "run":
log_event(self.__name__, button, cb, args, "Member callback")
getattr(self, cb)(*args)
else:
run_through_shell(cb, *args)
log_event(self.__name__, button, cb, args, "External command")
execute(cb, detach=True)
return True
def move(self, position):

View File

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

View File

@ -28,6 +28,8 @@ class Reddit(IntervalModule):
* {message_author}
* {message_subject}
* {message_body}
* {link_karma}
* {comment_karma}
"""
@ -73,7 +75,7 @@ class Reddit(IntervalModule):
r = praw.Reddit(user_agent='i3pystatus')
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())
if unread_messages:
d = vars(next(r.get_unread()))
@ -131,6 +133,14 @@ class Reddit(IntervalModule):
title = fdict["submission_title"][:(self.title_maxlen - 3)] + "..."
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)
self.output = {
"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