#!/usr/bin/env python # [SNIPPET_NAME: gtk3 interactive textview] # [SNIPPET_CATEGORIES: gtk] # [SNIPPET_TAGS:interactive, gtk3] # [SNIPPET_DESCRIPTION: using gtk3 textview run python code on running program] # [SNIPPET_AUTHOR: Oliver Marks ] # [SNIPPET_LICENSE: GPL] import sys from StringIO import StringIO from gi.repository import Gtk, Gdk import code import math class interactiveGtk: def __init__(self): window = Gtk.Window() window.set_default_size(380, 300) window.connect("destroy", lambda w: Gtk.main_quit()) box = Gtk.VBox() self.drawingarea = Gtk.DrawingArea() #python console using a textview console = interactive_console(Gtk.TextView()) self.drawingarea.connect("draw", self.area_expose_cb) box.add(self.drawingarea) box.add(console.textarea) window.add(box) window.show_all() self.drawarea = False def show_drawing(self, state): """self.show_drawing(True) to enable showing the lines""" self.drawarea = state def test(self): """run app.test() when program is running to print this message""" print ('hello world') def area_expose_cb(self, widget, context): """expose event lets draw, lines will only display if we run self.show_lines first. demonstrating running state change of our program""" self.style = self.drawingarea.get_style() if self.drawarea is True: self.drawing(context, 210, 10) def drawing(self, cr, x, y): """ draw a circle in the drawing area """ cr.set_line_width(10) cr.set_source_rgb(0.5, 0.8, 0.0) cr.translate(20 / 2, 20 / 2) cr.arc(50, 50, 50, 0, 2 * math.pi) cr.stroke_preserve() cr.set_source_rgb(0.3, 0.4, 0.4) cr.fill() class interactive_console: editor_chars = '>>>' editor_chars_other = '...' editor_history = [] editor_history_position = 0 def __init__(self, textview): #the python editor window self.textarea = textview self.textarea.connect('key-press-event', self.key_pressed) self.console_buffer = self.textarea.get_buffer() #setup some characters which can not be changed self.console_buffer.set_text(self.editor_chars + 'app.show_drawing(True)') self.console_buffer.create_tag("uneditable", editable=False, editable_set=True) self.console_buffer.create_mark("input_position", self.console_buffer.get_end_iter(), True) self.console_buffer.create_mark("end_position", self.console_buffer.get_end_iter(), False) self.console_buffer.apply_tag_by_name("uneditable", self.console_buffer.get_start_iter(), self.console_buffer.get_iter_at_offset(len(self.editor_chars))) #interactive mode interpreter, #pass locals() or globals() so we can access our programs functions and variables #using global here so we have access to the app object in the global scope self.interpreter = code.InteractiveInterpreter(globals()) def key_pressed(self, widget, event): """ grab key presses, run code from textview on return navigate history if arrow keys are pressed """ if event.keyval == Gdk.keyval_from_name('Return'): self.execute_line() return True if event.keyval == Gdk.keyval_from_name('Up'): self.console_history(-1) return True if event.keyval == Gdk.keyval_from_name('Down'): self.console_history(1) return True return False def execute_line(self): """ carriage return was captured so lets process the textview contents for code to run """ text = self.console_buffer.get_text(self.console_buffer.get_start_iter(), self.console_buffer.get_end_iter(), False) source = '' block = False indent = 0 #work out code to run if its not a block or a blank line then run what we have, #if its a block of code like a for loop or if condition dont run it yet unless the block has finished. last_line = '' for line in text.split("\n"): line = line[3:].strip('') if line.strip() == '': block = False indent = 0 else: if line.endswith(':'): if block is True: source += line + "\n\t" else: source = line + "\n\t" block = True indent += 1 else: if line.startswith("\t"): source += line + "\n" else: block = False source = line + "\n" indent = 0 last_line = line if last_line.strip() != '': self.append_history(last_line) chars = self.editor_chars # run the code grabbed from the text buffer and execute it if its not a block results = '' if block is True: chars = self.editor_chars_other else: chars = self.editor_chars results = self.execute_code(source) #build text for the editor, and workout which part of the text should be locked text_output = text + '\n' + results + chars text_output_length = len(text_output) if block is True: text_output += "\t" * indent self.update_editor(text_output, text_output_length) def execute_code(self, source): """ run any code sent here and capture output and return the results """ # capture output from stdio so we can display the errors in our editor result = StringIO() sys.stdout = result sys.stderr = result #run code output will be put into result because we are capturing stdout self.interpreter.runsource(source, "<>") # restore stdout so future output is capture to the terminal again sys.stdout = sys.__stdout__ sys.stdout = sys.__stderr__ return result.getvalue() def update_editor(self, text, uneditable_end=0): """ pass in text to put in the editor and portion to be locked from editing """ self.console_buffer.set_text(text) self.console_buffer.create_mark("input_position", self.console_buffer.get_end_iter(), True) self.console_buffer.create_mark("end_position", self.console_buffer.get_end_iter(), False) self.console_buffer.apply_tag_by_name("uneditable", self.console_buffer.get_start_iter(), self.console_buffer.get_iter_at_offset(uneditable_end)) def append_history(self, line): """ Store command in history drop command if limit has been reached """ if len(self.editor_history) > 10: self.editor_history.pop() self.editor_history.append(line) self.editor_history_position = len(self.editor_history) def console_history(self, direction=-1): """ drop any text on last line and insert text from history based on position """ # get current text excluding last line as we will update from our history text = self.console_buffer.get_text( self.console_buffer.get_start_iter(), self.console_buffer.get_iter_at_line_index( self.console_buffer.get_line_count(), 0), False) #work out position in history and what should be displayed linenumber = self.editor_history_position + direction if linenumber >= 0 and linenumber < len(self.editor_history): self.editor_history_position += direction self.console_buffer.set_text(text + self.editor_chars + self.editor_history[self.editor_history_position]) app = interactiveGtk() Gtk.main()