1
0
Fork 0

Adding upstream version 3.0.4.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
This commit is contained in:
Daniel Baumann 2025-06-23 00:14:50 +02:00
parent 1c8b56a4f5
commit 554424e00a
Signed by: daniel.baumann
GPG key ID: BCC918A2ABD66424
6822 changed files with 5440542 additions and 0 deletions

View file

@ -0,0 +1,5 @@
plugins += {
'name': 'python-console',
'srcs': [ 'python-console/pyconsole.py', 'python-console/python-console.py' ],
}

View file

@ -0,0 +1,949 @@
#
# pyconsole.py
#
# Copyright (C) 2004-2006 by Yevgen Muntyan <muntyan@math.tamu.edu>
# Portions of code by Geoffrey French.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public version 2.1 as
# published by the Free Software Foundation.
#
# See COPYING.lib file that comes with this distribution for full text
# of the license.
#
# This module 'runs' python interpreter in a TextView widget.
# The main class is Console, usage is:
# Console(locals=None, banner=None, completer=None, use_rlcompleter=True, start_script='') -
# it creates the widget and 'starts' interactive session; see the end
# of this file. If start_script is not empty, it pastes it as it was
# entered from keyboard.
#
# Console has "command" signal which is emitted when code is about to
# be executed. You may connect to it using console.connect or
# console.connect_after to get your callback ran before or after the
# code is executed.
#
# To modify output appearance, set attributes of console.stdout_tag and
# console.stderr_tag.
#
# Console may subclass a type other than gtk.TextView, to allow syntax
# highlighting and stuff,
# e.g.:
# console_type = pyconsole.ConsoleType(moo.edit.TextView)
# console = console_type(use_rlcompleter=False, start_script="import moo\nimport gtk\n")
#
# This widget is not a replacement for real terminal with python running
# inside: GtkTextView is not a terminal.
# The use case is: you have a python program, you create this widget,
# and inspect your program interiors.
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
gi.require_version('Gdk', '3.0')
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import Pango
import code
import sys
import keyword
import re
def pango_pixels(value):
# The PANGO_PIXELS macro is not accessible through GObject
# Introspection. Just reimplement it:
# #define PANGO_PIXELS(d) (((int)(d) + 512) >> 10)
return (value + 512) >> 10
# commonprefix() from posixpath
def _commonprefix(m):
"Given a list of pathnames, returns the longest common leading component"
if not m: return ''
prefix = m[0]
for item in m:
for i in range(len(prefix)):
if prefix[:i+1] != item[:i+1]:
prefix = prefix[:i]
if i == 0:
return ''
break
return prefix
class _ReadLine(object):
class Output(object):
def __init__(self, console, tag_name):
object.__init__(self)
self.buffer = console.get_buffer()
self.tag_name = tag_name
def write(self, text):
pos = self.buffer.get_iter_at_mark(self.buffer.get_insert())
self.buffer.insert_with_tags_by_name(pos, text, self.tag_name)
def flush(self):
# The output is just a GtkTextBuffer inside a GtkTexView so I
# believe it should be always in-sync without needing to be flushed.
# Nevertheless someone might be just copy-pasting plug-in code to
# test it, for instance. So let's just add a no-op flush() method to
# get a similar interface as the real sys.stdout which we overrode.
# It avoids useless and unexpected code failure.
pass
class History(object):
def __init__(self, initial_history):
object.__init__(self)
if initial_history is None or len(initial_history) == 0:
initial_history = ['']
elif initial_history[0] != '':
initial_history = [''] + initial_history
self.items = initial_history
self.ptr = 0
self.edited = {}
def commit(self, text):
if text and self.items[-1] != text:
self.items.append(text)
self.ptr = 0
self.edited = {}
def get(self, dir, text):
if len(self.items) == 1:
return None
if text != self.items[self.ptr]:
self.edited[self.ptr] = text
elif self.ptr in self.edited:
del self.edited[self.ptr]
# Do not cycle. It's confusing.
if (self.ptr == 0 and dir > 0) or self.ptr + dir >= len(self.items):
# Position 0 is a bit weird. It's the last position, not
# the first!
self.ptr = 0
elif self.ptr > 0 and dir < 0 and self.ptr + dir < 1:
self.ptr = 1
else:
self.ptr = self.ptr + dir
if self.ptr < 0:
self.ptr = len(self.items) - 1
try:
return self.edited[self.ptr]
except KeyError:
return self.items[self.ptr]
def search_backward(self, text):
if len(self.items) == 1:
return None, None
if self.ptr == 1:
return None, None
if self.ptr == 0:
search_iter = len(self.items) - 1
else:
search_iter = self.ptr - 1
while text not in self.items[search_iter]:
search_iter -= 1
if search_iter == 1:
return None, self.items[1]
return search_iter, self.items[search_iter]
def search_forward(self, text):
if len(self.items) == 1:
return None, None
if self.ptr == 0:
return None, None
if self.ptr == len(self.items) - 1:
search_iter = 0
else:
search_iter = self.ptr + 1
while text not in self.items[search_iter]:
if search_iter == 0:
return None, None
search_iter += 1
if search_iter > len(self.items):
search_iter = 0
return search_iter, self.items[search_iter]
def __init__(self, quit_func=None, initial_history=None):
object.__init__(self)
self.quit_func = quit_func
self.set_wrap_mode(Gtk.WrapMode.CHAR)
self.modify_font(Pango.FontDescription("Monospace"))
self.buffer = self.get_buffer()
self.buffer.connect("insert-text", self.on_buf_insert)
self.buffer.connect("delete-range", self.on_buf_delete)
self.buffer.connect("mark-set", self.on_buf_mark_set)
self.do_insert = False
self.do_delete = False
#Use the theme's color scheme for the text color
font_color = self.get_style_context().get_property('color', Gtk.StateFlags.NORMAL)
r = int (font_color.red * 255)
g = int (font_color.green * 255)
b = int (font_color.blue * 255)
hex_font_color = "#" + '{r:02x}{g:02x}{b:02x}'.format (r = r, g = g, b = b)
#Choose an error text color based on background luminance
bg_color = self.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL)
lum = (bg_color.red * 0.22248840) + (bg_color.green * 0.71690369) + (bg_color.blue * 0.06060791)
background_color = "#B00000"
if (lum < 0.4):
background_color = "#FF6161"
self.stdout_tag = self.buffer.create_tag("stdout", foreground=hex_font_color)
self.stderr_tag = self.buffer.create_tag("stderr", foreground=background_color)
self._stdout = _ReadLine.Output(self, "stdout")
self._stderr = _ReadLine.Output(self, "stderr")
self.cursor = self.buffer.create_mark("cursor",
self.buffer.get_start_iter(),
False)
insert = self.buffer.get_insert()
self.cursor.set_visible(True)
insert.set_visible(False)
self.ps = ''
self.in_raw_input = False
self.in_modal_raw_input = False
self.run_on_raw_input = None
self.tab_pressed = 0
self.history = _ReadLine.History(initial_history)
self.nonword_re = re.compile(r"[^\w\._]")
self._searching_backward = False
self._searching_forward = False
def freeze_undo(self):
try: self.begin_not_undoable_action()
except: pass
def thaw_undo(self):
try: self.end_not_undoable_action()
except: pass
def raw_input(self, ps=None):
'''Show prompt 'ps' and enter input mode until the current input
is committed.'''
if ps:
self.ps = ps
else:
self.ps = ''
iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
if ps:
self.freeze_undo()
self.buffer.insert(iter, self.ps)
self.thaw_undo()
self.__move_cursor_to(iter)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
self.in_raw_input = True
if self.run_on_raw_input:
run_now = self.run_on_raw_input
self.run_on_raw_input = None
self.buffer.insert_at_cursor(run_now + '\n')
def modal_raw_input(self, text):
'''Starts raw input in modal mode. The event loop is spinned until
the input is committed. Returns the text entered after the prompt.'''
orig_ps = self.ps
self.raw_input(text)
self.in_modal_raw_input = True
while self.in_modal_raw_input:
Gtk.main_iteration()
self.ps = orig_ps
self.in_modal_raw_input = False
self.in_raw_input = False
return self.modal_raw_input_result
def modal_input(self, text):
return eval(self.modal_raw_input(text))
# Each time the insert mark is modified, move the cursor to it.
def on_buf_mark_set(self, buffer, iter, mark):
if mark is not buffer.get_insert():
return
start = self.__get_start()
end = self.__get_end()
if iter.compare(self.__get_start()) >= 0 and \
iter.compare(self.__get_end()) <= 0:
buffer.move_mark_by_name("cursor", iter)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
def __insert(self, iter, text):
self.do_insert = True
self.buffer.insert(iter, text)
self.do_insert = False
# Make sure that text insertions while in text input mode are properly
# committed to the history.
def on_buf_insert(self, buf, iter, text, len):
# Bail out if not in input mode.
if not self.in_raw_input or self.do_insert or not len:
return
buf.stop_emission("insert-text")
lines = text.splitlines()
need_eol = False
for l in lines:
if need_eol:
self._commit()
iter = self.__get_cursor()
else:
cursor = self.__get_cursor()
if iter.compare(self.__get_start()) < 0:
iter = cursor
elif iter.compare(self.__get_end()) > 0:
iter = cursor
else:
self.__move_cursor_to(iter)
need_eol = True
self.__insert(iter, l)
self.__move_cursor(0)
def __delete(self, start, end):
self.do_delete = True
self.buffer.delete(start, end)
self.do_delete = False
def on_buf_delete(self, buf, start, end):
if not self.in_raw_input or self.do_delete:
return
buf.stop_emission("delete-range")
start.order(end)
line_start = self.__get_start()
line_end = self.__get_end()
if start.compare(line_end) > 0:
return
if end.compare(line_start) < 0:
return
self.__move_cursor(0)
if start.compare(line_start) < 0:
start = line_start
if end.compare(line_end) > 0:
end = line_end
self.__delete(start, end)
# We overload the key press event handler to handle "special keys"
# when in input mode to make history browsing, completions, etc. work.
def do_key_press_event(self, event):
if not self.in_raw_input:
return Gtk.TextView.do_key_press_event(self, event)
tab_pressed = self.tab_pressed
self.tab_pressed = 0
handled = True
state = event.state & (Gdk.ModifierType.CONTROL_MASK |
Gdk.ModifierType.MOD1_MASK)
keyval = event.keyval
if not state:
if keyval == Gdk.KEY_Escape:
self.__history_search_stop()
elif keyval == Gdk.KEY_Return:
self.__history_search_stop()
self._commit()
elif keyval == Gdk.KEY_Up or keyval == Gdk.KEY_Page_Up:
self.__history_search_stop()
self.__history(-1)
elif keyval == Gdk.KEY_Down or keyval == Gdk.KEY_Page_Down:
self.__history_search_stop()
self.__history(1)
elif keyval == Gdk.KEY_Left:
self.__history_search_stop()
self.__move_cursor(-1)
elif keyval == Gdk.KEY_Right:
self.__history_search_stop()
self.__move_cursor(1)
elif keyval == Gdk.KEY_Home:
self.__history_search_stop()
self.__move_cursor(-10000)
elif keyval == Gdk.KEY_End:
self.__history_search_stop()
self.__move_cursor(10000)
elif keyval == Gdk.KEY_Tab:
self.__history_search_stop()
cursor = self.__get_cursor()
if cursor.starts_line():
handled = False
else:
cursor.backward_char()
if cursor.get_char().isspace():
handled = False
else:
self.tab_pressed = tab_pressed + 1
self.__complete()
elif self._searching_backward:
if event.string != '' or keyval == Gdk.KEY_BackSpace:
self.__history_search_backward(keyval, event.string)
elif self._searching_forward:
if event.string != '' or keyval == Gdk.KEY_BackSpace:
self.__history_search_forward(keyval, event.string)
else:
handled = False
elif state == Gdk.ModifierType.CONTROL_MASK:
if keyval == Gdk.KEY_u:
self.__history_search_stop()
start = self.__get_start()
end = self.__get_cursor()
self.__delete(start, end)
elif keyval == Gdk.KEY_d:
self.__history_search_stop()
if self.quit_func:
self.quit_func()
elif keyval == Gdk.KEY_r:
self.__history_search_backward(keyval, '')
elif keyval == Gdk.KEY_s:
self.__history_search_forward(keyval, '')
elif keyval == Gdk.KEY_c and (self._searching_backward or self._searching_forward):
self.__history_search_cancel()
else:
self.__history_search_stop()
handled = False
else:
handled = False
# Handle ordinary keys
if not handled:
return Gtk.TextView.do_key_press_event(self, event)
else:
return True
def __history(self, dir):
text = self._get_line()
new_text = self.history.get(dir, text)
if not new_text is None:
self.__replace_line(new_text)
self.__move_cursor(0)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
def __history_search_stop(self):
if self._searching_backward or self._searching_forward:
self._searching_backward = False
self._searching_forward = False
self.ps = self.ps1
self.__replace_line(self.history.items[self.history.ptr])
self.__move_cursor(0)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
def __history_search_cancel(self):
if self._searching_backward or self._searching_forward:
self._searching_backward = False
self._searching_forward = False
self.history.ptr = self._search_initial_ptr
self.ps = self.ps1
self.__replace_line(self.history.items[self.history.ptr])
self.__move_cursor(0)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
def __history_search_backward(self, keyval, text):
search_anyway = False
if not self._searching_backward:
self._searching_backward = True
if not self._searching_forward:
self._search_initial_ptr = self.history.ptr
self._search_text = ''
else:
search_anyway = True
self.ps = self.sb
else:
if keyval == Gdk.KEY_BackSpace:
search_anyway = True
self._search_text = self._search_text[:-1]
elif text == '':
search_anyway = True
else:
self._search_text += text
self._searching_forward = False
if self._search_text != '' and (self._search_text not in self._get_line() or search_anyway):
ptr, new_text = self.history.search_backward(self._search_text)
if ptr is not None:
self.history.ptr = ptr
self.__replace_line(self.history.items[self.history.ptr])
self.__move_cursor(0)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
def __history_search_forward(self, keyval, text):
search_anyway = False
if not self._searching_forward:
self._searching_forward = True
if not self._searching_backward:
self._search_initial_ptr = self.history.ptr
self._search_text = ''
else:
search_anyway = True
self.ps = self.sf
else:
if keyval == Gdk.KEY_BackSpace:
search_anyway = True
self._search_text = self._search_text[:-1]
elif text == '':
search_anyway = True
else:
self._search_text += text
self._searching_backward = False
if self._search_text != '' and (self._search_text not in self._get_line() or search_anyway):
ptr, new_text = self.history.search_forward(self._search_text)
if ptr is not None:
self.history.ptr = ptr
self.__replace_line(self.history.items[self.history.ptr])
self.__move_cursor(0)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
def __get_cursor(self):
'''Returns an iterator at the current cursor position.'''
return self.buffer.get_iter_at_mark(self.cursor)
def __get_start(self, before_ps=False):
'''Returns an iterator at the start of the input on the current
cursor line.'''
iter = self.__get_cursor()
iter.set_line(iter.get_line())
if not before_ps:
iter.forward_chars(len(self.ps))
return iter
def __get_end(self):
'''Returns an iterator at the end of the cursor line.'''
iter = self.__get_cursor()
if not iter.ends_line():
iter.forward_to_line_end()
return iter
def __get_text(self, start, end):
'''Get text between 'start' and 'end' markers.'''
return self.buffer.get_text(start, end, False)
def __move_cursor_to(self, iter):
self.buffer.place_cursor(iter)
self.buffer.move_mark_by_name("cursor", iter)
def __move_cursor(self, howmany):
iter = self.__get_cursor()
end = self.__get_cursor()
if not end.ends_line():
end.forward_to_line_end()
line_len = end.get_line_offset()
move_to = iter.get_line_offset() + howmany
move_to = min(max(move_to, len(self.ps)), line_len)
iter.set_line_offset(move_to)
self.__move_cursor_to(iter)
def __delete_at_cursor(self, howmany):
iter = self.__get_cursor()
end = self.__get_cursor()
if not end.ends_line():
end.forward_to_line_end()
line_len = end.get_line_offset()
erase_to = iter.get_line_offset() + howmany
if erase_to > line_len:
erase_to = line_len
elif erase_to < len(self.ps):
erase_to = len(self.ps)
end.set_line_offset(erase_to)
self.__delete(iter, end)
def __get_width(self):
'''Estimate the number of characters that will fit in the area
currently allocated to this widget.'''
if not self.get_realized():
return 80
context = self.get_pango_context()
metrics = context.get_metrics(context.get_font_description(),
context.get_language())
pix_width = metrics.get_approximate_char_width()
allocation = Gtk.Widget.get_allocation(self)
return allocation.width * Pango.SCALE / pix_width
def __print_completions(self, completions):
line_start = self.__get_text(self.__get_start(), self.__get_cursor())
line_end = self.__get_text(self.__get_cursor(), self.__get_end())
iter = self.buffer.get_end_iter()
self.__move_cursor_to(iter)
self.__insert(iter, "\n")
width = max(self.__get_width(), 4)
max_width = max(len(s) for s in completions)
n_columns = max(int(width / (max_width + 1)), 1)
col_width = int(width / n_columns)
total = len(completions)
col_length = total / n_columns
if total % n_columns:
col_length = col_length + 1
col_length = max(col_length, 1)
if col_length == 1:
n_columns = total
col_width = width / total
for i in range(int(col_length)):
for j in range(n_columns):
ind = i + j*col_length
if ind < total:
if j == n_columns - 1:
n_spaces = 0
else:
n_spaces = int(col_width - len(completions[int(ind)]))
self.__insert(iter, completions[int(ind)] + " " * n_spaces)
self.__insert(iter, "\n")
self.__insert(iter, "%s%s%s" % (self.ps, line_start, line_end))
iter.set_line_offset(len(self.ps) + len(line_start))
self.__move_cursor_to(iter)
self.scroll_to_mark(self.cursor, 0.2, False, 0.0, 0.0)
def __complete(self):
text = self.__get_text(self.__get_start(), self.__get_cursor())
start = ''
word = text
nonwords = self.nonword_re.findall(text)
if nonwords:
last = text.rfind(nonwords[-1]) + len(nonwords[-1])
start = text[:last]
word = text[last:]
completions = self.complete(word)
if completions:
prefix = _commonprefix(completions)
if prefix != word:
start_iter = self.__get_start()
start_iter.forward_chars(len(start))
end_iter = start_iter.copy()
end_iter.forward_chars(len(word))
self.__delete(start_iter, end_iter)
self.__insert(end_iter, prefix)
elif self.tab_pressed > 1:
self.freeze_undo()
self.__print_completions(completions)
self.thaw_undo()
self.tab_pressed = 0
def complete(self, text):
return None
def _get_line(self):
'''Return the current input behind the prompt.'''
start = self.__get_start()
end = self.__get_end()
return self.buffer.get_text(start, end, False)
def __replace_line(self, new_text):
'''Replace the current input with 'new_text' '''
start = self.__get_start(True)
end = self.__get_end()
self.__delete(start, end)
self.__insert(end, self.ps)
if self._searching_backward or self._searching_forward:
self.__insert(end, '({}) '.format(self._search_text))
self.__insert(end, new_text)
def _commit(self):
'''Commit the input entered on the current line.'''
# Find iterator and end of cursor line.
end = self.__get_cursor()
if not end.ends_line():
end.forward_to_line_end()
# Get text at current line.
text = self._get_line()
# Move cursor to the end of the line, insert new line.
self.__move_cursor_to(end)
self.freeze_undo()
self.__insert(end, "\n")
self.history.commit(text)
if self.in_modal_raw_input:
self.in_modal_raw_input = False
self.modal_raw_input_result = text
else:
self.in_raw_input = False
self.do_raw_input(text)
self.thaw_undo()
def do_raw_input(self, text):
pass
class _Console(_ReadLine, code.InteractiveInterpreter):
def __init__(self, locals=None, banner=None,
completer=None, use_rlcompleter=True,
start_script=None, quit_func=None,
initial_history=None):
_ReadLine.__init__(self, quit_func, initial_history)
code.InteractiveInterpreter.__init__(self, locals)
self.locals["__console__"] = self
# The builtin raw_input function reads from stdin, we don't want
# this. Therefore, replace this function with our own modal raw
# input function.
exec ("import builtins", self.locals)
#self.locals['builtins'].__dict__['raw_input'] = lambda text='': self.modal_raw_input(text)
self.locals['builtins'].__dict__['input'] = lambda text='': self.modal_input(text)
self.start_script = start_script
self.completer = completer
self.banner = banner
if not self.completer and use_rlcompleter:
try:
import rlcompleter
self.completer = rlcompleter.Completer()
except ImportError:
pass
self.ps1 = ">>> "
self.ps2 = "... "
self.sb = "^^^ "
self.sf = "vvv "
self.__start()
self.run_on_raw_input = start_script
self.raw_input(self.ps1)
def __start(self):
self.cmd_buffer = ""
self.freeze_undo()
self.thaw_undo()
self.do_delete = True
self.buffer.set_text("")
self.do_delete = False
if self.banner:
iter = self.buffer.get_start_iter()
self.buffer.insert_with_tags_by_name(iter, self.banner, "stdout")
if not iter.starts_line():
self.buffer.insert(iter, "\n")
def clear(self, start_script=None):
if start_script is None:
start_script = self.start_script
else:
self.start_script = start_script
self.__start()
self.run_on_raw_input = start_script
self.raw_input(self.ps1)
def do_raw_input(self, text):
if self.cmd_buffer:
cmd = self.cmd_buffer + "\n" + text
else:
cmd = text
saved_stdout, saved_stderr = sys.stdout, sys.stderr
sys.stdout, sys.stderr = self._stdout, self._stderr
if self.runsource(cmd):
self.cmd_buffer = cmd
ps = self.ps2
else:
self.cmd_buffer = ''
if self._searching_backward:
ps = self.sb
elif self._searching_forward:
ps = self.sb
else:
ps = self.ps1
sys.stdout, sys.stderr = saved_stdout, saved_stderr
self.raw_input(ps)
def do_command(self, code):
try:
eval(code, self.locals)
except SystemExit:
if self.quit_func:
self.quit_func()
else:
raise
except:
self.showtraceback()
def runcode(self, code):
#if gtk.pygtk_version[1] < 8:
self.do_command(code)
#else:
#self.emit("command", code)
def complete_attr(self, start, end):
try:
obj = eval(start, self.locals)
strings = dir(obj)
if end:
completions = {}
for s in strings:
if s.startswith(end):
completions[s] = None
completions = completions.keys()
else:
completions = strings
completions.sort()
return [start + "." + s for s in completions]
except:
return None
def complete(self, text):
if self.completer:
completions = []
i = 0
try:
while 1:
s = self.completer.complete(text, i)
if s:
completions.append(s)
i += 1
else:
completions.sort()
return completions
except NameError:
return None
dot = text.rfind(".")
if dot >= 0:
return self.complete_attr(text[:dot], text[dot+1:])
completions = {}
strings = keyword.kwlist
if self.locals:
strings.extend(self.locals.keys())
try: strings.extend(eval("globals()", self.locals).keys())
except: pass
try:
exec("import __builtin__", self.locals)
strings.extend(eval("dir(__builtin__)", self.locals))
except:
pass
for s in strings:
if s.startswith(text):
completions[s] = None
completions = completions.keys()
completions.sort()
return completions
def ReadLineType(t=Gtk.TextView):
class readline(t, _ReadLine):
def __init__(self, *args, **kwargs):
t.__init__(self)
_ReadLine.__init__(self, *args, **kwargs)
def do_key_press_event(self, event):
return _ReadLine.do_key_press_event(self, event)
GObject.type_register(readline)
return readline
def ConsoleType(t=Gtk.TextView):
class console_type(t, _Console):
__gsignals__ = {
'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (object,)),
}
def __init__(self, *args, **kwargs):
#if gtk.pygtk_version[1] < 8:
GObject.GObject.__init__(self)
#else:
#t.__init__(self)
_Console.__init__(self, *args, **kwargs)
def do_command(self, code):
return _Console.do_command(self, code)
def do_key_press_event(self, event):
return _Console.do_key_press_event(self, event)
def get_default_size(self):
context = self.get_pango_context()
metrics = context.get_metrics(context.get_font_description(),
context.get_language())
width = metrics.get_approximate_char_width()
height = metrics.get_ascent() + metrics.get_descent()
# Default to a 80x40 console
width = pango_pixels(int(width * 80 * 1.05))
height = pango_pixels(height * 40)
return width, height
#if gtk.pygtk_version[1] < 8:
GObject.type_register(console_type)
return console_type
ReadLine = ReadLineType()
Console = ConsoleType()
def _make_window():
window = Gtk.Window()
window.set_title("pyconsole.py")
swin = Gtk.ScrolledWindow()
swin.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
window.add(swin)
console = Console(banner="Hello there!",
use_rlcompleter=False,
start_script="gi.require_version('Gimp', '3.0')\nfrom gi.repository import Gimp\n")
swin.add(console)
width, height = console.get_default_size()
sb_width, sb_height = swin.get_vscrollbar().size_request()
window.set_default_size(width + sb_width, height)
window.show_all()
if not Gtk.main_level():
window.connect("destroy", Gtk.main_quit)
Gtk.main()
return console
if __name__ == '__main__':
if len(sys.argv) < 2 or sys.argv[1] != '-gimp':
_make_window()

