Embedding cmd.exe into a Tkinter.Frame
Getting process output after it finishes is easy, this is not the problem being solved here, so if you want to do this you will be better served by the subprocess module included with Python. It starts getting problematic when you want to read process output during its execution.
The first problem is that file descriptors block, and you can't use the fcntl module to make them not block (because there isn't such fcntl under Windows). Instead, you have to use a combination of subprocess, process polling, msvcrt.get_osfhandle and win32pipe.PeekNamedPipe.
The second problem, and the bigger one, is when you want to read the output as it happens. If the process you want to watch happens to be a python program then this is actually simple, just run python in unbuffered mode (python -u), and you are done. If it is not, but if you are in a very good position to be able to modify its source and disable stdout buffering just because you want then, if setvbuf (or whatever else the language has for doing this) is available, things are fine again (or mostly fine). But I suspect in most situations you won't be able to solve the problem like this. This is where embedding a cmd.exe saves you, and it is actually very simple (since there is a module that does everything for us).
Bugs
- Take care with multi line paste;
cls is not going to do what you expect;
edit is not going to show an editor (try start edit instead);
prompt anything fails too;
The color command is not implemented;
- The great ^C isn't supported too (it actually copies text, instead of interrupting a process).
There are probably many other things, but these are the ones I noted on some quick usage.
Missing features
- Auto complete.
The code
As you may notice the pyconsole module does all the job, so be sure to get it.
(The code could be much shorter)
1 from Tkinter import Tk, Text, Frame, Scrollbar
2 try:
3 from ttk import Frame, Scrollbar
4 except ImportError:
5 pass
6
7 import pyconsole
8
9 class History(list):
10 def __init__(self):
11 self.reset_bookkepping()
12
13 def reset_bookkepping(self):
14 self._last_req = None
15
16 def get_item(self):
17 ret = ''
18 if self:
19 return self[self._last_req]
20 return ret
21
22 def next_relative(self):
23 if self:
24 if self._last_req is None:
25 self._last_req = 0
26 else:
27 self._last_req = (self._last_req + 1) % len(self)
28 return self.get_item()
29
30 def prev_relative(self):
31 if self:
32 if self._last_req is None:
33 self._last_req = -1
34 else:
35 self._last_req = -((-self._last_req + 1) % len(self))
36 return self.get_item()
37
38
39 class ConsoleProcessWindow(Text):
40
41 def __init__(self, master=None, **kwargs):
42 Text.__init__(self, **kwargs)
43 self.bind('<KeyPress>', self._evt_keypress)
44
45 # You can actually change this 'cmd.exe' to 'powershell.exe'
46 # or even something like 'ping -n 20 localhost' (if you use
47 # this one then be sure to not use console_process_end).
48 self.console_process = pyconsole.ConsoleProcess('cmd.exe',
49 console_update=self._console_update,
50 console_process_end=self._console_process_end)
51
52 self.history = History()
53 self._complete = False
54 self._last_line = 0
55
56 def send(self, command, partial=False):
57 """Send command to the cmd.exe."""
58 if partial:
59 end = '\t'
60 else:
61 end = '\n'
62 self.clear_cmd_line()
63
64 self.console_process.write(command + end)
65
66 def clear_cmd_line(self):
67 """Clear current text written in the command line."""
68 right_after_ps1, line = self._right_after_ps1()
69 self.delete(right_after_ps1, '%d.end' % line)
70
71 def get_cmd_line(self):
72 """Get current text written in the command line."""
73 right_after_ps1, line = self._right_after_ps1()
74 return self.get(right_after_ps1, '%d.end' % line)
75
76 def set_cmd_line(self, text):
77 """Write some text on the command line."""
78 if text is None:
79 return
80 self.insert(self._right_after_ps1()[0], text)
81
82
83 def _right_after_ps1(self):
84 index = str(self.tag_ranges('ps1')[1])
85 line = int(index.split('.')[0])
86 return index, line
87
88 def _evt_keypress(self, event):
89 self._complete = False
90
91 try:
92 right_after_ps1, line = self._right_after_ps1()
93 except IndexError:
94 return "break"
95
96 keysym = event.keysym.lower()
97 disallow = self.compare(self.index('insert'), '<',
98 right_after_ps1)
99 limit = self.compare(self.index('insert'), '==',
100 right_after_ps1)
101
102 if keysym in ('left', 'backspace') and limit:
103 custom_sel = self.tag_ranges('my_sel')
104 if keysym == 'backspace' and custom_sel:
105 # Assuming there is just one index selected
106 self.delete(custom_sel[0], custom_sel[1])
107 self.tag_delete('my_sel')
108 disallow = True
109
110 elif keysym in ('c', 'v') and event.state & 4:
111 # Allow Ctrl-C, Ctrl-V anywhere (take care with multiline Ctrl-V)
112 disallow = False
113
114 elif keysym == 'home':
115 if event.state & 1:
116 # Perform Shift-Home
117 self.tag_add('my_sel', right_after_ps1, '%d.end' % line)
118 self.tk.call('tk::TextKeySelect', event.widget,
119 right_after_ps1)
120 else:
121 # Unselect anything that might be selected by an earlier
122 # Shift-Home
123 self.tk.call('tk::TextSetCursor', event.widget,
124 right_after_ps1)
125 self.see('%d.0' % line)
126 disallow = True
127
128 elif keysym == 'up':
129 command = self.history.prev_relative()
130 self.clear_cmd_line()
131 self.set_cmd_line(command)
132 disallow = True
133
134 elif keysym == 'down':
135 command = self.history.next_relative()
136 self.clear_cmd_line()
137 self.set_cmd_line(command)
138 disallow = True
139
140 #elif keysym == 'tab':
141 # # let the shell complete
142 # self._complete = True
143 # command = self.get_cmd_line()
144 # self.send(command, partial=True)
145 # disallow = True
146
147 elif keysym == 'return':
148 # store command in the history and send it to the console process
149 command = self.get_cmd_line()
150 self.history.append(command)
151 self.history.reset_bookkepping()
152 self.send(command)
153 disallow = True
154
155 if disallow:
156 return "break"
157
158 def _console_process_end(self):
159 self.master.destroy()
160
161 def _console_update(self, x, y, text):
162 self.after_idle(lambda: self._update_text(x, y, text))
163
164 def _update_text(self, x, y, text):
165 y += 1
166
167 for i in xrange(y - self._last_line):
168 self.insert('%d.0' % (self._last_line + i + 1), '\n')
169
170 # XXX Allow auto complete
171
172 self._last_line = y
173
174 self.insert('%d.%d' % (y, x), text)
175 self.mark_set('insert', '%d.%d' % (y, x + len(text)))
176 self.see('insert')
177
178 # remove the extra newline
179 self.delete('end - 1 char')
180
181 self.tag_delete('ps1')
182 self.tag_add('ps1',
183 '%d.0' % y,
184 '%d.%d' % (y, text.find('>') + 1))
185
186 self.update_idletasks()
187
188
189 class ConsoleFrame(Frame):
190 def __init__(self):
191 Frame.__init__(self)
192
193 self.console = ConsoleProcessWindow(self, wrap='none')
194 self.console.focus()
195 hscroll = Scrollbar(orient='horizontal', command=self.console.xview)
196 vscroll = Scrollbar(orient='vertical', command=self.console.yview)
197 self.console.configure(
198 yscrollcommand=vscroll.set,
199 xscrollcommand=hscroll.set)
200
201 self.console.grid(sticky='news')
202 vscroll.grid(sticky='ns', row=0, column=1)
203 hscroll.grid(sticky='ew', row=1, column=0)
204
205
206 if __name__ == "__main__":
207 root = Tk()
208 frame = ConsoleFrame()
209 frame.grid()
210 root.grid_columnconfigure(0, weight=1)
211 root.grid_rowconfigure(0, weight=1)
212 root.mainloop()
213
And this is how it looks here (using ttk)