9

I want to create a popup window using wxPython that acts like a bash shell. I don't want a terminal emulator, I don't need job control, I just want a REPL (Read, Eval, Print Loop) based on a bash process.

Is there an easy way to do that with wxPython? I know the basic concept from my days as a tcl/tk programmer but my wxPython fu is weak and I don't want to have to reinvent the wheel if I don't have to. I've read a little about py.shell. Shell but that looks like it creates a python shell and I want one to run bash commands instead.

0

5 Answers 5

7
+250

ok here is another try, which reads all output and errors too, in a separate thread and communicates via Queue. I know it is not perfect(e.g. command with delayed output will not work and there output will get into next commnd for example tryr sleep 1; date) and replicating whole bash not trivial but for few commands i tested it seems to work fine

Regarding API of wx.py.shell I just implemented those method which Shell class was calling for Interpreter, if you go thru source code of Shell you will understand. basically

  • push is where user entered command is sent to interpreter
  • getAutoCompleteKeys returns keys which user can user for auto completing commands e.g. tab key
  • getAutoCompleteList return list of command matching given text

  • getCallTip "Display argument spec and docstring in a popup window. so for bash we may show man page :)

here is the source code

import threading
import Queue
import time

import wx
import wx.py
from subprocess import Popen, PIPE

class BashProcessThread(threading.Thread):
    def __init__(self, readlineFunc):
        threading.Thread.__init__(self)

        self.readlineFunc = readlineFunc
        self.outputQueue = Queue.Queue()
        self.setDaemon(True)

    def run(self):
        while True:
            line = self.readlineFunc()
            self.outputQueue.put(line)

    def getOutput(self):
        """ called from other thread """
        lines = []
        while True:
            try:
                line = self.outputQueue.get_nowait()
                lines.append(line)
            except Queue.Empty:
                break
        return ''.join(lines)

class MyInterpretor(object):
    def __init__(self, locals, rawin, stdin, stdout, stderr):
        self.introText = "Welcome to stackoverflow bash shell"
        self.locals = locals
        self.revision = 1.0
        self.rawin = rawin
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr

        self.more = False

        # bash process
        self.bp = Popen('bash', shell=False, stdout=PIPE, stdin=PIPE, stderr=PIPE)

        # start output grab thread
        self.outputThread = BashProcessThread(self.bp.stdout.readline)
        self.outputThread.start()

        # start err grab thread
        self.errorThread = BashProcessThread(self.bp.stderr.readline)
        self.errorThread.start()

    def getAutoCompleteKeys(self):
        return [ord('\t')]

    def getAutoCompleteList(self, *args, **kwargs):
        return []

    def getCallTip(self, command):
        return ""

    def push(self, command):
        command = command.strip()
        if not command: return

        self.bp.stdin.write(command+"\n")
        # wait a bit
        time.sleep(.1)

        # print output
        self.stdout.write(self.outputThread.getOutput())

        # print error
        self.stderr.write(self.errorThread.getOutput())

app = wx.PySimpleApp()
frame = wx.py.shell.ShellFrame(InterpClass=MyInterpretor)
frame.Show()
app.SetTopWindow(frame)
app.MainLoop()
Sign up to request clarification or add additional context in comments.

2 Comments

This example is useful. It still has some issues but I think it's enough to give me an idea of what can and can't be done. If a better answer doesn't appear in the next couple of days I'll accept this one.
I know this question is old but I was unable to find anything better so I hope to ask a follow up question in this subject. I would like to have two ShellFrame vertically in one window! Is it possible? Using the above code I am only able to open two shellFrame at the same time but they are not encapsulated in one window.
2

I found the solution to my problem. It is funny how it never turned up in Google searches before now. It's not production-ready code, but ultimately, I was looking for a way to run a bash shell in a wxPython window.

http://sivachandran.blogspot.com/2008/04/termemulator-10-released.html on webarchive https://sourceforge.net/projects/termemulator/files/TermEmulator/1.0/

Comments

0

i searched but there doesn't seem to be any exiting bash shell for wxPython though wx.py module has Shell module which is for python interpretor good thing is you can pass your own interpretor to it,so I have come with very simple bash interpreter. example currently reads only one line from bash stdout, otherwise it will get stuck, in real code you must read output in thread or use select

import wx
import wx.py
from subprocess import Popen, PIPE

class MyInterpretor(object):
    def __init__(self, locals, rawin, stdin, stdout, stderr):
        self.introText = "Welcome to stackoverflow bash shell"
        self.locals = locals
        self.revision = 1.0
        self.rawin = rawin
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr

        #
        self.more = False

        # bash process
        self.bp = Popen('bash', shell=False, stdout=PIPE, stdin=PIPE, stderr=PIPE)


    def getAutoCompleteKeys(self):
        return [ord('\t')]

    def getAutoCompleteList(self, *args, **kwargs):
        return []

    def getCallTip(self, command):
        return ""

    def push(self, command):
        command = command.strip()
        if not command: return

        self.bp.stdin.write(command+"\n")
        self.stdout.write(self.bp.stdout.readline())

