Hi all,

Here's one that has me stumped. I'm trying to build a system shell with a Tkinter GUI in Python, and I can't figure out what to do with one of the widgets. Here's the program concept:

I have two major widgets: a ScrolledText widget and an Entry widget. The purpose of the ScrolledText widget is to display the results (from STDOUT or STDERR) of what the user has typed. To that end, it is user-uneditable. The Entry widget contains both the prompt and the command line; when the application starts, this widget displays the prompt (which looks like "CURRENT_DIR >> "), steals focus from the application, and sets the cursor to the rightmost index, just after the double-carets. The Entry widget is bound to a command-processing function, so that when the user hits the Enter key, the command-processing function grabs whatever has been typed in the Entry widget and does the appropriate thing.

The problem: because Entry widgets are designed to let users do whatever they want to the text inside, the user can delete the prompt! This is no good. I need some way to protect the prompt text without disabling the entire widget. I don't want to move the prompt to another widget, because that will mess up the feel I'm trying to give the program (the feel shared by every terminal application ever created). The only solution I could come up with was to bind the Entry widget to a "correcting function" so that whenever Tk registers Backspace, Delete, or Left-click events, the function activates and rewrites whatever part of the prompt the user has gotten rid of. This seems to me like a pretty horrific kludge, and I'm sure it will backfire on me somehow.

Does anyone have any better ideas (within the confines of Tkinter)? Is there a secret widget I've never heard of that emulates terminals?

Thanks in advance for any help you can give. Here is the code (I ripped out the guts of the processing function to save space, and the kludge I mentioned is not included):

import os
from Tkinter import *
from ScrolledText import ScrolledText
# -- The application class
class Dumbshell:
     # -- Initializes new instances of Dumbshell
     def __init__(self, root):
          # Set instance-specific variables (widget dimensions,
          # command variable)
          self.display_height = 20
          self.display_width = 80
          self.line_width = 80
          self.command = StringVar()
          self.command.set(os.path.basename(os.path.abspath("."))+ \
                           " >> ")
          # Add the window Frame to which all widgets will be bound
          self.mainframe = Frame(root)
          self.mainframe.pack()
          # Add a ScrolledText widget (the display)
          self.display = ScrolledText(self.mainframe,
                                     height=self.display_height,
                                     width=self.display_width,
                                     state=NORMAL)
          self.display.pack()
          # Blank out the display by inserting newlines
          # Ensures that our output gets printed at the bottom of the
          # display
          for x in range(self.display_height): self.display.insert(END, "\n")
          # Make the display user-uneditable by changing state to
          # DISABLED
          self.display.config(state=DISABLED)
          # Add an Entry widget (the command-line)
          self.entry = Entry(self.mainframe,
                             width=self.line_width,
                             textvariable=self.command,
                             state=NORMAL)
          self.entry.pack(anchor="w")
          # Bind the Entry widget to the Enter button and self.process
          self.entry.bind(sequence="<Return>", func=self.process)
          # Steal the focus
          self.entry.focus_set()
          # Send the cursor to the end of the prompt, where it belongs
          self.entry.icursor(END)
     # -- Processes entered commands, does the actual shell work
     def process(self, args):
          # First things first: print the entered command in the display
          self.sendToDisplay(self.command.get()+"\n")
          # Process it
          # Lots of complicated stuff happens here, don't worry about it
          # Reset the command window to display only the prompt
          self.command.set(os.path.basename(os.path.abspath("."))+ \
                           " >> ");
     # -- Prints input string to the display widget
     def sendToDisplay(self, string):
          # Make the display editable
          self.display.config(state=NORMAL)
          # Add our string
          self.display.insert(END, string)
          # Scroll down to the bottom again
          self.display.see(END)
          # Make the display uneditable
          self.display.config(state=DISABLED)
     # Other functions which handle the builtins, other commands, etc
     # Don't sweat it
# -- The following code executes upon command-line invocation
root = Tk()
ds = Dumbshell(root)
root.mainloop()

I have just played a little with your code. I see what you mean. My solution would be to put the "tk>>" prompt in a label in front of the enter field and combine the two texts when the enter key is pressed.

Nice code by the way!

Thanks, vegaseat.

I was hesitant to do that because I want the program to look like a terminal - I don't want the Entry widget and the Label to look like separate entities.

So I got to thinking: what if I broke the command-line into a Label widget (the prompt) and an Entry widget (the command-line), then flattened the border around the Entry and ScrolledText widgets? With the exception of the scrollbar on the display, it should look just like one big widget, right? Here is the code, I removed the terminal behavior for clarity's sake:

import os
from Tkinter import *
from ScrolledText import ScrolledText
# -- The application class
class Dumbwidget:
     # -- Initializes new instances of Dumbwidget
     def __init__(self, root):
          # Set instance-specific variables (widget dimensions,
          # the prompt, the command variable
          self.display_height = 20
          self.display_width = 80
          self.line_width = 60
          self.command = StringVar()
          self.command.set("")
          self.prompt_text = os.path.basename(os.path.abspath("."))+ \
                             " >> "
          # Add the window Frame to which all widgets will be bound
          self.mainframe = Frame(root)
          self.mainframe.pack()
          # Add a ScrolledText widget (the display)
          self.display = ScrolledText(self.mainframe,
                                      height=self.display_height,
                                      width=self.display_width,
                                      relief="flat", state=DISABLED)
          self.display.pack()
          # Add a subframe to which the command-line widgets
          # (Label and Entry) will be bound
          self.subframe = Frame(self.mainframe)
          self.subframe.pack(anchor="w")
          # Add a Label widget (the prompt)
          self.prompt = Label(self.subframe, text=self.prompt_text)
          self.prompt.pack(side=LEFT)
          # Add an Entry widget (the command-line)
          self.entry = Entry(self.subframe,
                             width=self.line_width,
                             textvariable=self.command,
                             relief="flat", state=NORMAL)
          self.entry.pack(side=LEFT)
# -- The following code executes upon command-line invocation
root = Tk()
dw = Dumbwidget(root)
root.mainloop()

This would be fine, except that when the Entry widget takes focus it produces a black, single-pixel border around the text, and that is no good. It should look seamless, like it's part of the Label. I poked around with the Entry attributes, and it doesn't look like there's any way to make that border disappear.

Any ideas?

EDIT: Haha, I just found it on Google like five seconds after posting this. The attribute that controls the focus border width is highlightthickness; the relevant link is here.

:)

Okay! I am glad i was able to help in a small way at least!

Do you consider yourself a Pythonian or a Pythonista? :mrgreen:

I'm not really militant about my language choice, so I guess I'm a Pythonian, because Pythonista sounds like you code in the jungle while hoarding weapons. I've coded a lot in Java, some in C/C++, some in Perl, a few experiments with Scheme and Prolog, and I'm pretty handy with the Bash shell.

In fact, the only programming/scripting language I truly despise is - well, let's not get negative. Instead, let's say that I know the face of the foe I will slay come Ragnarok.

This question has already been answered. Start a new discussion instead.