# Copyright (c) 2010-2011 Aldo Cortesi
# Copyright (c) 2010 Philip Kranz
# Copyright (c) 2011 Mounier Florian
# Copyright (c) 2011 Paul Colomiets
# Copyright (c) 2011-2012 roger
# Copyright (c) 2011-2012, 2014 Tycho Andersen
# Copyright (c) 2012 Dustin Lacewell
# Copyright (c) 2012 Laurie Clark-Michalek
# Copyright (c) 2012-2014 Craig Barnes
# Copyright (c) 2013 Tao Sauvage
# Copyright (c) 2014 ramnes
# Copyright (c) 2014 Sean Vig
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import glob
import os
import string
from .. import bar, xkeysyms, xcbq, command
from . import base
class NullCompleter:
def __init__(self, qtile):
self.qtile = qtile
self.thisfinal = ""
def actual(self):
return self.thisfinal
def reset(self):
pass
def complete(self, txt):
return txt
class FileCompleter:
def __init__(self, qtile, _testing=False):
self._testing = _testing
self.qtile = qtile
self.thisfinal = None
self.reset()
def actual(self):
return self.thisfinal
def reset(self):
self.lookup = None
def complete(self, txt):
"""
Returns the next completion for txt, or None if there is no completion.
"""
if not self.lookup:
self.lookup = []
if txt == "" or txt[0] not in "~/":
txt = "~/" + txt
path = os.path.expanduser(txt)
if os.path.isdir(path):
files = glob.glob(os.path.join(path, "*"))
prefix = txt
else:
files = glob.glob(path + "*")
prefix = os.path.dirname(txt)
prefix = prefix.rstrip("/") or "/"
for f in files:
display = os.path.join(prefix, os.path.basename(f))
if os.path.isdir(f):
display += "/"
self.lookup.append((display, f))
self.lookup.sort()
self.offset = -1
self.lookup.append((txt, txt))
self.offset += 1
if self.offset >= len(self.lookup):
self.offset = 0
ret = self.lookup[self.offset]
self.thisfinal = ret[1]
return ret[0]
class QshCompleter:
def __init__(self, qtile):
self.qtile = qtile
self.client = command.CommandRoot(self.qtile)
self.thisfinal = None
self.reset()
def actual(self):
return self.thisfinal
def reset(self):
self.lookup = None
self.path = ''
self.offset = -1
def complete(self, txt):
txt = txt.lower()
if not self.lookup:
self.lookup = []
path = txt.split('.')[:-1]
self.path = '.'.join(path)
term = txt.split('.')[-1]
if len(self.path) > 0:
self.path += '.'
contains_cmd = 'self.client.%s_contains' % self.path
try:
contains = eval(contains_cmd)
except AttributeError:
contains = []
for obj in contains:
if obj.lower().startswith(term):
self.lookup.append((obj, obj))
commands_cmd = 'self.client.%scommands()' % self.path
try:
commands = eval(commands_cmd)
except (command.CommandError, AttributeError):
commands = []
for cmd in commands:
if cmd.lower().startswith(term):
self.lookup.append((cmd + '()', cmd + '()'))
self.offset = -1
self.lookup.append((term, term))
self.offset += 1
if self.offset >= len(self.lookup):
self.offset = 0
ret = self.lookup[self.offset]
self.thisfinal = self.path + ret[0]
return self.path + ret[0]
class GroupCompleter:
def __init__(self, qtile):
self.qtile = qtile
self.thisfinal = None
self.lookup = None
self.offset = None
def actual(self):
"""
Returns the current actual value.
"""
return self.thisfinal
def reset(self):
self.lookup = None
self.offset = -1
def complete(self, txt):
"""
Returns the next completion for txt, or None if there is no completion.
"""
txt = txt.lower()
if not self.lookup:
self.lookup = []
for group in self.qtile.groupMap.keys():
if group.lower().startswith(txt):
self.lookup.append((group, group))
self.lookup.sort()
self.offset = -1
self.lookup.append((txt, txt))
self.offset += 1
if self.offset >= len(self.lookup):
self.offset = 0
ret = self.lookup[self.offset]
self.thisfinal = ret[1]
return ret[0]
class WindowCompleter:
def __init__(self, qtile):
self.qtile = qtile
self.thisfinal = None
self.lookup = None
self.offset = None
def actual(self):
"""
Returns the current actual value.
"""
return self.thisfinal
def reset(self):
self.lookup = None
self.offset = -1
def complete(self, txt):
"""
Returns the next completion for txt, or None if there is no completion.
"""
if not self.lookup:
self.lookup = []
for wid, window in self.qtile.windowMap.items():
if window.group and window.name.lower().startswith(txt):
self.lookup.append((window.name, wid))
self.lookup.sort()
self.offset = -1
self.lookup.append((txt, txt))
self.offset += 1
if self.offset >= len(self.lookup):
self.offset = 0
ret = self.lookup[self.offset]
self.thisfinal = ret[1]
return ret[0]
class CommandCompleter:
DEFAULTPATH = "/bin:/usr/bin:/usr/local/bin"
def __init__(self, qtile, _testing=False):
"""
_testing: disables reloading of the lookup table
to make testing possible.
"""
self.lookup = None
self.offset = None
self.thisfinal = None
self._testing = _testing
def actual(self):
"""
Returns the current actual value.
"""
return self.thisfinal
def executable(self, fpath):
return os.access(fpath, os.X_OK)
def reset(self):
self.lookup = None
self.offset = -1
def complete(self, txt):
"""
Returns the next completion for txt, or None if there is no completion.
"""
if not self.lookup:
if not self._testing:
# Lookup is a set of (display value, actual value) tuples.
self.lookup = []
if txt and txt[0] in "~/":
path = os.path.expanduser(txt)
if os.path.isdir(path):
files = glob.glob(os.path.join(path, "*"))
prefix = txt
else:
files = glob.glob(path + "*")
prefix = os.path.dirname(txt)
prefix = prefix.rstrip("/") or "/"
for f in files:
if self.executable(f):
display = os.path.join(prefix, os.path.basename(f))
if os.path.isdir(f):
display += "/"
self.lookup.append((display, f))
else:
dirs = os.environ.get("PATH", self.DEFAULTPATH).split(":")
for didx, d in enumerate(dirs):
try:
for cmd in glob.glob(os.path.join(d, "%s*" % txt)):
if self.executable(cmd):
self.lookup.append(
(
os.path.basename(cmd),
cmd
),
)
except OSError:
pass
self.lookup.sort()
self.offset = -1
self.lookup.append((txt, txt))
self.offset += 1
if self.offset >= len(self.lookup):
self.offset = 0
ret = self.lookup[self.offset]
self.thisfinal = ret[1]
return ret[0]
[docs]class Prompt(base._TextBox):
"""
A widget that prompts for user input. Input should be started using the
.startInput method on this class.
"""
completers = {
"file": FileCompleter,
"qsh": QshCompleter,
"cmd": CommandCompleter,
"group": GroupCompleter,
"window": WindowCompleter,
None: NullCompleter
}
defaults = [("cursorblink", 0.5, "Cursor blink rate. 0 to disable."),
("prompt", "{prompt}: ", "Text displayed at the prompt")]
def __init__(self, name="prompt", **config):
base._TextBox.__init__(self, "", bar.CALCULATED, **config)
self.add_defaults(Prompt.defaults)
self.name = name
self.active = False
self.blink = False
self.completer = None
def _configure(self, qtile, bar):
base._TextBox._configure(self, qtile, bar)
def startInput(self, prompt, callback,
complete=None, strict_completer=False):
"""
complete: Tab-completion. Can be None, or "cmd".
Displays a prompt and starts to take one line of keyboard input
from the user. When done, calls the callback with the input string
as argument.
prompt = text displayed at the prompt, e.g. "spawn: "
callback = function to call with returned value.
complete = completer to use.
strict_completer = When True the retuen value wil be the exact
completer result where available.
"""
if self.cursorblink and not self.active:
self.timeout_add(self.cursorblink, self._blink)
self.display = self.prompt.format(prompt=prompt)
self.active = True
self.userInput = ""
self.callback = callback
self.completer = self.completers[complete](self.qtile)
self.strict_completer = strict_completer
self._update()
self.bar.widget_grab_keyboard(self)
def _calculate_real_width(self):
if self.blink:
return min(
self.layout.width,
self.bar.width
) + self.actual_padding * 2
else:
_text = self.text
self.text = _text + "_"
width = min(
self.layout.width,
self.bar.width
) + self.actual_padding * 2
self.text = _text
return width
def calculate_width(self):
if self.text:
return self._calculate_real_width()
else:
return 0
def _blink(self):
self.blink = not self.blink
self._update()
if self.active:
self.timeout_add(self.cursorblink, self._blink)
def _update(self):
if self.active:
self.text = "%s%s" % (self.display, self.userInput)
if self.blink:
self.text = self.text + "_"
else:
self.text = self.text
else:
self.text = ""
self.bar.draw()
def handle_KeyPress(self, e):
"""
KeyPress handler for the minibuffer.
Currently only supports ASCII characters.
"""
state = e.state & ~(self.qtile.numlockMask)
keysym = self.qtile.conn.keycode_to_keysym(e.detail, state)
if keysym == xkeysyms.keysyms['Tab']:
self.userInput = self.completer.complete(self.userInput)
else:
actual_value = self.completer.actual()
self.completer.reset()
if keysym < 127 and chr(keysym) in string.printable:
# No LookupString in XCB... oh,
# the shame! Unicode users beware!
self.userInput += chr(keysym)
elif (keysym == xkeysyms.keysyms['BackSpace'] and
len(self.userInput) > 0):
self.userInput = self.userInput[:-1]
elif keysym == xkeysyms.keysyms['Escape']:
self.active = False
self.bar.widget_ungrab_keyboard()
elif keysym == xkeysyms.keysyms['Return']:
self.active = False
self.bar.widget_ungrab_keyboard()
if self.strict_completer:
self.callback(actual_value or self.userInput)
else:
self.callback(self.userInput)
self._update()
def cmd_fake_keypress(self, key):
class Dummy:
pass
d = Dummy()
keysym = xcbq.keysyms[key]
d.detail = self.qtile.conn.keysym_to_keycode(keysym)
d.state = 0
self.handle_KeyPress(d)
def cmd_info(self):
"""
Returns a dictionary of info for this object.
"""
return dict(
name=self.name,
width=self.width,
text=self.text,
active=self.active,
)