View file

@ -0,0 +1,335 @@
#!/usr/bin/env python3
# Gimp-Python - allows the writing of Gimp plugins in Python.
# Copyright (C) 1997 James Henstridge <james@daa.com.au>
#
# 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 3 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, see <https://www.gnu.org/licenses/>.
import gi
gi.require_version('Gimp', '3.0')
gi.require_version('GimpUi', '3.0')
from gi.repository import Gimp
from gi.repository import GimpUi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import Gio
from gi.repository import GLib
import sys
import warnings
import pyconsole
#import gimpshelf, gimpui, pyconsole
import gettext
textdomain = "gimp30-python"
gettext.textdomain(textdomain)
_ = gettext.gettext
PROC_NAME = 'python-fu-console'
RESPONSE_BROWSE, RESPONSE_CLEAR, RESPONSE_SAVE = range(3)
def run(procedure, config, data):
GimpUi.init ("python-console.py")
namespace = {'__builtins__': __builtins__,
'__name__': '__main__', '__doc__': None,
'Babl': gi.repository.Babl,
'cairo': gi.repository.cairo,
'Gdk': gi.repository.Gdk,
'Gegl': gi.repository.Gegl,
'Gimp': gi.repository.Gimp,
'GimpUi': gi.repository.GimpUi,
'Gio': gi.repository.Gio,
'Gtk': gi.repository.Gtk,
'GdkPixbuf': gi.repository.GdkPixbuf,
'GLib': gi.repository.GLib,
'GObject': gi.repository.GObject,
'Pango': gi.repository.Pango }
class GimpConsole(pyconsole.Console):
def __init__(self, quit_func=None, initial_history=None):
banner = ('GIMP %s Python Console\nPython %s\n' %
(Gimp.version(), sys.version))
pyconsole.Console.__init__(self,
locals=namespace, banner=banner,
quit_func=quit_func,
initial_history=initial_history)
def _commit(self):
pyconsole.Console._commit(self)
Gimp.displays_flush()
class ConsoleDialog(GimpUi.Dialog):
def __init__(self):
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
GimpUi.Dialog.__init__(self, use_header_bar=use_header_bar)
self.set_property("help-id", PROC_NAME)
Gtk.Window.set_title(self, _("Python Console"))
Gtk.Window.set_role(self, PROC_NAME)
Gtk.Dialog.add_button(self, _("_Save"), Gtk.ResponseType.OK)
Gtk.Dialog.add_button(self, _("Cl_ear"), RESPONSE_CLEAR)
Gtk.Dialog.add_button(self, _("_Browse..."), RESPONSE_BROWSE)
Gtk.Dialog.add_button(self, _("_Close"), Gtk.ResponseType.CLOSE)
Gtk.Widget.set_name (self, PROC_NAME)
GimpUi.Dialog.set_alternative_button_order_from_array(self,
[ Gtk.ResponseType.CLOSE,
RESPONSE_BROWSE,
RESPONSE_CLEAR,
Gtk.ResponseType.OK ])
history = config.get_property('history')
self.cons = GimpConsole(quit_func=lambda: Gtk.main_quit(), initial_history=history)
self.style_set (None, None)
self.connect('response', self.response)
self.connect('style-set', self.style_set)
self.browse_dlg = None
self.save_dlg = None
vbox = Gtk.VBox(homogeneous=False, spacing=12)
vbox.set_border_width(12)
contents_area = Gtk.Dialog.get_content_area(self)
contents_area.pack_start(vbox, True, True, 0)
scrl_win = Gtk.ScrolledWindow()
scrl_win.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
vbox.pack_start(scrl_win, True, True, 0)
scrl_win.add(self.cons)
width, height = self.cons.get_default_size()
minreq, requisition = Gtk.Widget.get_preferred_size(scrl_win.get_vscrollbar())
sb_width = requisition.width
sb_height = requisition.height
# Account for scrollbar width and border width to ensure
# the text view gets a width of 80 characters. We don't care
# so much whether the height will be exactly 40 characters.
Gtk.Window.set_default_size(self, width + sb_width + 2 * 12, height)
def style_set(self, old_style, user_data):
pass
#style = Gtk.Widget.get_style (self)
#self.cons.stdout_tag.set_property ("foreground", style.text[Gtk.StateType.NORMAL])
#self.cons.stderr_tag.set_property ("foreground", style.text[Gtk.StateType.INSENSITIVE])
def response(self, dialog, response_id):
if response_id == RESPONSE_BROWSE:
self.browse()
elif response_id == RESPONSE_CLEAR:
self.cons.banner = None
self.cons.clear()
elif response_id == Gtk.ResponseType.OK:
self.save_dialog()
else:
# Store up to 100 commands.
config.set_property('history', self.cons.history.items[-100:])
Gtk.main_quit()
self.cons.grab_focus()
def command_for_procedure(self, proc_name):
'''
Assemble string of Python code that when eval'd
will call proc_name with contrived arguments.
The purpose is to generate a template for a call
to the PDB procedure the user has selected.
The call MIGHT work as is.
Otherwise, the names of the arguments might be enough
that the user can figure out how to edit the template.
The code will run in the environment of the console/browser,
which is not the GIMP v2 GimpFu environment
but the GIMP v3 PyGObject introspected environment.
If ever GimpFu module is resurrected, and Python console imports it,
then revert this code to its v2 form.
'''
proc = Gimp.get_pdb().lookup_procedure(proc_name)
if proc is None:
return None
return_values = proc.get_return_values()
param_specs = proc.get_arguments()
cmd = f"procedure = Gimp.get_pdb().lookup_procedure('{proc_name}'); "
cmd += f"config = procedure.create_config(); "
for arg in param_specs:
if arg.name == 'run-mode':
# Special handling for run mode.
cmd += "config.set_property('" + arg.name + "', Gimp.RunMode.INTERACTIVE); "
elif type(arg) == Gimp.ParamCoreObjectArray:
# Special handling for GimpCoreObjectArray parameters
cmd += "config.set_core_object_array('" + arg.name + "', " + arg.name.replace('-', '_') + "); "
else:
cmd += "config.set_property('" + arg.name + "', " + arg.name.replace('-', '_') + "); "
cmd += f"result = procedure.run(config); "
cmd += f"success = result.index(0)"
if len(return_values) > 0:
i = 1
for retval in return_values:
cmd += '; {} = result.index({})'.format(retval.name.replace('-', '_'), i)
i += 1
return cmd
def browse_response(self, dlg, response_id):
if response_id != Gtk.ResponseType.APPLY:
Gtk.Widget.hide(dlg)
return
proc_name = dlg.get_selected()
if not proc_name:
# Apply button was enabled without a selection?
return
cmd = self.command_for_procedure(proc_name)
if cmd is None:
# Should not happen. We browsed a name not in the PDB?
return
buffer = self.cons.buffer
lines = buffer.get_line_count()
iter = buffer.get_iter_at_line_offset(lines - 1, 4)
buffer.delete(iter, buffer.get_end_iter())
buffer.place_cursor(buffer.get_end_iter())
buffer.insert_at_cursor(cmd)
# not insert a newline, user can edit and then "enter" the command
def browse(self):
if not self.browse_dlg:
use_header_bar = Gtk.Settings.get_default().get_property("gtk-dialogs-use-header")
dlg = GimpUi.ProcBrowserDialog(use_header_bar=use_header_bar)
Gtk.Window.set_title(dlg, _("Python Procedure Browser"))
Gtk.Window.set_role(dlg, PROC_NAME)
Gtk.Dialog.add_button(dlg, _("_Apply"), Gtk.ResponseType.APPLY)
Gtk.Dialog.add_button(dlg, _("_Close"), Gtk.ResponseType.CLOSE)
Gtk.Dialog.set_default_response(self, Gtk.ResponseType.OK)
GimpUi.Dialog.set_alternative_button_order_from_array(dlg,
[ Gtk.ResponseType.CLOSE,
Gtk.ResponseType.APPLY ])
dlg.connect('response', self.browse_response)
dlg.connect('row-activated',
lambda dlg: dlg.response(Gtk.ResponseType.APPLY))
self.browse_dlg = dlg
Gtk.Window.present(self.browse_dlg)
def save_response(self, dlg, response_id):
if response_id == Gtk.ResponseType.DELETE_EVENT:
self.save_dlg = None
return
elif response_id == Gtk.ResponseType.OK:
filename = dlg.get_filename()
try:
logfile = open(filename, 'w')
except IOError as e:
Gimp.message(_("Could not open '%s' for writing: %s") %
(filename, e.strerror))
return
buffer = self.cons.buffer
start = buffer.get_start_iter()
end = buffer.get_end_iter()
log = buffer.get_text(start, end, False)
try:
logfile.write(log)
logfile.close()
except IOError as e:
Gimp.message(_("Could not write to '%s': %s") %
(filename, e.strerror))
return
Gtk.Widget.hide(dlg)
def save_dialog(self):
if not self.save_dlg:
dlg = Gtk.FileChooserDialog(action=Gtk.FileChooserAction.SAVE)
Gtk.Window.set_title(dlg, _("Save Python-Fu Console Output"))
Gtk.Window.set_transient_for(dlg, self)
Gtk.Dialog.add_button(dlg, _("_Cancel"), Gtk.ResponseType.CANCEL)
Gtk.Dialog.add_button(dlg, _("_Save"), Gtk.ResponseType.OK)
Gtk.Dialog.set_default_response(self, Gtk.ResponseType.OK)
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
Gtk.FileChooserDialog.set_alternative_button_order_from_array(dlg,
[ Gtk.ResponseType.OK,
Gtk.ResponseType.CANCEL ])
dlg.connect('response', self.save_response)
self.save_dlg = dlg
self.save_dlg.present()
def run(self):
Gtk.Widget.show_all(self)
Gtk.main()
ConsoleDialog().run()
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())
class PythonConsole (Gimp.PlugIn):
## GimpPlugIn virtual methods ##
def do_set_i18n(self, name):
gettext.bindtextdomain(textdomain, Gimp.locale_directory())
return True, 'gimp30-python', None
def do_query_procedures(self):
return [ PROC_NAME ]
def do_create_procedure(self, name):
if name == PROC_NAME:
procedure = Gimp.Procedure.new(self, name,
Gimp.PDBProcType.PLUGIN,
run, None)
procedure.set_menu_label(_("Python _Console"))
procedure.set_documentation(_("Interactive GIMP Python interpreter"),
_("Type in commands and see results"),
"")
procedure.set_attribution("James Henstridge",
"James Henstridge",
"1997-1999")
procedure.add_enum_argument ("run-mode", _("Run mode"),
_("The run mode"), Gimp.RunMode,
Gimp.RunMode.INTERACTIVE,
GObject.ParamFlags.READWRITE)
procedure.add_string_array_aux_argument ("history",
"Command history", "Command history",
GObject.ParamFlags.READWRITE)
procedure.add_menu_path ("<Image>/Filters/Development/Python-Fu")
return procedure
return None
Gimp.main(PythonConsole.__gtype__, sys.argv)