app = wx.PySimpleApp()
frame = wx.py.shell.ShellFrame(InterpClass=MyInterpretor)
frame.Show()
app.SetTopWindow(frame)
app.MainLoop()

5 Comments

Why -1 ? please atleast comment after putting downvote, so I have atleast chance to improve myself, I see no reason for down vote?
I downvoted because this isn't working code. Given there's a bounty, the bar is set a little higher than normal here. OK, the code runs without error but it's not something that can be used as-is, not when it reads only a single line of output after each command. I appreciate the pointer to the wx.py.Shell widget and how to create a special interpreter but that's still not enough IMO to earn the bounty.
This is not supposed to be a rentacoder service where you expect me to give full solution, I think whatever i gave was a good starting point and I have alsogiven reason how to improve it e.g. I said I am using readline because reading all dat a will block so if you don't like you do not sleetc it and I will not get bounty but by down voting you discourage me from giving the correct hints for final solution
if you want a a complete solution put a bid on rentacoder/elancer etc and people will give complete solution with liablity
Anurag, I'm sorry you think I'm being unfair. I honestly don't think you've fully answered my question. I don't need a full, production ready solution but I do want something that gives me more of a hint than what you provided. Show a bit more of an explanation for the wx.py.Shell API, or show a solution that doesn't have the critical flaw of only reading a single line of output. I think you underestimate the difficulty of reading the output and posting it back to the shell widget in a usable fashion.
0

Going to see what i can come up with.

But if you change your mind and decide to use pygtk instead, here it is:

enjoy!!

EDIT

I started making a poor man's version of a terminal using the text control widget. I stopped because there are flaws that can't be fixed, such as when you use the sudo command.

import wx
import subprocess

class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)

        self.prompt = "user@stackOvervlow:~ "
        self.textctrl = wx.TextCtrl(self, -1, '', style=wx.TE_PROCESS_ENTER|wx.TE_MULTILINE)
        self.default_txt = self.textctrl.GetDefaultStyle()
        self.textctrl.AppendText(self.prompt)

        self.__set_properties()
        self.__do_layout()
        self.__bind_events()


    def __bind_events(self):
        self.Bind(wx.EVT_TEXT_ENTER, self.__enter)


    def __enter(self, e):
        self.value = (self.textctrl.GetValue())
        self.eval_last_line()
        e.Skip()


    def __set_properties(self):
        self.SetTitle("Poor Man's Terminal")
        self.SetSize((800, 600))
        self.textctrl.SetFocus()

    def __do_layout(self):
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_1.Add(self.textctrl, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_1)
        self.Layout()

    def eval_last_line(self):
        nl = self.textctrl.GetNumberOfLines()
        ln =  self.textctrl.GetLineText(nl-1)
        ln = ln[len(self.prompt):]
        args = ln.split(" ")

        proc = subprocess.Popen(args, stdout=subprocess.PIPE)
        retvalue = proc.communicate()[0]

        c = wx.Colour(239, 177, 177)
        tc = wx.TextAttr(c)
        self.textctrl.SetDefaultStyle(tc)
        self.textctrl.AppendText(retvalue)
        self.textctrl.SetDefaultStyle(self.default_txt)
        self.textctrl.AppendText(self.prompt)
        self.textctrl.SetInsertionPoint(GetLastPosition() - 1)

if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame_1 = MyFrame(None, -1, "")
    app.SetTopWindow(frame_1)
    frame_1.Show()
    app.MainLoop()

If really wanted, this could be worked upon.

3 Comments

pygtk isn't an option, this is for an existing app with a few thousand lines of wxPython. Personally, if I had a choice I'd use Tkinter.
You spawn a process for each command? That won't work because changing directories won't persist from one command to the next. You also don't handle multi-line commands. Thanks anyway.
No Problem. I would try asking in #wxwidgets on freenode.
0

After cleanup this works on windows 12. Errors are forwarded to STDOUT. There's no newline after prompt nay more:

