diff --git a/dev-requirements.txt b/dev-requirements.txt index ed38b0e..d4ff798 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,3 +3,4 @@ sphinx>=1.1 colour>=0.0.5 mock>=1.0 pep8>=1.5.7 +i3ipc>=1.2.0 diff --git a/i3pystatus/network.py b/i3pystatus/network.py index 879711f..92a5baa 100644 --- a/i3pystatus/network.py +++ b/i3pystatus/network.py @@ -1,13 +1,13 @@ import netifaces -from i3pystatus import IntervalModule +from i3pystatus import IntervalModule, formatp from i3pystatus.core.color import ColorRangeModule from i3pystatus.core.util import make_graph, round_dict, make_bar def count_bits(integer): bits = 0 - while (integer): + while integer: integer &= integer - 1 bits += 1 return bits @@ -67,7 +67,7 @@ def sysfs_interface_up(interface, unknown_up=False): return status == "up" or unknown_up and status == "unknown" -class NetworkInfo(): +class NetworkInfo: """ Retrieve network information. """ @@ -161,7 +161,7 @@ class NetworkInfo(): return info -class NetworkTraffic(): +class NetworkTraffic: """ Retrieve network traffic information """ @@ -231,6 +231,8 @@ class NetworkTraffic(): class Network(IntervalModule, ColorRangeModule): """ Displays network information for an interface. + formatp support + if u wanna display recv/send speed separate in dynamic color mode, please enable pango hint. Requires the PyPI packages `colour`, `netifaces`, `psutil` (optional, see below) and `basiciw` (optional, see below). @@ -255,10 +257,10 @@ class Network(IntervalModule, ColorRangeModule): * `{quality}` — Link quality in percent * `{quality_bar}` —Bar graphically representing link quality - Network Traffic Formatters (requires PyPI pacakge `psutil`): + Network Traffic Formatters (requires PyPI package `psutil`): * `{interface}` — the configured network interface - * `{kbs}` – Float representing kb\s + * `{kbs}` – Float representing KiB\s corresponds to graph type * `{network_graph}` – Unicode graph representing network usage * `{bytes_sent}` — bytes sent per second (divided by divisor) * `{bytes_recv}` — bytes received per second (divided by divisor) @@ -267,6 +269,7 @@ class Network(IntervalModule, ColorRangeModule): * `{rx_tot_Mbytes}` — total Mbytes received * `{tx_tot_Mbytes}` — total Mbytes sent """ + settings = ( ("format_up", "format string"), ("format_down", "format string"), @@ -278,8 +281,10 @@ class Network(IntervalModule, ColorRangeModule): ("end_color", "Hex or English name for end of color range, eg '#FF0000' or 'red'"), ("graph_width", "Width of the network traffic graph"), ("graph_style", "Graph style ('blocks', 'braille-fill', 'braille-peak', or 'braille-snake')"), - ("upper_limit", - "Expected max kb/s. This value controls how the network traffic graph is drawn and in what color"), + ("recv_limit", "Expected max KiB/s. This value controls the drawing color of receive speed"), + ("sent_limit", "Expected max KiB/s. similar with receive_limit"), + ("separate_color", "display recv/send color separate in dynamic color mode." + "Note: only network speed formatters will display with range color "), ("graph_type", "Whether to draw the network traffic graph for input or output. " "Allowed values 'input' or 'output'"), ("divisor", "divide all byte values by this value"), @@ -301,7 +306,9 @@ class Network(IntervalModule, ColorRangeModule): graph_type = 'input' graph_width = 15 graph_style = 'blocks' - upper_limit = 150.0 + recv_limit = 2048 + sent_limit = 1024 + separate_color = False # Network traffic settings divisor = 1024 @@ -319,7 +326,7 @@ class Network(IntervalModule, ColorRangeModule): def init(self): # Don't require importing basiciw unless using the functionality it offers. - if any(s in self.format_up or s in self.format_up for s in + if any(s in self.format_down or s in self.format_up for s in ['essid', 'freq', 'quality', 'quality_bar']): get_wifi_info = True else: @@ -337,9 +344,10 @@ class Network(IntervalModule, ColorRangeModule): self.network_traffic = None if not self.dynamic_color: - self.end_color = self.start_color - self.colors = self.get_hex_color_range(self.start_color, self.end_color, int(self.upper_limit)) + self.end_color = self.start_color = self.color_up + self.colors = self.get_hex_color_range(self.start_color, self.end_color, 100) self.kbs_arr = [0.0] * self.graph_width + self.pango_enabled = self.hints.get("markup", False) and self.hints["markup"] == "pango" def cycle_interface(self, increment=1): """Cycle through available interfaces in `increment` steps. Sign indicates direction.""" @@ -354,11 +362,11 @@ class Network(IntervalModule, ColorRangeModule): self.network_traffic.clear_counters() self.kbs_arr = [0.0] * self.graph_width - def get_network_graph(self, kbs): + def get_network_graph(self, kbs, limit): # Cycle array by inserting at the start and chopping off the last element self.kbs_arr.insert(0, kbs) self.kbs_arr = self.kbs_arr[:self.graph_width] - return make_graph(self.kbs_arr, 0.0, self.upper_limit, self.graph_style) + return make_graph(self.kbs_arr, 0.0, limit, self.graph_style) def run(self): format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="", @@ -369,15 +377,35 @@ class Network(IntervalModule, ColorRangeModule): network_usage = self.network_traffic.get_usage(self.interface) format_values.update(network_usage) if self.graph_type == 'input': - kbs = network_usage['bytes_recv'] + limit = self.recv_limit + kbs = network_usage['bytes_recv'] * self.divisor / 1024 elif self.graph_type == 'output': - kbs = network_usage['bytes_sent'] + limit = self.sent_limit + kbs = network_usage['bytes_sent'] * self.divisor / 1024 else: raise Exception("graph_type must be either 'input' or 'output'!") - format_values['network_graph'] = self.get_network_graph(kbs) - format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6) - color = self.get_gradient(kbs, self.colors, self.upper_limit) + format_values['network_graph'] = self.get_network_graph(kbs, limit) + format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)) + + if self.separate_color and self.pango_enabled: + color = self.color_up + color_template = "{}" + per_recv = network_usage["bytes_recv"] * self.divisor / (self.recv_limit * 1024) + per_sent = network_usage["bytes_sent"] * self.divisor / (self.sent_limit * 1024) + c_recv = self.get_gradient(int(per_recv * 100), self.colors, 100) + c_sent = self.get_gradient(int(per_sent * 100), self.colors, 100) + format_values["bytes_recv"] = color_template.format(c_recv, network_usage["bytes_recv"]) + format_values["bytes_sent"] = color_template.format(c_sent, network_usage["bytes_sent"]) + if self.graph_type == 'output': + c_kbs = c_sent + else: + c_kbs = c_recv + format_values['network_graph'] = color_template.format(c_kbs, format_values["network_graph"]) + format_values['kbs'] = color_template.format(c_kbs, format_values["kbs"]) + else: + percent = int(kbs * 100 / limit) + color = self.get_gradient(percent, self.colors, 100) else: color = None @@ -395,6 +423,6 @@ class Network(IntervalModule, ColorRangeModule): self.data = format_values self.output = { - "full_text": format_str.format(**format_values), + "full_text": formatp(format_str, **format_values).strip(), 'color': color, } diff --git a/i3pystatus/scratchpad.py b/i3pystatus/scratchpad.py new file mode 100644 index 0000000..f6e4d9b --- /dev/null +++ b/i3pystatus/scratchpad.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +from threading import Thread +from i3pystatus import Module +import i3ipc + + +class Scratchpad(Module): + """ + Display the amount of windows and indicate urgency hints on scratchpad (async). + + fork from scratchpad_async of py3status by cornerman + + Requires the PyPI package `i3ipc`. + + .. rubric:: Available formaters + + * `{number}` — amount of windows on scratchpad + + @author jok + @license BSD + """ + + settings = ( + ("format", "format string."), + ("always_show", "whether the indicator should be shown if there are" + " no scratchpad windows"), + ("color_urgent", "color of urgent"), + ("color", "text color"), + ) + + format = u"{number} ⌫" + always_show = True + color_urgent = "#900000" + color = "#FFFFFF" + + def init(self): + self.count = 0 + self.urgent = False + + t = Thread(target=self._listen) + t.daemon = True + t.start() + + def update_scratchpad_counter(self, conn, *args): + cons = conn.get_tree().scratchpad().leaves() + self.urgent = any(con for con in cons if con.urgent) + self.count = len(cons) + + # output + if self.urgent: + color = self.color_urgent + else: + color = self.color + + if self.always_show or self.count > 0: + full_text = self.format.format(number=self.count) + else: + full_text = '' + + self.output = { + "full_text": full_text, + "color": color, + } + + def _listen(self): + conn = i3ipc.Connection() + self.update_scratchpad_counter(conn) + + conn.on('window::move', self.update_scratchpad_counter) + conn.on('window::urgent', self.update_scratchpad_counter) + conn.on('window::new', self.update_scratchpad_counter) + conn.on('window::close', self.update_scratchpad_counter) + + conn.main() diff --git a/i3pystatus/window_title.py b/i3pystatus/window_title.py new file mode 100644 index 0000000..359e1d2 --- /dev/null +++ b/i3pystatus/window_title.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +from i3pystatus import Module +from threading import Thread +import i3ipc + + +class WindowTitle(Module): + """ + Display the current window title with async update. + Uses asynchronous update via i3 IPC events. + Provides instant title update only when it required. + + fork from window_tile_async of py3status by Anon1234 https://github.com/Anon1234 + + Requires the PyPI package `i3ipc`. + + .. rubric:: Available formaters + + * `{title}` — title of current focused window + * `{class_name}` - name of application class + + @author jok + @license BSD + """ + + settings = ( + ("format", "format string."), + ("always_show", "do not hide the title when it can be already visible"), + ("empty_title", "string that will be shown instead of the title when the title is hidden"), + ("max_width", "maximum width of title"), + ("color", "text color"), + ) + + format = "{title}" + always_show = False + empty_title = "" + max_width = 79 + color = "#FFFFFF" + + def init(self): + self.title = self.empty_title + self.output = { + "full_text": self.title, + "color": self.color, + } + + # we are listening to i3 events in a separate thread + t = Thread(target=self._loop) + t.daemon = True + t.start() + + def get_title(self, conn): + tree = conn.get_tree() + w = tree.find_focused() + p = w.parent + + # don't show window title when the window already has means + # to display it + if (not self.always_show and + (w.border == "normal" or w.type == "workspace" or + (p.layout in ("stacked", "tabbed") and len(p.nodes) > 1))): + return self.empty_title + else: + title = w.name + class_name = w.window_class + if len(title) > self.max_width: + title = title[:self.max_width - 1] + "…" + return self.format.format(title=title, class_name=class_name) + + def update_title(self, conn, e): + # catch only focused window title updates + title_changed = hasattr(e, "container") and e.container.focused + + # check if we need to update title due to changes + # in the workspace layout + layout_changed = ( + hasattr(e, "binding") and + (e.binding.command.startswith("layout") or + e.binding.command.startswith("move container") or + e.binding.command.startswith("border")) + ) + + if title_changed or layout_changed: + self.title = self.get_title(conn) + self.update_display() + + def clear_title(self, *args): + self.title = self.empty_title + self.update_display() + + def update_display(self): + self.output = { + "full_text": self.title, + "color": self.color, + } + + def _loop(self): + conn = i3ipc.Connection() + self.title = self.get_title(conn) # set title on startup + self.update_display() + + # The order of following callbacks is important! + # clears the title on empty ws + conn.on('workspace::focus', self.clear_title) + + # clears the title when the last window on ws was closed + conn.on("window::close", self.clear_title) + + # listens for events which can trigger the title update + conn.on("window::title", self.update_title) + conn.on("window::focus", self.update_title) + + conn.main() # run the event loop