import re
from powerline.theme import requires_segment_info
from powerline.bindings.wm import get_i3_connection
def workspace_groups(w):
group = []
if w.focused:
group.append('workspace:focused')
if w.urgent:
group.append('workspace:urgent')
if w.visible:
group.append('workspace:visible')
group.append('workspace')
return group
WS_ICONS = {
"Xfce4-terminal": "",
"Chromium": "",
"Google-chrome": "",
"Steam": "",
"jetbrains": "",
"Gimp": "",
"Pavucontrol": "",
"Lmms": "",
"thunderbird": "",
"Thunar": "",
"Skype": "",
"TelegramDesktop": "",
"feh": "",
"firefox": "",
"Evince": "",
"Okular": "",
"libreoffice-calc": "",
"libreoffice-writer": "",
"multiple": ""
}
def get_icon(w, separator, icons, show_multiple_icons, ws_containers):
if w.num == -5:
return ""
icons_tmp = WS_ICONS
icons_tmp.update(icons)
icons = icons_tmp
wins = [win for win in ws_containers[w.name].leaves() \
if win.parent.scratchpad_state == 'none']
if len(wins) == 0:
return ""
result = ""
cnt = 0
for key in icons:
if not icons[key] or len(icons[key]) < 1:
continue
if any(key.upper() in win.window_class.upper() for win in wins if win.window_class):
result += separator + icons[key]
cnt += 1
if not show_multiple_icons and cnt > 1:
if 'multiple' in icons:
return separator + icons['multiple']
else:
return ""
return result
import copy
def get_next_ws(ws, outputs):
names = [w.name for w in ws]
for i in range(1, 100):
if not str(i) in names:
res_ls = []
res = copy.deepcopy(ws[0])
res.num = -5
res.name = str(i)
res.urgent = False
res.focused = False
res.visible = False
for o in outputs:
r2 = copy.deepcopy(res)
r2.output = o
res_ls += [r2]
return res_ls
return []
def is_empty_workspace(w, ws_containers):
if w.num == -5:
return False
if w.focused or w.visible:
return False
wins = [win for win in ws_containers[w.name].leaves()]
return False if len(wins) > 0 else True
[docs]@requires_segment_info
def workspaces(pl, segment_info, only_show=None, output=None, strip=0, separator=" ",
icons=WS_ICONS, show_icons=True, show_multiple_icons=True, show_dummy_workspace=False,
show_output=False, priority_workspaces=[], hide_empty_workspaces=False):
'''Return list of used workspaces
:param list only_show:
Specifies which workspaces to show. Valid entries are ``"visible"``,
``"urgent"`` and ``"focused"``. If omitted or ``null`` all workspaces
are shown.
:param string output:
May be set to the name of an X output. If specified, only workspaces
on that output are shown. Overrides automatic output detection by
the lemonbar renderer and bindings.
Use "__all__" to show workspaces on all outputs.
:param int strip:
Specifies how many characters from the front of each workspace name
should be stripped (e.g. to remove workspace numbers). Defaults to zero.
:param string separator:
Specifies a string to be inserted between the workspace name and program icons
and between program icons.
:param dict icons:
A dictionary mapping a substring of window classes to strings to be used as an icon for that
window class. The following window classes have icons by default:
``Xfce4-terminal``, ``Chromium``, ``Steam``, ``jetbrains``, ``Gimp``, ``Pavucontrol``, ``Lmms``,
``Thunderbird``, ``Thunar``, ``Skype``, ``TelegramDesktop``, ``feh``, ``Firefox``, ``Evince``,
``Okular``, ``libreoffice-calc``, ``libreoffice-writer``.
You can override the default icons by defining an icon for that window class yourself, and disable
single icons by setting their icon to "" or None.
Further, there is a ``multiple`` icon for workspaces containing more than one window (which is used if
``show_multiple_icons`` is ``False``)
:param boolean show_icons:
Determines whether to show icons. Defaults to True.
:param boolean show_multiple_icons:
If this is set to False, instead of displaying multiple icons per workspace,
the icon "multiple" will be used.
:param boolean show_dummy_workspace:
If this is set to True, this segment will always display an additional, non-existing
workspace. This workspace will be handled as if it was a non-urgent and non-focused
regular workspace, i.e., click events will work as with normal workspaces.
:param boolean show_output:
Show the name of the output if more than one output is connected and output is not
set to ``__all__``.
:param string list priority_workspaces:
A list of workspace names to be sorted before any other workspaces in the given
order.
:param boolean hide_empty_workspaces:
Hides all workspaces without any open window. (Does not remove the dummy workspace.)
Also hides non-focussed workspaces containing only an open scratchpad.
Highlight groups used: ``workspace`` or ``workspace:visible``, ``workspace`` or ``workspace:focused``, ``workspace`` or ``workspace:urgent`` or ``output``.
Click values supplied: ``workspace_name`` (string) for workspaces and ``output_name`` (string) for outputs.
'''
channel_name = 'i3wm.workspaces'
conn = get_i3_connection()
channel_value = None
if 'payloads' in segment_info and channel_name in segment_info['payloads']:
channel_value = segment_info['payloads'][channel_name]
if channel_value:
# Shrink the segment as far as possible
only_show = ['focused', 'visible']
show_multiple_icons = False
output_count = 1
if not output == "__all__":
output = output or segment_info.get('output')
if show_output:
output_count = len([o for o in conn.get_outputs() if o.active])
else:
output = None
if output:
output = [output]
else:
output = [o.name for o in conn.get_outputs() if o.active]
def sort_ws(ws):
import re
def natural_key(ws):
str = ws.name
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', str)]
ws = sorted(ws, key=natural_key)
result = []
for n in priority_workspaces:
result += [w for w in ws if w.name == n]
return result + [w for w in ws if not w.name in priority_workspaces] \
+ (get_next_ws(ws, output) if show_dummy_workspace else [])
ws_containers = {w_con.name : w_con for w_con in conn.get_tree().workspaces()}
if len(output) <= 1:
res = []
if output_count > 1:
res += [{
'contents': output[0],
'payload_name': channel_name,
'highlight_groups': ['output'],
'click_values': {'output_name': output[0]}
}]
res += [{
'contents': w.name[min(len(w.name), strip):] \
+ (get_icon(w, separator, icons, show_multiple_icons, ws_containers) \
if show_icons else ""),
'highlight_groups': workspace_groups(w),
'payload_name': channel_name,
'click_values': {'workspace_name': w.name}
} for w in sort_ws(conn.get_workspaces())
if (not only_show or any(getattr(w, typ) for typ in only_show))
if w.output == output[0]
if not (hide_empty_workspaces and is_empty_workspace(w, ws_containers))
]
return res
else:
res = []
for n in output:
res += [{
'contents': n,
'highlight_groups': ['output'],
'payload_name': channel_name,
'click_values': {'output_name': n}
}]
res += [{'contents': w.name[min(len(w.name), strip):] \
+ (get_icon(w, separator, icons, show_multiple_icons, ws_containers) \
if show_icons else ""),
'highlight_groups': workspace_groups(w),
'payload_name': channel_name,
'click_values': {'workspace_name': w.name}} \
for w in sort_ws(conn.get_workspaces())
if (not only_show or any(getattr(w, typ) for typ in only_show))
if w.output == n
if not (hide_empty_workspaces and is_empty_workspace(w, ws_containers))
]
return res
[docs]@requires_segment_info
def mode(pl, segment_info, names={'default': None}):
'''Returns current i3 mode
:param str default:
Specifies the name to be displayed instead of "default".
By default the segment is left out in the default mode.
Highlight groups used: ``mode``
'''
current_mode = segment_info['mode']
if current_mode in names:
return names[current_mode]
return current_mode
def scratchpad_groups(w):
group = []
if w.urgent:
group.append('scratchpad:urgent')
if w.nodes[0].focused:
group.append('scratchpad:focused')
if w.workspace().name != '__i3_scratch':
group.append('scratchpad:visible')
group.append('scratchpad')
return group
SCRATCHPAD_ICONS = {
'fresh': 'O',
'changed': 'X',
}
[docs]def scratchpad(pl, icons=SCRATCHPAD_ICONS):
'''Returns the windows currently on the scratchpad
:param dict icons:
Specifies the strings to show for the different scratchpad window states. Must
contain the keys ``fresh`` and ``changed``.
Highlight groups used: ``scratchpad`` or ``scratchpad:visible``, ``scratchpad`` or ``scratchpad:focused``, ``scratchpad`` or ``scratchpad:urgent``.
'''
windows = get_i3_connection().get_tree().descendants()
return [{'contents': icons.get(w.scratchpad_state, icons['changed']),
'highlight_groups': scratchpad_groups(w)
} for w in windows if w.scratchpad_state != 'none']
# Global menu support heavily influenced by https://github.com/jamcnaughton/hud-menu
def compute_appmenu_menu(window_id):
import dbus, time
try:
sbus = dbus.SessionBus()
areg = sbus.get_object('com.canonical.AppMenu.Registrar',
'/com/canonical/AppMenu/Registrar')
aregi = dbus.Interface(areg, 'com.canonical.AppMenu.Registrar')
dbmenu, dbmenu_path = aregi.GetMenuForWindow(window_id)
dbo = sbus.get_object(dbmenu, dbmenu_path)
dboi = dbus.Interface(dbo, 'com.canonical.dbusmenu')
db_items = dboi.GetLayout(0, -1, ["label"])
def explore(item):
item_id = item[0]
item_props = item[1]
if 'children-display' in item_props:
dboi.AboutToShow(item_id)
dboi.Event(item_id, "opened", "not used", dbus.UInt32(time.time())) #fix firefox
try:
item = dboi.GetLayout(item_id, 1, ["label", "children-display"])[1]
except:
return { }
item_children = item[2]
name = 'Root'
if 'label' in item_props:
name = item_props['label'].replace('_', '')
if len(item_children) == 0:
return { name: lambda: dboi.Event(item_id, 'clicked', 0, 0) }
else:
res = {}
for child in item_children:
res.update(explore(child))
return { name : { r:res[r] for r in res if r != 'Root' } }
itm = explore(db_items[1])
if 'Root' in itm:
return itm['Root']
else:
return itm
except dbus.exceptions.DBusException:
return None
gtk_click = None
def compute_gtk_menu(window_id):
try:
from Xlib import display, protocol, X
import dbus
dis = display.Display()
win = dis.create_resource_object('window', window_id)
def get_prop(prop):
atom = win.get_full_property(dis.get_atom(prop), X.AnyPropertyType)
if atom:
return atom.value
gtk_bus_name = get_prop('_GTK_UNIQUE_BUS_NAME')
gtk_menu = get_prop('_GTK_MENUBAR_OBJECT_PATH')
gtk_app = get_prop('_GTK_APPLICATION_OBJECT_PATH')
gtk_win = get_prop('_GTK_WINDOW_OBJECT_PATH')
gtk_unity = get_prop('_UNITY_OBJECT_PATH')
gtk_bus_name, gtk_menu, gtk_app, gtk_win, gtk_unity = \
[i.decode("utf8") if isinstance(i, bytes) \
else i for i in [gtk_bus_name, gtk_menu, gtk_app, gtk_win, gtk_unity]]
gtk_actions = list(set([gtk_win, gtk_menu, gtk_app, gtk_unity]))
if not gtk_bus_name or not gtk_menu:
return None
session_bus = dbus.SessionBus()
gtk_menu_o = session_bus.get_object(gtk_bus_name, gtk_menu)
gtk_menu_i = dbus.Interface(gtk_menu_o, dbus_interface='org.gtk.Menus')
gtk_menubar_action_dict = dict()
gtk_menubar_action_target_dict = dict()
usedLayers = []
def Start(i):
usedLayers.append(i)
return gtk_menu_i.Start([i])
no_data = dict()
no_data["not used"] = "not used"
def explore(parent):
res = {}
for node in parent:
content = node[2]
for element in content:
if 'label' in element:
if ':section' in element or ':submenu' in element:
if ':submenu' in element:
res.update({ element['label'].replace('_', ''):
explore(Start(element[':submenu'][0])) })
if ':section' in element:
if element[':section'][0] != node[0]:
res.update(explore(Start(element[':submenu'][0])))
elif 'action' in element:
menu_action = str(element['action']).split(".",1)[1]
target = []
if 'target' in element:
target = element['target']
if not isinstance(target, list):
target = [target]
res.update({ element['label'].replace('_', ''): (menu_action, target) })
else:
if ':submenu' in element or ':section' in element:
if ':section' in element:
if element[':section'][0] != node[0]:
res.update(explore(Start(element[':section'][0])))
if ':submenu' in element:
res.update(explore(Start(element[':submenu'][0])))
return res
menuKeys = explore(Start(0))
gtk_menu_i.End(usedLayers)
def click(menu_action, target):
for action_path in gtk_actions:
if action_path == None:
continue
try:
ao = session_bus.get_object(gtk_bus_name, action_path)
ai = dbus.Interface(ao, dbus_interface='org.gtk.Actions')
ai.Activate(menu_action, target, no_data)
except Exception as e:
print('_'*20)
print(action_path)
print(str(e))
global gtk_click
gtk_click = click
return menuKeys
except:
return None
def compute_menu(window_id):
amen = compute_gtk_menu(window_id)
if amen == None:
amen = compute_appmenu_menu(window_id)
return amen
def compute_highlight(ws, window):
highlight_groups = []
desc = [d.layout in ['tabbed', 'stacked'] for d in ws.descendants() \
if d.parent.type == 'workspace' and d.floating in ['user_off', 'auto_off'] \
and d.type != 'floating_con']
if len([w for w in ws.leaves() if w.floating in ['user_off', 'auto_off']]) == 1:
highlight_groups = ['active_window_title:single', 'active_window_title']
elif len(desc) > 0 and all(desc) and (not window or window.floating) in ['user_on', 'auto_on']:
highlight_groups = ['active_window_title:stacked_unfocused',
'active_window_title']
elif len(desc) > 0 and all(desc) and (not window or not window.focused):
highlight_groups = ['active_window_title:stacked_unfocused',
'active_window_title']
elif len(desc) > 0 and all(desc):
highlight_groups = ['active_window_title:stacked', 'active_window_title']
else:
highlight_groups = ['active_window_title']
return highlight_groups
def split_layer(layer, max_length, item_length):
res = []
ln = 0
cur = {}
lst = list(layer.keys())
for i in range(0, len(lst)):
cl = min(len(lst[i]), item_length)
if ln + cl > max_length:
res += [cur]
cur = {}
ln = 0
ln += cl
cur.update({lst[i]: layer[lst[i]]})
if ln:
res += [cur]
return res
active_window_state = 0
last_active_window = None
last_active_window_name = None
last_oneshot = 0
menu_items = None
current_layer = None
start = 0
path = []
[docs]@requires_segment_info
def active_window(pl, segment_info, cutoff=100, global_menu=False, item_length=20, \
max_width=80, auto_expand=False, show_empty=False, **kwargs):
'''
Returns the title of the currently active window.
To enhance the global menu support, add the following to your ``.bashrc``:
.. code-block:: shell
if [ -n "$GTK_MODULES" ]; then
GTK_MODULES="${GTK_MODULES}:appmenu-gtk-module"
else
GTK_MODULES="appmenu-gtk-module"
fi
if [ -z "$UBUNTU_MENUPROXY" ]; then
UBUNTU_MENUPROXY=1
fi
export GTK_MODULES
export UBUNTU_MENUPROXY
:param int cutoff:
Maximum title length. If the title is longer, the window_class is used instead.
:param boolean global_menu:
Activate global menu support (experimental)
:param int item_length:
Maximum length of a menu item.
:param int max_width:
Maximum total length of the content.
:param bool auto_expand:
Add spaces to center the segment.
:param bool show_empty:
Show the sehment if no window is focused.
Highlight groups used: ``active_window_title:single`` or ``active_window_title:stacked_unfocused`` or ``active_window_title:stacked`` or ``active_window_title``.
'''
global active_window_state
global last_active_window
global last_active_window_name
global last_oneshot
global menu_items
global current_layer
global start
global path
conn = get_i3_connection()
if len(path) > 0:
max_width = max_width - len('Main Menu')
if len(path) > 1:
max_width = max_width - len('Up a Level')
channel_name = 'i3wm.active_window'
channel_value = None
if global_menu and 'payloads' in segment_info and channel_name in segment_info['payloads']:
channel_value = segment_info['payloads'][channel_name]
focused = conn.get_tree().find_focused()
ws = focused.workspace()
o_name = [w.output for w in conn.get_workspaces() \
if w.name == ws.name][0]
output = segment_info.get('output')
if last_active_window != focused.window and last_active_window:
# Please, don't kill me for this line
if global_menu and last_active_window and 'payloads' in segment_info \
and 'i3wm.workspaces' in segment_info['payloads']:
segment_info['payloads']['i3wm.workspaces'] = None
last_active_window = None
last_active_window_name = None
active_window_state = 0
start = 0
menu_items = None
current_layer = None
path = []
if o_name != output:
if not show_empty:
return None
# Get visible workspace
ws = [w for w in conn.get_workspaces() if w.output == output \
and w.visible]
if not len(ws):
return None
ws = [w for w in focused.workspaces() if w.name == ws[0].name]
if not len(ws):
return None
highlight = compute_highlight(ws[0], None)
return [{
'contents': '',
'width': 'auto',
'highlight_groups': highlight,
'click_values': { 'segment': '' },
'payload_name': 'DROP'
}]
if focused.name == focused.workspace().name:
if not show_empty:
return None
return [{
'contents': '',
'width': 'auto',
'highlight_groups': compute_highlight(ws, None),
'click_values': { 'segment': '' },
'payload_name': 'DROP'
}]
cont = [focused.name]
if cutoff and len(cont) > cutoff:
cont = [focused.window_class]
main_cont = cont[0]
if channel_value and not isinstance(channel_value, str) and len(channel_value) == 2 \
and channel_value[0].startswith('menu_click') and channel_value[1] > last_oneshot:
last_oneshot = channel_value[1]
click_area = channel_value[0].split(':')[1]
if active_window_state == 0:
last_active_window = focused.window
last_active_window_name = focused.name
menu_items = compute_menu(focused.window)
current_layer = split_layer(menu_items, max_width, item_length)
path = []
active_window_state = 1
elif click_area == 'Main Menu':
start = 0
current_layer = split_layer(menu_items, max_width, item_length)
path = []
elif click_area == 'Up a Level':
start = 0
path = path[:-1]
current_layer = menu_items
cnt = 0
for i in path:
current_layer = current_layer[i]
cnt += 1
if cnt > 1:
current_layer.update({'Up a Level': ''})
current_layer.update({'Main Menu': ''})
current_layer = split_layer(current_layer, max_width, item_length)
elif click_area == '$<':
start = max(0, start - 1)
elif click_area == '$>':
start = min(len(current_layer) - 1, start + 1)
elif click_area != '':
if isinstance(current_layer[start][click_area], dict):
current_layer = current_layer[start][click_area]
if len(path) > 0:
current_layer.update({'Up a Level': ''})
current_layer.update({'Main Menu': ''})
current_layer = split_layer(current_layer, max_width, item_length)
start = 0
path += [click_area]
else:
if isinstance(current_layer[start][click_area], tuple):
gtk_click(current_layer[start][click_area][0], current_layer[start][click_area][1])
else:
current_layer[start][click_area]()
current_layer = split_layer(menu_items, max_width, item_length)
path = []
start = 0
if channel_value and not isinstance(channel_value, str) and len(channel_value) == 2 \
and channel_value[0] == 'menu_off' and channel_value[1] > last_oneshot:
last_oneshot = channel_value[1]
active_window_state = 0
if channel_value and not isinstance(channel_value, str) and len(channel_value) == 2 \
and channel_value[0] == 'menu_on' and channel_value[1] > last_oneshot:
last_oneshot = channel_value[1]
active_window_state = 1
if last_active_window != focused.window:
last_active_window = focused.window
last_active_window_name = focused.name
menu_items = compute_menu(focused.window)
current_layer = split_layer(menu_items, max_width, item_length)
path = []
start = 0
if current_layer and active_window_state:
cont = list(current_layer[start].keys())
highlight = compute_highlight(ws, focused)
res = []
show_prev = start > 0 and active_window_state > 0
show_next = current_layer and active_window_state > 0 \
and start < len(current_layer) - 1
def shorten(string, length):
if len(string) < length - 1:
return string
return string[:length-1] + '…'
if global_menu and (auto_expand or show_prev):
res += [{
'contents': '<' if show_prev else '',
'highlight_groups': highlight,
'payload_name': channel_name if show_prev else 'DROP',
'draw_soft_divider': False,
'draw_inner_divider': True if show_prev else False,
'width': 'auto' if auto_expand else None,
'align': 'r',
'click_values': { 'segment': '$<' }
}]
total_len = 0
for i in range(0, len(cont)):
total_len += min(len(cont[i]), item_length) if cont[i] != main_cont \
else min(len(cont[i]), max_width)
def truncate(pl, wd, seg):
nl = max(5 * len(cont), int(total_len) - wd)
if int(total_len) <= nl:
return seg['contents']
shr = (int(total_len) - nl) // len(cont)
return shorten(seg['contents'], len(seg['contents']) - shr)
for i in range(0, len(cont)):
draw_div = i != 0 or show_prev or not auto_expand
res += [{
'contents': (shorten(cont[i],item_length) if cont[i] != main_cont \
else shorten(cont[i], max_width)),
'highlight_groups': highlight,
'payload_name': channel_name,
'draw_inner_divider': draw_div,
'draw_soft_divider': True ,
'click_values': { 'segment': cont[i] if cont[i] != main_cont else '' },
'truncate': truncate
}]
if global_menu and (auto_expand or show_next):
res += [{
'contents': '>' if show_next else '',
'highlight_groups': highlight,
'payload_name': channel_name if show_next else 'DROP',
'width': 'auto' if auto_expand else None,
'click_values': { 'segment': '$>' },
'draw_inner_divider': bool(show_next),
}]
return res