import wx
import subprocess
from pprint import pprint as pp
class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)

        self.prompt = "user@stackOvervlow:~ "
        self.textctrl = wx.TextCtrl(self, -1, '', style=wx.TE_PROCESS_ENTER|wx.TE_MULTILINE)
        self.default_txt = self.textctrl.GetDefaultStyle()
        self.textctrl.AppendText(self.prompt)

        self.__set_properties()
        self.__do_layout()
        self.__bind_events()


    def __bind_events(self):
        self.Bind(wx.EVT_TEXT_ENTER, self.__enter)


    def __enter(self, e):
        print('__enter')
        self.value = (self.textctrl.GetValue())
        self.eval_last_line()
        #e.Skip()


    def __set_properties(self):
        self.SetTitle("Poor Man's Terminal")
        self.SetSize((800, 600))
        self.textctrl.SetFocus()

    def __do_layout(self):
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_1.Add(self.textctrl, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_1)
        self.Layout()

    def eval_last_line(self):
        nl = self.textctrl.GetNumberOfLines()
        ln =  self.textctrl.GetLineText(nl-1)
        self.prompt = self.prompt.strip()
        ln = ln[len(self.prompt):]
        args = ln.split(" ")
        pp(args)
        proc = subprocess.Popen(['cmd', '/C']+args, stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
        retvalue = proc.communicate()[0]

        c = wx.Colour(239, 177, 177)
        tc = wx.TextAttr(c)
        self.textctrl.SetDefaultStyle(tc)
        self.textctrl.AppendText(b'\n' + retvalue)
        self.textctrl.SetDefaultStyle(self.default_txt)
        self.textctrl.AppendText('\n' + self.prompt)
        self.textctrl.SetInsertionPointEnd()
        pp(self.textctrl.GetValue())

if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame_1 = MyFrame(None, -1, "")
    app.SetTopWindow(frame_1)
    frame_1.Show()
    app.MainLoop()

And if you need colored errors and command history here's better version:

import wx
import subprocess
import wx.richtext as rt
from pprint import pprint as pp
class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)

        self.prompt = "user@stackOvervlow:~ "
        self.textctrl = rt.RichTextCtrl(self, -1, '', style=wx.TE_MULTILINE|wx.TE_PROCESS_ENTER|wx.TE_RICH2)
        font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        self.textctrl.SetFont(font)
        self.default_txt = wx.TextAttr(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT))
        #self.textctrl.BeginTextColour(wx.Colour(239, 177, 177))
        self.textctrl.AppendText(self.prompt)

        self.__set_properties()
        self.__do_layout()
        self.__bind_events()
        self.command_history = []
        self.command_index = 0
        self.textctrl.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)        

    def OnKeyDown(self, event):
        keycode = event.GetKeyCode()
        if keycode == wx.WXK_UP:
            if self.command_index > 0:
                self.command_index -= 1
                self.set_current_line(self.command_history[self.command_index])
        elif keycode == wx.WXK_DOWN:
            if self.command_index < len(self.command_history) - 1:
                self.command_index += 1
                self.set_current_line(self.command_history[self.command_index])
        else:
            event.Skip() 
    def set_current_line(self, text):
        nl = self.textctrl.GetNumberOfLines()
        self.textctrl.Remove(self.textctrl.XYToPosition(len(self.prompt), nl - 1), self.textctrl.GetLastPosition())
        self.textctrl.AppendText(text)


    def __bind_events(self):
        self.Bind(wx.EVT_TEXT_ENTER, self.__enter)


    def __enter(self, e):
        print('__enter')
        self.value = (self.textctrl.GetValue())
        self.eval_last_line()
        #e.Skip()


    def __set_properties(self):
        self.SetTitle("Poor Man's Terminal")
        self.SetSize((800, 600))
        self.textctrl.SetFocus()

    def __do_layout(self):
        sizer_1 = wx.BoxSizer(wx.VERTICAL)
        sizer_1.Add(self.textctrl, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_1)
        self.Layout()

    def eval_last_line(self):
        nl = self.textctrl.GetNumberOfLines()
        last_line = self.textctrl.GetLineText(nl - 2)
        command = last_line[len(self.prompt):].strip()
        if not command:  # If the command is empty, return early


            start = self.textctrl.GetLastPosition()
            
            self.textctrl.AppendText(self.prompt)  # Remove the '\n' before the prompt
            end = self.textctrl.GetLastPosition() 
            self.textctrl.SetInsertionPointEnd()
            self.textctrl.ShowPosition(self.textctrl.GetLastPosition())  # Scroll to the bottom

            return
        self.command_history.append(command)
        self.command_index = len(self.command_history)
        args = command.split()
        proc = subprocess.Popen(['cmd', '/C']+args, stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
        retvalue = proc.communicate()[0]
        if proc.returncode != 0:
            c = wx.Colour(255, 0, 0)  # Red color for error
        else:
            c = self.default_txt  # black  color for success
        start = self.textctrl.GetLastPosition()  # Get the position of the last character
        self.textctrl.AppendText(retvalue.decode())  # Remove the '\n' before the output
        end = self.textctrl.GetLastPosition()  # Get the position of the new last character

        self.textctrl.SetStyle(start, end, wx.TextAttr(c))  # Set the style of the appended text
        start = self.textctrl.GetLastPosition()
        
        self.textctrl.AppendText(self.prompt)  # Remove the '\n' before the prompt
        end = self.textctrl.GetLastPosition() 
        self.textctrl.SetInsertionPointEnd()
        self.textctrl.ShowPosition(self.textctrl.GetLastPosition())  # Scroll to the bottom

if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame_1 = MyFrame(None, -1, "")
    app.SetTopWindow(frame_1)
    frame_1.Show()
    app.MainLoop()

enter image description here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.