# -*- coding: utf-8 -*-
#
# entry.py - commander
#
# Copyright (C) 2010 - Jesse van den Kieboom
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
from gi.repository import GObject, GLib, Gdk, Gtk
import cairo
import os
import re
import inspect
import sys
import colorsys
import commander.commands as commands
import commands.completion
import commands.module
import commands.method
import commands.exceptions
import commands.accel_group
import commander.utils as utils
from history import History
from info import Info
from xml.sax import saxutils
import traceback
class Entry(Gtk.Box):
__gtype_name__ = "CommanderEntry"
def _show(self):
self._reveal.set_reveal_child(True)
def _hide(self):
self._reveal.set_reveal_child(False)
def __init__(self, view):
super(Entry, self).__init__()
self._view = view
view.connect("destroy", self._on_view_destroyed)
self._history = History(os.path.join(GLib.get_user_config_dir(), 'gedit/commander/history'))
self._history_prefix = None
self._prompt_text = None
self._accel_group = None
self._wait_timeout = 0
self._cancel_button = None
self._info = None
self._info_revealer = None
self._suspended = None
self._handlers = [
[0, Gdk.KEY_Up, self._on_history_move, -1],
[0, Gdk.KEY_Down, self._on_history_move, 1],
[None, Gdk.KEY_Return, self._on_execute, None],
[None, Gdk.KEY_KP_Enter, self._on_execute, None],
[0, Gdk.KEY_Tab, self._on_complete, None],
[0, Gdk.KEY_ISO_Left_Tab, self._on_complete, None]
]
self._re_complete = re.compile('("((?:\\\\"|[^"])*)"?|\'((?:\\\\\'|[^\'])*)\'?|[^\s]+)')
self._command_state = commands.Commands.State()
self.connect('destroy', self._on_destroy)
self._build_ui()
self._setup_keybindings()
self._attach()
def view(self):
return self._view
def _setup_keybindings(self):
css = Gtk.CssProvider()
css.load_from_data(bytes("""
@binding-set terminal-like-bindings {
unbind "<Control>A";
bind "<Control>W" { "delete-from-cursor" (word-ends, -1) };
bind "<Control>A" { "move-cursor" (buffer-ends, -1, 0) };
bind "<Control>U" { "delete-from-cursor" (display-line-ends, -1) };
bind "<Control>K" { "delete-from-cursor" (display-line-ends, 1) };
bind "<Control>E" { "move-cursor" (buffer-ends, 1, 0) };
bind "Escape" { "delete-from-cursor" (display-lines, 1) };
}
GtkEntry#gedit-commander-entry {
gtk-key-bindings: terminal-like-bindings;
background-image: none;
box-shadow: 0 0;
transition: none;
border: 0;
}
""", 'utf-8'))
self._entry.get_style_context().add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
def _find_overlay(self, view):
parent = view.get_parent()
while not isinstance(parent, Gtk.Overlay):
parent = parent.get_parent()
return parent
def _build_ui(self):
self.set_orientation(Gtk.Orientation.VERTICAL)
self._overlay = self._find_overlay(self._view)
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
hbox.show()
self.pack_end(hbox, False, False, 0)
self._info_revealer = Gtk.Revealer()
self._info_revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_UP)
self._info_revealer.set_transition_duration(150)
self.pack_start(self._info_revealer, False, False, 0)
self._info_revealer.connect('notify::child-revealed', self._on_info_revealer_child_revealed)
self._prompt_label = Gtk.Label(label='<b>>>></b>', use_markup=True)
self._prompt_label.set_margin_top(3)
self._prompt_label.set_margin_bottom(3)
self._prompt_label.set_margin_start(3)
self._prompt_label.show()
hbox.add(self._prompt_label)
self._entry = Gtk.Entry()
self._entry.set_has_frame(False)
self._entry.set_name('gedit-commander-entry')
self._entry.set_hexpand(True)
self._entry.set_margin_top(3)
self._entry.set_margin_bottom(3)
self._entry.set_margin_end(3)
self._entry.connect('key-press-event', self._on_entry_key_press)
self._entry.show()
hbox.add(self._entry)
self._copy_style_from_view()
self._view_style_updated_id = self._view.connect('style-updated', self._on_view_style_updated)
def _on_view_destroyed (self, widget, user_data=None):
self._view.disconnect(self._view_style_updated_id)
self._view_style_updated_id = None
def _on_view_style_updated(self, widget):
self._copy_style_from_view()
@property
def _border_color(self):
style = self._view.get_buffer().get_style_scheme().get_style('right-margin')
if not style is None and style.props.foreground_set:
color = Gdk.RGBA()
color.parse(style.props.foreground)
else:
color = self._get_background_color(Gtk.StateFlags.NORMAL, 'bottom').copy()
color.red = 1 - color.red
color.green = 1 - color.green
color.blue = 1 - color.blue
color.alpha = 0.3
return color
def _get_background_color(self, state, cls=None):
context = self._view.get_style_context()
context.save()
if not cls is None:
context.add_class(cls)
ret = context.get_background_color(state)
context.restore()
return ret
def _get_foreground_color(self, state, cls=None):
context = self._view.get_style_context()
context.save()
if not cls is None:
context.add_class(cls)
ret = context.get_color(state)
context.restore()
return ret
def _get_font(self):
context = self._view.get_style_context()
return context.get_font(Gtk.StateFlags.NORMAL)
def _styled_widgets(self):
widgets = [self, self._entry, self._prompt_label]
if not self._info is None:
widgets.append(self._info)
widgets.append(self._info.text_view)
return widgets
def _modify_bg(self, col, widget):
if self._info is None or (self._info.text_view != widget and self._info != widget):
return col
d = 0.1
h, l, s = colorsys.rgb_to_hls(col.red, col.green, col.blue)
if l < 0.5:
factor = 1 + d
else:
factor = 1 - d
l = max(0, min(1, l * factor))
s = max(0, min(1, s * factor))
r, g, b = colorsys.hls_to_rgb(h, l, s)
return Gdk.RGBA(r, g, b, col.alpha)
def _copy_style_from_view(self, widgets=None):
font = self._get_font()
fg = self._get_foreground_color(Gtk.StateFlags.NORMAL, 'bottom')
bg = self._get_background_color(Gtk.StateFlags.NORMAL, 'bottom')
fgsel = self._get_foreground_color(Gtk.StateFlags.SELECTED)
bgsel = self._get_background_color(Gtk.StateFlags.SELECTED)
cursor = self._view.style_get_property('cursor-color')
if not cursor is None:
cursor = Gdk.RGBA.from_color(cursor)
secondary_cursor = self._view.style_get_property('secondary-cursor-color')
if not secondary_cursor is None:
secondary_cursor = Gdk.RGBA.from_color(secondary_cursor)
if widgets is None:
widgets = self._styled_widgets()
for widget in widgets:
widget.override_color(Gtk.StateFlags.NORMAL, fg)
widget.override_background_color(Gtk.StateFlags.NORMAL, self._modify_bg(bg, widget))
widget.override_color(Gtk.StateFlags.SELECTED, fgsel)
widget.override_background_color(Gtk.StateFlags.SELECTED, self._modify_bg(bgsel, widget))
widget.override_font(font)
widget.override_cursor(cursor, secondary_cursor)
def _attach(self):
reveal = Gtk.Revealer()
reveal.add(self)
self.show()
reveal.set_transition_type(Gtk.RevealerTransitionType.SLIDE_UP)
reveal.set_transition_duration(200)
reveal.set_valign(Gtk.Align.END)
reveal.set_halign(Gtk.Align.FILL)
self._overlay.add_overlay(reveal)
reveal.show()
reveal.set_reveal_child(True)
self._reveal = reveal
self._entry.grab_focus()
def grab_focus(self):
self._entry.grab_focus()
def _on_entry_key_press(self, widget, evnt):
state = evnt.state & Gtk.accelerator_get_default_mod_mask()
text = self._entry.get_text()
if evnt.keyval == Gdk.KEY_Escape:
if not self._info is None:
if not self._suspended is None:
self._suspended.resume()
if not self._info is None:
self._info_revealer.set_reveal_child(False)
self._entry.set_sensitive(True)
elif self._accel_group:
self._accel_group = self._accel_group.parent
if not self._accel_group or not self._accel_group.parent:
self._entry.set_editable(True)
self._accel_group = None
self._prompt()
elif text:
self._entry.set_text('')
elif self._command_state:
self._command_state.clear()
self._prompt()
else:
self._view.grab_focus()
self._reveal.set_reveal_child(False)
return True
if state or self._accel_group:
# Check if it should be handled by the accel group
group = self._accel_group
if not self._accel_group:
group = commands.Commands().accelerator_group()
accel = group.activate(evnt.keyval, state)
if isinstance(accel, commands.accel_group.AccelGroup):
self._accel_group = accel
self._entry.set_text('')
self._entry.set_editable(False)
self._prompt()
return True
elif isinstance(accel, commands.accel_group.AccelCallback):
self._entry.set_editable(True)
self._run_command(lambda: accel.activate(self._command_state, self))
return True
if not self._entry.get_editable():
return True
for handler in self._handlers:
if (handler[0] == None or handler[0] == state) and evnt.keyval == handler[1] and handler[2](handler[3], state):
return True
if not self._info is None and self._info.is_empty:
self._info_revealer.set_reveal_child(False)
self._history_prefix = None
return False
def _on_history_move(self, direction, modifier):
pos = self._entry.get_position()
self._history.update(self._entry.get_text())
if self._history_prefix == None:
if len(self._entry.get_text()) == pos:
self._history_prefix = self._entry.get_chars(0, pos)
else:
self._history_prefix = ''
if self._history_prefix == None:
hist = ''
else:
hist = self._history_prefix
next = self._history.move(direction, hist)
if next != None:
self._entry.set_text(next)
self._entry.set_position(-1)
return True
def _prompt(self, pr=''):
self._prompt_text = pr
if self._accel_group != None:
pr = '<i>%s</i>' % (saxutils.escape(self._accel_group.full_name()),)
if not pr:
pr = ''
else:
pr = ' ' + pr
self._prompt_label.set_markup('<b>>>></b>%s' % pr)
def _make_info(self):
if self._info is None:
self._info = Info()
self._copy_style_from_view([self._info, self._info.text_view])
self._info_revealer.add(self._info)
self._info.show()
self._info_revealer.show()
self._info_revealer.set_reveal_child(True)
self._info.connect('destroy', self._on_info_destroy)
def _on_info_revealer_child_revealed(self, widget, pspec):
if not self._info_revealer.get_child_revealed():
self._info.destroy()
self._info_revealer.hide()
def _on_info_destroy(self, widget):
self._info = None
def info_show(self, text='', use_markup=False):
self._make_info()
self._info.add_lines(text, use_markup)
def info_status(self, text):
self._make_info()
self._info.status(text)
def _info_add_action(self, stock, callback, data=None):
self._make_info()
return self._info.add_action(stock, callback, data)
def _command_history_done(self):
self._history.add(self._entry.get_text())
self._history_prefix = None
self._entry.set_text('')
def _on_wait_cancel(self):
self._on_execute(None, 0)
def _show_wait_cancel(self):
self._cancel_button = self._info_add_action('process-stop-symbolic', self._on_wait_cancel)
self.info_status('<i>Waiting to finish\u2026</i>')
self._wait_timeout = 0
return False
def _complete_word_match(self, match):
for i in (3, 2, 0):
if match.group(i) != None:
return [match.group(i), match.start(i), match.end(i)]
def _on_suspend_resume(self):
if self._wait_timeout:
GLib.source_remove(self._wait_timeout)
self._wait_timeout = 0
else:
if not self._cancel_button is None:
self._cancel_button.destroy()
self._cancel_button = None
self.info_status(None)
self._entry.set_sensitive(True)
self._command_history_done()
if self._entry.props.has_focus or (not self._info is None and not self._info.is_empty):
self._entry.grab_focus()
def _run_command(self, cb):
self._suspended = None
try:
ret = cb()
except Exception as e:
sys.stderr.write(self._format_trace() + '\n')
self._command_history_done()
self._command_state.clear()
self._prompt()
# Show error in info
self.info_show('<b><span color="#f66">Error:</span></b> ' + saxutils.escape(str(e)), True)
if not isinstance(e, commands.exceptions.Execute):
self.info_show(self._format_trace(), False)
return None
mod = sys.modules['commander.commands.result']
if ret == mod.Result.SUSPEND:
# Wait for it...
self._suspended = ret
ret.register(self._on_suspend_resume)
self._wait_timeout = GLib.timeout_add(500, self._show_wait_cancel)
self._entry.set_sensitive(False)
else:
self._command_history_done()
self._prompt('')
if ret == mod.Result.PROMPT:
self._prompt(ret.prompt)
elif (ret == None or ret == mod.HIDE) and not self._prompt_text and (self._info is None or self._info.is_empty):
self._command_state.clear()
self._view.grab_focus()
self._reveal.set_reveal_child(False)
else:
self._entry.grab_focus()
return ret
def _format_trace(self):
tp, val, tb = sys.exc_info()
origtb = tb
thisdir = os.path.dirname(__file__)
# Skip frames up until after the last entry.py...
while not tb is None:
filename = tb.tb_frame.f_code.co_filename
dname = os.path.dirname(filename)
if not dname.startswith(thisdir):
break
tb = tb.tb_next
msg = traceback.format_exception(tp, val, tb)
r = ''.join(msg[0:-1])
# This is done to prevent cyclic references, see python
# documentation on sys.exc_info
del origtb
return r
def _on_execute(self, dummy, modifier):
if not self._info is None and not self._suspended:
self._info_revealer.set_reveal_child(False)
text = self._entry.get_text().strip()
words = list(self._re_complete.finditer(text))
wordsstr = []
for word in words:
spec = self._complete_word_match(word)
wordsstr.append(spec[0])
if not wordsstr and not self._command_state:
self._entry.set_text('')
return
self._run_command(lambda: commands.Commands().execute(self._command_state, text, words, wordsstr, self, modifier))
return True
def _on_complete(self, dummy, modifier):
# First split all the text in words
text = self._entry.get_text()
pos = self._entry.get_position()
words = list(self._re_complete.finditer(text))
wordsstr = []
for word in words:
spec = self._complete_word_match(word)
wordsstr.append(spec[0])
# Find out at which word the cursor actually is
# Examples:
# * hello world|
# * hello| world
# * |hello world
# * hello wor|ld
# * hello | world
# * "hello world|"
posidx = None
for idx in range(0, len(words)):
spec = self._complete_word_match(words[idx])
if words[idx].start(0) > pos:
# Empty space, new completion
wordsstr.insert(idx, '')
words.insert(idx, None)
posidx = idx
break
elif spec[2] == pos:
# At end of word, resume completion
posidx = idx
break
elif spec[1] <= pos and spec[2] > pos:
# In middle of word, do not complete
return True
if posidx == None:
wordsstr.append('')
words.append(None)
posidx = len(wordsstr) - 1
# First word completes a command, if not in any special 'mode'
# otherwise, relay completion to the command, or complete by advice
# from the 'mode' (prompt)
cmds = commands.Commands()
if not self._command_state and posidx == 0:
# Complete the first command
ret = commands.completion.command(words=wordsstr, idx=posidx)
else:
complete = None
realidx = posidx
if not self._command_state:
# Get the command first
cmd = commands.completion.single_command(wordsstr, 0)
realidx -= 1
ww = wordsstr[1:]
else:
cmd = self._command_state.top()
ww = wordsstr
if cmd:
complete = cmd.autocomplete_func()
if not complete:
return True
# 'complete' contains a dict with arg -> func to do the completion
# of the named argument the command (or stack item) expects
args, varargs = cmd.args()
# Remove system arguments
s = ['argstr', 'args', 'entry', 'view']
args = list(filter(lambda x: not x in s, args))
if realidx < len(args):
arg = args[realidx]
elif varargs:
arg = '*'
else:
return True
if not arg in complete:
return True
func = complete[arg]
try:
spec = utils.getargspec(func)
if not ww:
ww = ['']
kwargs = {
'words': ww,
'idx': realidx,
'view': self._view
}
if not spec.keywords:
for k in list(kwargs.keys()):
if not k in spec.args:
del kwargs[k]
ret = func(**kwargs)
except Exception as e:
# Can be number of arguments, or return values or simply buggy
# modules
print(e)
traceback.print_exc()
return True
if not ret or not ret[0]:
return True
res = ret[0]
completed = ret[1]
if len(ret) > 2:
after = ret[2]
else:
after = ' '
# Replace the word
if words[posidx] == None:
# At end of everything, just append
spec = None
self._entry.insert_text(completed, self._entry.get_text_length())
self._entry.set_position(-1)
else:
spec = self._complete_word_match(words[posidx])
self._entry.delete_text(spec[1], spec[2])
self._entry.insert_text(completed, spec[1])
self._entry.set_position(spec[1] + len(completed))
if len(res) == 1:
# Full completion
lastpos = self._entry.get_position()
if not isinstance(res[0], commands.module.Module) or not res[0].commands():
if words[posidx] and after == ' ' and (words[posidx].group(2) != None or words[posidx].group(3) != None):
lastpos = lastpos + 1
self._entry.insert_text(after, lastpos)
self._entry.set_position(lastpos + 1)
elif completed == wordsstr[posidx] or not res[0].method:
self._entry.insert_text('.', lastpos)
self._entry.set_position(lastpos + 1)
if not self._info is None:
self._info_revealer.set_reveal_child(False)
else:
# Show popup with completed items
if not self._info is None:
self._info.clear()
ret = []
for x in res:
if isinstance(x, commands.method.Method):
ret.append('<b>' + saxutils.escape(x.name) + '</b> (<i>' + x.oneline_doc() + '</i>)')
else:
ret.append(str(x))
self.info_show("\n".join(ret), True)
return True
def do_draw(self, ctx):
ret = Gtk.Box.do_draw(self, ctx)
col = self._border_color
ctx.set_line_width(1)
ctx.set_source_rgba(col.red, col.green, col.blue, col.alpha)
w = self.get_allocated_width()
ctx.move_to(0, 0.5)
ctx.line_to(w, 0.5)
ctx.stroke()
if not self._info is None:
alloc = self._info_revealer.get_allocation()
y = alloc.y + alloc.height + 0.5
if y >= 3:
ctx.move_to(0, y)
ctx.line_to(w, y)
ctx.stroke()
return ret
def _on_destroy(self, widget):
# Note we do this not as an override because somehow something
# goes wrong when finalizing in that case, maybe self is NULL
# or something like that, and then gets some new empty instance?
if self._view_style_updated_id:
self._view.disconnect(self._view_style_updated_id)
self._history.save()
self._view = None
self._view_style_updated_id = None
# ex:ts=4:et