summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFederico Ceratto <federico.ceratto@gmail.com>2015-03-22 19:38:01 +0000
committerFederico Ceratto <federico.ceratto@gmail.com>2015-03-22 19:38:01 +0000
commitd5440a6e81fe713c3e53e396d60605b663e79944 (patch)
treee5b0b04ce6e7ea9cf4a5faffc1fbe746043c1875
parenta5e63832c8fd950c27c4172260b67b2fec6ab9f8 (diff)
parent820e0731ba350a84a67140aa60bc07556d0af105 (diff)
Merge branch 'master' of github.com:ultrabug/py3status
-rw-r--r--py3status/modules/imap.py6
-rw-r--r--py3status/modules/pomodoro.py118
-rw-r--r--py3status/modules/xrandr.py387
3 files changed, 497 insertions, 14 deletions
diff --git a/py3status/modules/imap.py b/py3status/modules/imap.py
index 757d9fd..ff53f02 100644
--- a/py3status/modules/imap.py
+++ b/py3status/modules/imap.py
@@ -1,7 +1,7 @@
# -*- coding: utf8 -*-
"""
Module displaying the number of unread messages
-on an IMAP inbox (configurable, may also be a
+on an IMAP inbox (configurable, may also be a
comma-separated list of IMAP folders).
@author obb
@@ -33,7 +33,7 @@ class Py3status:
}
if not self.new_mail_color:
- self.new_mail_color = i3s_config['color_bad']
+ self.new_mail_color = i3s_config['color_good']
if mail_count == 'N/A':
response['color'] = ''
@@ -56,7 +56,7 @@ class Py3status:
directories = self.mailbox.split(',')
connection = imaplib.IMAP4_SSL(self.imap_server, self.port)
connection.login(self.user, self.password)
-
+
for directory in directories:
connection.select(directory)
unseen_response = connection.search(None, self.criterion)
diff --git a/py3status/modules/pomodoro.py b/py3status/modules/pomodoro.py
index 1a167b9..ecb3f57 100644
--- a/py3status/modules/pomodoro.py
+++ b/py3status/modules/pomodoro.py
@@ -4,13 +4,40 @@ Pomodoro countdown on i3bar originally written by @Fandekasp (Adrien Lemaire)
"""
from subprocess import call
+from syslog import syslog, LOG_INFO
from time import time
+try:
+ from pygame import mixer
+ mixer.init()
+except ImportError:
+ mixer = None
+
+# PROGRESS_BAR_ITEMS = u"▁▃▄▅▆▇█"
+PROGRESS_BAR_ITEMS = u"▏▎▍▌▋▊▉"
+
class Py3status:
+ """
+ Configuration parameters:
+ - display_bar: display time in bars when True, otherwise in seconds
+ - max_breaks: maximum number of breaks
+ - num_progress_bars: number of progress bars
+ - sound_break_end: break end sound (file path)
+ - sound_pomodoro_end: pomodoro end sound (file path)
+ - sound_pomodoro_start: pomodoro start sound (file path)
+ - timer_break: normal break time (seconds) (requires pygame)
+ - timer_long_break: long break time (seconds) (requires pygame)
+ - timer_pomodoro: pomodoro time (seconds) (requires pygame)
+ """
# available configuration parameters
+ display_bar = False
max_breaks = 4
+ num_progress_bars = 5
+ sound_break_end = None
+ sound_pomodoro_end = None
+ sound_pomodoro_start = None
timer_break = 5 * 60
timer_long_break = 15 * 60
timer_pomodoro = 25 * 60
@@ -22,19 +49,42 @@ class Py3status:
def on_click(self, i3s_output_list, i3s_config, event):
"""
- Handles click events
+ Handles click events:
+ - left click starts an inactive counter and pauses a running
+ Pomodoro
+ - middle click resets everything
+ - right click starts (and ends, if needed) a break
+
"""
if event['button'] == 1:
if self.status == 'stop':
self.status = 'start'
- self.run = True
+ self.__play_sound(self.sound_pomodoro_start)
+ self.run = True
+
+ elif self.status == 'break':
+ self.run = True
+
+ elif self.status == 'start':
+ if self.run:
+ self.status = 'pause'
+ self.run = False
+ else:
+ self.run = True
+
+ elif self.status == 'pause':
+ self.status = 'start'
+ self.run = True
elif event['button'] == 2:
self.__setup('stop')
self.run = False
elif event['button'] == 3:
- self.__setup('pause')
+ if self.status == 'break':
+ self.__setup('start')
+ else:
+ self.__setup('break')
self.run = False
@property
@@ -42,9 +92,28 @@ class Py3status:
"""
Return the response full_text string
"""
- return {
- 'full_text': '{} ({})'.format(self.prefix, self.timer)
- }
+ if self.display_bar and self.status in ('start', 'pause'):
+ bar = u''
+ items_cnt = len(PROGRESS_BAR_ITEMS)
+ bar = u''
+ bar_val = float(self.timer) / self.time_window * \
+ self.num_progress_bars
+ while bar_val > 0:
+ selector = int(bar_val * items_cnt)
+ selector = min(selector, items_cnt - 1)
+ bar += PROGRESS_BAR_ITEMS[selector]
+ bar_val -= 1
+
+ bar = bar.ljust(self.num_progress_bars).encode('utf_8')
+ else:
+ bar = self.timer
+
+ if self.run:
+ text = '{} [{}]'.format(self.prefix, bar)
+ else:
+ text = '{} ({})'.format(self.prefix, bar)
+
+ return dict(full_text=text)
def __setup(self, status):
"""
@@ -55,20 +124,24 @@ class Py3status:
self.prefix = 'Pomodoro'
self.status = 'stop'
self.timer = self.timer_pomodoro
+ self.time_window = self.timer
self.breaks = 1
elif status == 'start':
self.prefix = 'Pomodoro'
self.timer = self.timer_pomodoro
+ self.time_window = self.timer
- elif status == 'pause':
+ elif status == 'break':
self.prefix = 'Break #%d' % self.breaks
if self.breaks > self.max_breaks:
self.timer = self.timer_long_break
+ self.time_window = self.timer
self.breaks = 1
else:
self.breaks += 1
self.timer = self.timer_break
+ self.time_window = self.timer
def __decrement(self):
"""
@@ -79,12 +152,16 @@ class Py3status:
self.alert = True
self.run = False
self.__i3_nagbar()
+
if self.status == 'start':
- self.__setup('pause')
- self.status = 'pause'
- elif self.status == 'pause':
+ self.__setup('break')
+ self.status = 'break'
+ self.__play_sound(self.sound_pomodoro_end)
+
+ elif self.status == 'break':
self.__setup('start')
self.status = 'start'
+ self.__play_sound(self.sound_break_end)
def __i3_nagbar(self, level='warning'):
"""
@@ -114,7 +191,7 @@ class Py3status:
if self.status == 'start':
response['color'] = i3s_config['color_good']
- elif self.status == 'pause':
+ elif self.status == 'break':
response['color'] = i3s_config['color_degraded']
else:
response['color'] = i3s_config['color_bad']
@@ -122,6 +199,25 @@ class Py3status:
response['cached_until'] = time()
return response
+ def __play_sound(self, sound_fname):
+ """Play sound if required
+ """
+ if not sound_fname:
+ return
+
+ if not mixer:
+ syslog(LOG_INFO, "pomodoro module: the pygame library is required"
+ " to play sounds")
+ return
+
+ try:
+ mixer.music.load(sound_fname)
+ except Exception as e:
+ return
+
+ mixer.music.play()
+
+
if __name__ == "__main__":
"""
Test this module by calling it directly.
diff --git a/py3status/modules/xrandr.py b/py3status/modules/xrandr.py
new file mode 100644
index 0000000..2d3e757
--- /dev/null
+++ b/py3status/modules/xrandr.py
@@ -0,0 +1,387 @@
+# -*- coding: utf-8 -*-
+"""
+This modules allows you to handle your screens outputs directly from your bar!
+ - Detect and propose every possible screen combinations
+ - Switch between combinations using click events and mouse scroll
+ - Activate the screen or screen combination on a single click
+ - It will detect any newly connected or removed screen automatically
+
+For convenience, this module also proposes some added features:
+ - Dynamic parameters for POSITION and WORKSPACES assignment (see below)
+ - Automatic fallback to a given screen or screen combination when no more
+ screen is available (handy for laptops)
+ - Automatically apply this screen combination on start: no need for xorg!
+ - Automatically move workspaces to screens when they are available
+
+Example config:
+ xrandr {
+ force_on_start = "eDP1+DP1"
+ DP1_pos = "left-of eDP1"
+ VGA_workspaces = "7"
+ }
+
+@author ultrabug
+"""
+import shlex
+
+from collections import deque
+from collections import OrderedDict
+from itertools import combinations
+from subprocess import call, Popen, PIPE
+from syslog import syslog, LOG_INFO
+from time import sleep, time
+
+
+class Py3status:
+ """
+ Configuration parameters:
+ - cache_timeout: how often to (re)detect the outputs
+ - fallback: when the current output layout is not available anymore,
+ fallback to this layout if available. This is very handy if you
+ have a laptop and switched to an external screen for presentation
+ and want to automatically fallback to your laptop screen when you
+ disconnect the external screen.
+ - force_on_start: switch to the given combination mode if available
+ when the module starts (saves you from having to configure xorg)
+ - format_clone: string used to display a 'clone' combination
+ - format_extend: string used to display a 'extend' combination
+
+ Dynamic configuration parameters:
+ - <OUTPUT>_pos: apply the given position to the OUTPUT
+ Example: DP1_pos = "-2560x0"
+ Example: DP1_pos = "left-of LVDS1"
+ Example: DP1_pos = "right-of eDP1"
+
+ - <OUTPUT>_workspaces: comma separated list of workspaces to move to
+ the given OUTPUT when it is activated
+ Example: DP1_workspaces = "1,2,3"
+ """
+ # available configuration parameters
+ cache_timeout = 10
+ fallback = True
+ fixed_width = True
+ force_on_start = None
+ format_clone = '='
+ format_extend = '+'
+
+ def __init__(self):
+ """
+ """
+ self.active_comb = None
+ self.active_layout = None
+ self.active_mode = 'extend'
+ self.displayed = None
+ self.max_width = 0
+
+ def _get_layout(self):
+ """
+ Get the current outputs layout from xrandr and try to detect the
+ currently active layout as best as we can on start.
+ """
+ connected = list()
+ active_layout = list()
+ disconnected = list()
+ layout = OrderedDict(
+ {
+ 'connected': OrderedDict(),
+ 'disconnected': OrderedDict()
+ }
+ )
+
+ current = Popen(['xrandr', '--current'], stdout=PIPE)
+ for line in current.stdout.readlines():
+ try:
+ # python3
+ line = line.decode()
+ except:
+ pass
+ try:
+ s = line.split(' ')
+ if s[1] == 'connected':
+ output, state = s[0], s[1]
+ if s[2][0] == '(':
+ mode, infos = None, ' '.join(s[2:]).strip('\n')
+ else:
+ mode, infos = s[2], ' '.join(s[3:]).strip('\n')
+ active_layout.append(output)
+ connected.append(output)
+ elif s[1] == 'disconnected':
+ output, state = s[0], s[1]
+ mode, infos = None, ' '.join(s[2:]).strip('\n')
+ disconnected.append(output)
+ else:
+ continue
+ except Exception as err:
+ syslog(LOG_INFO, 'xrandr error="{}"'.format(err))
+ else:
+ layout[state][output] = {
+ 'infos': infos,
+ 'mode': mode,
+ 'state': state
+ }
+
+ # initialize the active layout
+ if self.active_layout is None:
+ self.active_comb = tuple(active_layout)
+ self.active_layout = self._get_string_and_set_width(
+ tuple(active_layout),
+ self.active_mode
+ )
+
+ return layout
+
+ def _set_available_combinations(self):
+ """
+ Generate all connected outputs combinations and
+ set the max display width while iterating.
+ """
+ available_combinations = set()
+ combinations_map = {}
+
+ self.max_width = 0
+ for output in range(len(self.layout['connected'])+1):
+ for comb in combinations(self.layout['connected'], output):
+ if comb:
+ for mode in ['clone', 'extend']:
+ string = self._get_string_and_set_width(comb, mode)
+ if len(comb) == 1:
+ combinations_map[string] = (comb, None)
+ else:
+ combinations_map[string] = (comb, mode)
+ available_combinations.add(string)
+ self.available_combinations = deque(available_combinations)
+ self.combinations_map = combinations_map
+
+ def _get_string_and_set_width(self, combination, mode):
+ """
+ Construct the string to be displayed and record the max width.
+ """
+ show = '{}'.format(self._separator(mode)).join(combination)
+ show = show.rstrip('{}'.format(self._separator(mode)))
+ self.max_width = max([self.max_width, len(show)])
+ return show
+
+ def _choose_what_to_display(self, force_refresh=False):
+ """
+ Choose what combination to display on the bar.
+
+ By default we try to display the active layout on the first run, else
+ we display the last selected combination.
+ """
+ for _ in range(len(self.available_combinations)):
+ if (
+ self.displayed is None and
+ self.available_combinations[0] == self.active_layout
+ ):
+ self.displayed = self.available_combinations[0]
+ break
+ else:
+ if self.displayed == self.available_combinations[0]:
+ break
+ else:
+ self.available_combinations.rotate(1)
+ else:
+ if force_refresh:
+ self.displayed = self.available_combinations[0]
+ else:
+ syslog(
+ LOG_INFO,
+ 'xrandr error="displayed combination is not available"'
+ )
+
+ def _center(self, s):
+ """
+ Center the given string on the detected max width.
+ """
+ fmt = '{:^%d}' % self.max_width
+ return fmt.format(s)
+
+ def _apply(self, force=False):
+ """
+ Call xrandr and apply the selected (displayed) combination mode.
+ """
+ if self.displayed == self.active_layout and not force:
+ # no change, do nothing
+ return
+
+ combination, mode = self.combinations_map.get(
+ self.displayed, (None, None)
+ )
+ if combination is None and mode is None:
+ # displayed combination cannot be activated, ignore
+ return
+
+ cmd = 'xrandr'
+ outputs = list(self.layout['connected'].keys())
+ outputs += list(self.layout['disconnected'].keys())
+ previous_output = None
+ for output in outputs:
+ cmd += ' --output {}'.format(output)
+ #
+ if output in combination:
+ pos = getattr(self, '{}_pos'.format(output), '0x0')
+ #
+ if mode == 'clone' and previous_output is not None:
+ cmd += ' --auto --same-as {}'.format(previous_output)
+ else:
+ if 'left-of' in pos:
+ cmd += ' --auto --{} --rotate normal'.format(pos)
+ elif 'right-of' in pos:
+ cmd += ' --auto --{} --rotate normal'.format(pos)
+ else:
+ cmd += ' --auto --pos {} --rotate normal'.format(pos)
+ previous_output = output
+ else:
+ cmd += ' --off'
+ #
+ code = call(shlex.split(cmd))
+ if code == 0:
+ self.active_comb = combination
+ self.active_layout = self.displayed
+ self.active_mode = mode
+ syslog(LOG_INFO, 'command "{}" exit code {}'.format(cmd, code))
+
+ # move workspaces to outputs as configured
+ self._apply_workspaces(combination, mode)
+
+ def _apply_workspaces(self, combination, mode):
+ """
+ Allows user to force move a comma separated list of workspaces to the
+ given output when it's activated.
+
+ Example:
+ - DP1_workspaces = "1,2,3"
+ """
+ if len(combination) > 1 and mode == 'extend':
+ sleep(3)
+ for output in combination:
+ workspaces = getattr(
+ self, '{}_workspaces'.format(output), '').split(',')
+ for workspace in workspaces:
+ if not workspace:
+ continue
+ # switch to workspace
+ cmd = 'i3-msg workspace "{}"'.format(workspace)
+ call(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
+ # move it to output
+ cmd = 'i3-msg move workspace to output "{}"'.format(output)
+ call(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
+ # log this
+ syslog(
+ LOG_INFO,
+ 'moved workspace {} to output {}'.format(
+ workspace, output)
+ )
+
+ def _refresh_py3status(self):
+ """
+ Send a SIGUSR1 signal to py3status to force a bar refresh.
+ """
+ call(shlex.split('killall -s USR1 py3status'))
+
+ def _fallback_to_available_output(self):
+ """
+ Fallback to the first available output when the active layout
+ was composed of only one output.
+
+ This allows us to avoid cases where you get stuck with a black sreen
+ on your laptop by switching back to the integrated screen
+ automatically !
+ """
+ if len(self.active_comb) == 1:
+ self._choose_what_to_display(force_refresh=True)
+ self._apply()
+ self._refresh_py3status()
+
+ def _force_force_on_start(self):
+ """
+ Force the user configured mode on start.
+ """
+ if self.force_on_start in self.available_combinations:
+ self.displayed = self.force_on_start
+ self.force_on_start = None
+ self._choose_what_to_display(force_refresh=True)
+ self._apply(force=True)
+ self._refresh_py3status()
+
+ def _separator(self, mode):
+ """
+ Return the separator for the given mode.
+ """
+ if mode == 'extend':
+ return self.format_extend
+ if mode == 'clone':
+ return self.format_clone
+
+ def _switch_selection(self, direction):
+ self.available_combinations.rotate(direction)
+ self.displayed = self.available_combinations[0]
+
+ def on_click(self, i3s_output_list, i3s_config, event):
+ """
+ Click events
+ - left click & scroll up/down: switch between modes
+ - right click: apply selected mode
+ - middle click: force refresh of available modes
+ """
+ button = event['button']
+ if button == 4:
+ self._switch_selection(-1)
+ if button in [1, 5]:
+ self._switch_selection(1)
+ if button == 2:
+ self._choose_what_to_display(force_refresh=True)
+ if button == 3:
+ self._apply()
+
+ def xrandr(self, i3s_output_list, i3s_config):
+ """
+ This is the main py3status method, it will orchestrate what's being
+ displayed on the bar.
+ """
+ self.layout = self._get_layout()
+ self._set_available_combinations()
+ self._choose_what_to_display()
+
+ if self.fixed_width is True:
+ full_text = self._center(self.displayed)
+ else:
+ full_text = self.displayed
+
+ response = {
+ 'cached_until': time() + self.cache_timeout,
+ 'full_text': full_text
+ }
+
+ # coloration
+ if self.displayed == self.active_layout:
+ response['color'] = i3s_config['color_good']
+ elif self.displayed not in self.available_combinations:
+ response['color'] = i3s_config['color_bad']
+
+ # force default layout setup
+ if self.force_on_start is not None:
+ sleep(1)
+ self._force_force_on_start()
+
+ # fallback detection
+ if self.active_layout not in self.available_combinations:
+ response['color'] = i3s_config['color_degraded']
+ if self.fallback is True:
+ self._fallback_to_available_output()
+
+ return response
+
+if __name__ == "__main__":
+ """
+ Test this module by calling it directly.
+ """
+ x = Py3status()
+ config = {
+ 'color_bad': '#FF0000',
+ 'color_degraded': '#FFFF00',
+ 'color_good': '#00FF00'
+ }
+ while True:
+ print(x.xrandr([], config))
+ sleep(1)