1#! /usr/bin/env python3
2
3import sys
4if __name__ == "__main__":
5    sys.modules['idlelib.pyshell'] = sys.modules['__main__']
6
7try:
8    from tkinter import *
9except ImportError:
10    print("** IDLE can't import Tkinter.\n"
11          "Your Python may not be configured for Tk. **", file=sys.__stderr__)
12    raise SystemExit(1)
13
14# Valid arguments for the ...Awareness call below are defined in the following.
15# https://msdn.microsoft.com/en-us/library/windows/desktop/dn280512(v=vs.85).aspx
16if sys.platform == 'win32':
17    try:
18        import ctypes
19        PROCESS_SYSTEM_DPI_AWARE = 1  # Int required.
20        ctypes.OleDLL('shcore').SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)
21    except (ImportError, AttributeError, OSError):
22        pass
23
24from tkinter import messagebox
25
26from code import InteractiveInterpreter
27import itertools
28import linecache
29import os
30import os.path
31from platform import python_version
32import re
33import socket
34import subprocess
35from textwrap import TextWrapper
36import threading
37import time
38import tokenize
39import warnings
40
41from idlelib.colorizer import ColorDelegator
42from idlelib.config import idleConf
43from idlelib.delegator import Delegator
44from idlelib import debugger
45from idlelib import debugger_r
46from idlelib.editor import EditorWindow, fixwordbreaks
47from idlelib.filelist import FileList
48from idlelib.outwin import OutputWindow
49from idlelib import replace
50from idlelib import rpc
51from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile
52from idlelib.undo import UndoDelegator
53
54# Default for testing; defaults to True in main() for running.
55use_subprocess = False
56
57HOST = '127.0.0.1' # python execution server on localhost loopback
58PORT = 0  # someday pass in host, port for remote debug capability
59
60try:  # In case IDLE started with -n.
61    eof = 'Ctrl-D (end-of-file)'
62    exit.eof = eof
63    quit.eof = eof
64except NameError: # In case python started with -S.
65    pass
66
67# Override warnings module to write to warning_stream.  Initialize to send IDLE
68# internal warnings to the console.  ScriptBinding.check_syntax() will
69# temporarily redirect the stream to the shell window to display warnings when
70# checking user's code.
71warning_stream = sys.__stderr__  # None, at least on Windows, if no console.
72
73def idle_showwarning(
74        message, category, filename, lineno, file=None, line=None):
75    """Show Idle-format warning (after replacing warnings.showwarning).
76
77    The differences are the formatter called, the file=None replacement,
78    which can be None, the capture of the consequence AttributeError,
79    and the output of a hard-coded prompt.
80    """
81    if file is None:
82        file = warning_stream
83    try:
84        file.write(idle_formatwarning(
85                message, category, filename, lineno, line=line))
86        file.write(">>> ")
87    except (AttributeError, OSError):
88        pass  # if file (probably __stderr__) is invalid, skip warning.
89
90_warnings_showwarning = None
91
92def capture_warnings(capture):
93    "Replace warning.showwarning with idle_showwarning, or reverse."
94
95    global _warnings_showwarning
96    if capture:
97        if _warnings_showwarning is None:
98            _warnings_showwarning = warnings.showwarning
99            warnings.showwarning = idle_showwarning
100    else:
101        if _warnings_showwarning is not None:
102            warnings.showwarning = _warnings_showwarning
103            _warnings_showwarning = None
104
105capture_warnings(True)
106
107def extended_linecache_checkcache(filename=None,
108                                  orig_checkcache=linecache.checkcache):
109    """Extend linecache.checkcache to preserve the <pyshell#...> entries
110
111    Rather than repeating the linecache code, patch it to save the
112    <pyshell#...> entries, call the original linecache.checkcache()
113    (skipping them), and then restore the saved entries.
114
115    orig_checkcache is bound at definition time to the original
116    method, allowing it to be patched.
117    """
118    cache = linecache.cache
119    save = {}
120    for key in list(cache):
121        if key[:1] + key[-1:] == '<>':
122            save[key] = cache.pop(key)
123    orig_checkcache(filename)
124    cache.update(save)
125
126# Patch linecache.checkcache():
127linecache.checkcache = extended_linecache_checkcache
128
129
130class PyShellEditorWindow(EditorWindow):
131    "Regular text edit window in IDLE, supports breakpoints"
132
133    def __init__(self, *args):
134        self.breakpoints = []
135        EditorWindow.__init__(self, *args)
136        self.text.bind("<<set-breakpoint-here>>", self.set_breakpoint_here)
137        self.text.bind("<<clear-breakpoint-here>>", self.clear_breakpoint_here)
138        self.text.bind("<<open-python-shell>>", self.flist.open_shell)
139
140        #TODO: don't read/write this from/to .idlerc when testing
141        self.breakpointPath = os.path.join(
142                idleConf.userdir, 'breakpoints.lst')
143        # whenever a file is changed, restore breakpoints
144        def filename_changed_hook(old_hook=self.io.filename_change_hook,
145                                  self=self):
146            self.restore_file_breaks()
147            old_hook()
148        self.io.set_filename_change_hook(filename_changed_hook)
149        if self.io.filename:
150            self.restore_file_breaks()
151        self.color_breakpoint_text()
152
153    rmenu_specs = [
154        ("Cut", "<<cut>>", "rmenu_check_cut"),
155        ("Copy", "<<copy>>", "rmenu_check_copy"),
156        ("Paste", "<<paste>>", "rmenu_check_paste"),
157        (None, None, None),
158        ("Set Breakpoint", "<<set-breakpoint-here>>", None),
159        ("Clear Breakpoint", "<<clear-breakpoint-here>>", None)
160    ]
161
162    def color_breakpoint_text(self, color=True):
163        "Turn colorizing of breakpoint text on or off"
164        if self.io is None:
165            # possible due to update in restore_file_breaks
166            return
167        if color:
168            theme = idleConf.CurrentTheme()
169            cfg = idleConf.GetHighlight(theme, "break")
170        else:
171            cfg = {'foreground': '', 'background': ''}
172        self.text.tag_config('BREAK', cfg)
173
174    def set_breakpoint(self, lineno):
175        text = self.text
176        filename = self.io.filename
177        text.tag_add("BREAK", "%d.0" % lineno, "%d.0" % (lineno+1))
178        try:
179            self.breakpoints.index(lineno)
180        except ValueError:  # only add if missing, i.e. do once
181            self.breakpoints.append(lineno)
182        try:    # update the subprocess debugger
183            debug = self.flist.pyshell.interp.debugger
184            debug.set_breakpoint_here(filename, lineno)
185        except: # but debugger may not be active right now....
186            pass
187
188    def set_breakpoint_here(self, event=None):
189        text = self.text
190        filename = self.io.filename
191        if not filename:
192            text.bell()
193            return
194        lineno = int(float(text.index("insert")))
195        self.set_breakpoint(lineno)
196
197    def clear_breakpoint_here(self, event=None):
198        text = self.text
199        filename = self.io.filename
200        if not filename:
201            text.bell()
202            return
203        lineno = int(float(text.index("insert")))
204        try:
205            self.breakpoints.remove(lineno)
206        except:
207            pass
208        text.tag_remove("BREAK", "insert linestart",\
209                        "insert lineend +1char")
210        try:
211            debug = self.flist.pyshell.interp.debugger
212            debug.clear_breakpoint_here(filename, lineno)
213        except:
214            pass
215
216    def clear_file_breaks(self):
217        if self.breakpoints:
218            text = self.text
219            filename = self.io.filename
220            if not filename:
221                text.bell()
222                return
223            self.breakpoints = []
224            text.tag_remove("BREAK", "1.0", END)
225            try:
226                debug = self.flist.pyshell.interp.debugger
227                debug.clear_file_breaks(filename)
228            except:
229                pass
230
231    def store_file_breaks(self):
232        "Save breakpoints when file is saved"
233        # XXX 13 Dec 2002 KBK Currently the file must be saved before it can
234        #     be run.  The breaks are saved at that time.  If we introduce
235        #     a temporary file save feature the save breaks functionality
236        #     needs to be re-verified, since the breaks at the time the
237        #     temp file is created may differ from the breaks at the last
238        #     permanent save of the file.  Currently, a break introduced
239        #     after a save will be effective, but not persistent.
240        #     This is necessary to keep the saved breaks synched with the
241        #     saved file.
242        #
243        #     Breakpoints are set as tagged ranges in the text.
244        #     Since a modified file has to be saved before it is
245        #     run, and since self.breakpoints (from which the subprocess
246        #     debugger is loaded) is updated during the save, the visible
247        #     breaks stay synched with the subprocess even if one of these
248        #     unexpected breakpoint deletions occurs.
249        breaks = self.breakpoints
250        filename = self.io.filename
251        try:
252            with open(self.breakpointPath) as fp:
253                lines = fp.readlines()
254        except OSError:
255            lines = []
256        try:
257            with open(self.breakpointPath, "w") as new_file:
258                for line in lines:
259                    if not line.startswith(filename + '='):
260                        new_file.write(line)
261                self.update_breakpoints()
262                breaks = self.breakpoints
263                if breaks:
264                    new_file.write(filename + '=' + str(breaks) + '\n')
265        except OSError as err:
266            if not getattr(self.root, "breakpoint_error_displayed", False):
267                self.root.breakpoint_error_displayed = True
268                messagebox.showerror(title='IDLE Error',
269                    message='Unable to update breakpoint list:\n%s'
270                        % str(err),
271                    parent=self.text)
272
273    def restore_file_breaks(self):
274        self.text.update()   # this enables setting "BREAK" tags to be visible
275        if self.io is None:
276            # can happen if IDLE closes due to the .update() call
277            return
278        filename = self.io.filename
279        if filename is None:
280            return
281        if os.path.isfile(self.breakpointPath):
282            with open(self.breakpointPath) as fp:
283                lines = fp.readlines()
284            for line in lines:
285                if line.startswith(filename + '='):
286                    breakpoint_linenumbers = eval(line[len(filename)+1:])
287                    for breakpoint_linenumber in breakpoint_linenumbers:
288                        self.set_breakpoint(breakpoint_linenumber)
289
290    def update_breakpoints(self):
291        "Retrieves all the breakpoints in the current window"
292        text = self.text
293        ranges = text.tag_ranges("BREAK")
294        linenumber_list = self.ranges_to_linenumbers(ranges)
295        self.breakpoints = linenumber_list
296
297    def ranges_to_linenumbers(self, ranges):
298        lines = []
299        for index in range(0, len(ranges), 2):
300            lineno = int(float(ranges[index].string))
301            end = int(float(ranges[index+1].string))
302            while lineno < end:
303                lines.append(lineno)
304                lineno += 1
305        return lines
306
307# XXX 13 Dec 2002 KBK Not used currently
308#    def saved_change_hook(self):
309#        "Extend base method - clear breaks if module is modified"
310#        if not self.get_saved():
311#            self.clear_file_breaks()
312#        EditorWindow.saved_change_hook(self)
313
314    def _close(self):
315        "Extend base method - clear breaks when module is closed"
316        self.clear_file_breaks()
317        EditorWindow._close(self)
318
319
320class PyShellFileList(FileList):
321    "Extend base class: IDLE supports a shell and breakpoints"
322
323    # override FileList's class variable, instances return PyShellEditorWindow
324    # instead of EditorWindow when new edit windows are created.
325    EditorWindow = PyShellEditorWindow
326
327    pyshell = None
328
329    def open_shell(self, event=None):
330        if self.pyshell:
331            self.pyshell.top.wakeup()
332        else:
333            self.pyshell = PyShell(self)
334            if self.pyshell:
335                if not self.pyshell.begin():
336                    return None
337        return self.pyshell
338
339
340class ModifiedColorDelegator(ColorDelegator):
341    "Extend base class: colorizer for the shell window itself"
342    def recolorize_main(self):
343        self.tag_remove("TODO", "1.0", "iomark")
344        self.tag_add("SYNC", "1.0", "iomark")
345        ColorDelegator.recolorize_main(self)
346
347    def removecolors(self):
348        # Don't remove shell color tags before "iomark"
349        for tag in self.tagdefs:
350            self.tag_remove(tag, "iomark", "end")
351
352
353class ModifiedUndoDelegator(UndoDelegator):
354    "Extend base class: forbid insert/delete before the I/O mark"
355    def insert(self, index, chars, tags=None):
356        try:
357            if self.delegate.compare(index, "<", "iomark"):
358                self.delegate.bell()
359                return
360        except TclError:
361            pass
362        UndoDelegator.insert(self, index, chars, tags)
363
364    def delete(self, index1, index2=None):
365        try:
366            if self.delegate.compare(index1, "<", "iomark"):
367                self.delegate.bell()
368                return
369        except TclError:
370            pass
371        UndoDelegator.delete(self, index1, index2)
372
373    def undo_event(self, event):
374        # Temporarily monkey-patch the delegate's .insert() method to
375        # always use the "stdin" tag.  This is needed for undo-ing
376        # deletions to preserve the "stdin" tag, because UndoDelegator
377        # doesn't preserve tags for deleted text.
378        orig_insert = self.delegate.insert
379        self.delegate.insert = \
380            lambda index, chars: orig_insert(index, chars, "stdin")
381        try:
382            super().undo_event(event)
383        finally:
384            self.delegate.insert = orig_insert
385
386
387class UserInputTaggingDelegator(Delegator):
388    """Delegator used to tag user input with "stdin"."""
389    def insert(self, index, chars, tags=None):
390        if tags is None:
391            tags = "stdin"
392        self.delegate.insert(index, chars, tags)
393
394
395class MyRPCClient(rpc.RPCClient):
396
397    def handle_EOF(self):
398        "Override the base class - just re-raise EOFError"
399        raise EOFError
400
401def restart_line(width, filename):  # See bpo-38141.
402    """Return width long restart line formatted with filename.
403
404    Fill line with balanced '='s, with any extras and at least one at
405    the beginning.  Do not end with a trailing space.
406    """
407    tag = f"= RESTART: {filename or 'Shell'} ="
408    if width >= len(tag):
409        div, mod = divmod((width -len(tag)), 2)
410        return f"{(div+mod)*'='}{tag}{div*'='}"
411    else:
412        return tag[:-2]  # Remove ' ='.
413
414
415class ModifiedInterpreter(InteractiveInterpreter):
416
417    def __init__(self, tkconsole):
418        self.tkconsole = tkconsole
419        locals = sys.modules['__main__'].__dict__
420        InteractiveInterpreter.__init__(self, locals=locals)
421        self.restarting = False
422        self.subprocess_arglist = None
423        self.port = PORT
424        self.original_compiler_flags = self.compile.compiler.flags
425
426    _afterid = None
427    rpcclt = None
428    rpcsubproc = None
429
430    def spawn_subprocess(self):
431        if self.subprocess_arglist is None:
432            self.subprocess_arglist = self.build_subprocess_arglist()
433        self.rpcsubproc = subprocess.Popen(self.subprocess_arglist)
434
435    def build_subprocess_arglist(self):
436        assert (self.port!=0), (
437            "Socket should have been assigned a port number.")
438        w = ['-W' + s for s in sys.warnoptions]
439        # Maybe IDLE is installed and is being accessed via sys.path,
440        # or maybe it's not installed and the idle.py script is being
441        # run from the IDLE source directory.
442        del_exitf = idleConf.GetOption('main', 'General', 'delete-exitfunc',
443                                       default=False, type='bool')
444        command = f"__import__('idlelib.run').run.main({del_exitf!r})"
445        return [sys.executable] + w + ["-c", command, str(self.port)]
446
447    def start_subprocess(self):
448        addr = (HOST, self.port)
449        # GUI makes several attempts to acquire socket, listens for connection
450        for i in range(3):
451            time.sleep(i)
452            try:
453                self.rpcclt = MyRPCClient(addr)
454                break
455            except OSError:
456                pass
457        else:
458            self.display_port_binding_error()
459            return None
460        # if PORT was 0, system will assign an 'ephemeral' port. Find it out:
461        self.port = self.rpcclt.listening_sock.getsockname()[1]
462        # if PORT was not 0, probably working with a remote execution server
463        if PORT != 0:
464            # To allow reconnection within the 2MSL wait (cf. Stevens TCP
465            # V1, 18.6),  set SO_REUSEADDR.  Note that this can be problematic
466            # on Windows since the implementation allows two active sockets on
467            # the same address!
468            self.rpcclt.listening_sock.setsockopt(socket.SOL_SOCKET,
469                                           socket.SO_REUSEADDR, 1)
470        self.spawn_subprocess()
471        #time.sleep(20) # test to simulate GUI not accepting connection
472        # Accept the connection from the Python execution server
473        self.rpcclt.listening_sock.settimeout(10)
474        try:
475            self.rpcclt.accept()
476        except TimeoutError:
477            self.display_no_subprocess_error()
478            return None
479        self.rpcclt.register("console", self.tkconsole)
480        self.rpcclt.register("stdin", self.tkconsole.stdin)
481        self.rpcclt.register("stdout", self.tkconsole.stdout)
482        self.rpcclt.register("stderr", self.tkconsole.stderr)
483        self.rpcclt.register("flist", self.tkconsole.flist)
484        self.rpcclt.register("linecache", linecache)
485        self.rpcclt.register("interp", self)
486        self.transfer_path(with_cwd=True)
487        self.poll_subprocess()
488        return self.rpcclt
489
490    def restart_subprocess(self, with_cwd=False, filename=''):
491        if self.restarting:
492            return self.rpcclt
493        self.restarting = True
494        # close only the subprocess debugger
495        debug = self.getdebugger()
496        if debug:
497            try:
498                # Only close subprocess debugger, don't unregister gui_adap!
499                debugger_r.close_subprocess_debugger(self.rpcclt)
500            except:
501                pass
502        # Kill subprocess, spawn a new one, accept connection.
503        self.rpcclt.close()
504        self.terminate_subprocess()
505        console = self.tkconsole
506        was_executing = console.executing
507        console.executing = False
508        self.spawn_subprocess()
509        try:
510            self.rpcclt.accept()
511        except TimeoutError:
512            self.display_no_subprocess_error()
513            return None
514        self.transfer_path(with_cwd=with_cwd)
515        console.stop_readline()
516        # annotate restart in shell window and mark it
517        console.text.delete("iomark", "end-1c")
518        console.write('\n')
519        console.write(restart_line(console.width, filename))
520        console.text.mark_set("restart", "end-1c")
521        console.text.mark_gravity("restart", "left")
522        if not filename:
523            console.showprompt()
524        # restart subprocess debugger
525        if debug:
526            # Restarted debugger connects to current instance of debug GUI
527            debugger_r.restart_subprocess_debugger(self.rpcclt)
528            # reload remote debugger breakpoints for all PyShellEditWindows
529            debug.load_breakpoints()
530        self.compile.compiler.flags = self.original_compiler_flags
531        self.restarting = False
532        return self.rpcclt
533
534    def __request_interrupt(self):
535        self.rpcclt.remotecall("exec", "interrupt_the_server", (), {})
536
537    def interrupt_subprocess(self):
538        threading.Thread(target=self.__request_interrupt).start()
539
540    def kill_subprocess(self):
541        if self._afterid is not None:
542            self.tkconsole.text.after_cancel(self._afterid)
543        try:
544            self.rpcclt.listening_sock.close()
545        except AttributeError:  # no socket
546            pass
547        try:
548            self.rpcclt.close()
549        except AttributeError:  # no socket
550            pass
551        self.terminate_subprocess()
552        self.tkconsole.executing = False
553        self.rpcclt = None
554
555    def terminate_subprocess(self):
556        "Make sure subprocess is terminated"
557        try:
558            self.rpcsubproc.kill()
559        except OSError:
560            # process already terminated
561            return
562        else:
563            try:
564                self.rpcsubproc.wait()
565            except OSError:
566                return
567
568    def transfer_path(self, with_cwd=False):
569        if with_cwd:        # Issue 13506
570            path = ['']     # include Current Working Directory
571            path.extend(sys.path)
572        else:
573            path = sys.path
574
575        self.runcommand("""if 1:
576        import sys as _sys
577        _sys.path = {!r}
578        del _sys
579        \n""".format(path))
580
581    active_seq = None
582
583    def poll_subprocess(self):
584        clt = self.rpcclt
585        if clt is None:
586            return
587        try:
588            response = clt.pollresponse(self.active_seq, wait=0.05)
589        except (EOFError, OSError, KeyboardInterrupt):
590            # lost connection or subprocess terminated itself, restart
591            # [the KBI is from rpc.SocketIO.handle_EOF()]
592            if self.tkconsole.closing:
593                return
594            response = None
595            self.restart_subprocess()
596        if response:
597            self.tkconsole.resetoutput()
598            self.active_seq = None
599            how, what = response
600            console = self.tkconsole.console
601            if how == "OK":
602                if what is not None:
603                    print(repr(what), file=console)
604            elif how == "EXCEPTION":
605                if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"):
606                    self.remote_stack_viewer()
607            elif how == "ERROR":
608                errmsg = "pyshell.ModifiedInterpreter: Subprocess ERROR:\n"
609                print(errmsg, what, file=sys.__stderr__)
610                print(errmsg, what, file=console)
611            # we received a response to the currently active seq number:
612            try:
613                self.tkconsole.endexecuting()
614            except AttributeError:  # shell may have closed
615                pass
616        # Reschedule myself
617        if not self.tkconsole.closing:
618            self._afterid = self.tkconsole.text.after(
619                self.tkconsole.pollinterval, self.poll_subprocess)
620
621    debugger = None
622
623    def setdebugger(self, debugger):
624        self.debugger = debugger
625
626    def getdebugger(self):
627        return self.debugger
628
629    def open_remote_stack_viewer(self):
630        """Initiate the remote stack viewer from a separate thread.
631
632        This method is called from the subprocess, and by returning from this
633        method we allow the subprocess to unblock.  After a bit the shell
634        requests the subprocess to open the remote stack viewer which returns a
635        static object looking at the last exception.  It is queried through
636        the RPC mechanism.
637
638        """
639        self.tkconsole.text.after(300, self.remote_stack_viewer)
640        return
641
642    def remote_stack_viewer(self):
643        from idlelib import debugobj_r
644        oid = self.rpcclt.remotequeue("exec", "stackviewer", ("flist",), {})
645        if oid is None:
646            self.tkconsole.root.bell()
647            return
648        item = debugobj_r.StubObjectTreeItem(self.rpcclt, oid)
649        from idlelib.tree import ScrolledCanvas, TreeNode
650        top = Toplevel(self.tkconsole.root)
651        theme = idleConf.CurrentTheme()
652        background = idleConf.GetHighlight(theme, 'normal')['background']
653        sc = ScrolledCanvas(top, bg=background, highlightthickness=0)
654        sc.frame.pack(expand=1, fill="both")
655        node = TreeNode(sc.canvas, None, item)
656        node.expand()
657        # XXX Should GC the remote tree when closing the window
658
659    gid = 0
660
661    def execsource(self, source):
662        "Like runsource() but assumes complete exec source"
663        filename = self.stuffsource(source)
664        self.execfile(filename, source)
665
666    def execfile(self, filename, source=None):
667        "Execute an existing file"
668        if source is None:
669            with tokenize.open(filename) as fp:
670                source = fp.read()
671                if use_subprocess:
672                    source = (f"__file__ = r'''{os.path.abspath(filename)}'''\n"
673                              + source + "\ndel __file__")
674        try:
675            code = compile(source, filename, "exec")
676        except (OverflowError, SyntaxError):
677            self.tkconsole.resetoutput()
678            print('*** Error in script or command!\n'
679                 'Traceback (most recent call last):',
680                  file=self.tkconsole.stderr)
681            InteractiveInterpreter.showsyntaxerror(self, filename)
682            self.tkconsole.showprompt()
683        else:
684            self.runcode(code)
685
686    def runsource(self, source):
687        "Extend base class method: Stuff the source in the line cache first"
688        filename = self.stuffsource(source)
689        # at the moment, InteractiveInterpreter expects str
690        assert isinstance(source, str)
691        # InteractiveInterpreter.runsource() calls its runcode() method,
692        # which is overridden (see below)
693        return InteractiveInterpreter.runsource(self, source, filename)
694
695    def stuffsource(self, source):
696        "Stuff source in the filename cache"
697        filename = "<pyshell#%d>" % self.gid
698        self.gid = self.gid + 1
699        lines = source.split("\n")
700        linecache.cache[filename] = len(source)+1, 0, lines, filename
701        return filename
702
703    def prepend_syspath(self, filename):
704        "Prepend sys.path with file's directory if not already included"
705        self.runcommand("""if 1:
706            _filename = {!r}
707            import sys as _sys
708            from os.path import dirname as _dirname
709            _dir = _dirname(_filename)
710            if not _dir in _sys.path:
711                _sys.path.insert(0, _dir)
712            del _filename, _sys, _dirname, _dir
713            \n""".format(filename))
714
715    def showsyntaxerror(self, filename=None):
716        """Override Interactive Interpreter method: Use Colorizing
717
718        Color the offending position instead of printing it and pointing at it
719        with a caret.
720
721        """
722        tkconsole = self.tkconsole
723        text = tkconsole.text
724        text.tag_remove("ERROR", "1.0", "end")
725        type, value, tb = sys.exc_info()
726        msg = getattr(value, 'msg', '') or value or "<no detail available>"
727        lineno = getattr(value, 'lineno', '') or 1
728        offset = getattr(value, 'offset', '') or 0
729        if offset == 0:
730            lineno += 1 #mark end of offending line
731        if lineno == 1:
732            pos = "iomark + %d chars" % (offset-1)
733        else:
734            pos = "iomark linestart + %d lines + %d chars" % \
735                  (lineno-1, offset-1)
736        tkconsole.colorize_syntax_error(text, pos)
737        tkconsole.resetoutput()
738        self.write("SyntaxError: %s\n" % msg)
739        tkconsole.showprompt()
740
741    def showtraceback(self):
742        "Extend base class method to reset output properly"
743        self.tkconsole.resetoutput()
744        self.checklinecache()
745        InteractiveInterpreter.showtraceback(self)
746        if self.tkconsole.getvar("<<toggle-jit-stack-viewer>>"):
747            self.tkconsole.open_stack_viewer()
748
749    def checklinecache(self):
750        c = linecache.cache
751        for key in list(c.keys()):
752            if key[:1] + key[-1:] != "<>":
753                del c[key]
754
755    def runcommand(self, code):
756        "Run the code without invoking the debugger"
757        # The code better not raise an exception!
758        if self.tkconsole.executing:
759            self.display_executing_dialog()
760            return 0
761        if self.rpcclt:
762            self.rpcclt.remotequeue("exec", "runcode", (code,), {})
763        else:
764            exec(code, self.locals)
765        return 1
766
767    def runcode(self, code):
768        "Override base class method"
769        if self.tkconsole.executing:
770            self.restart_subprocess()
771        self.checklinecache()
772        debugger = self.debugger
773        try:
774            self.tkconsole.beginexecuting()
775            if not debugger and self.rpcclt is not None:
776                self.active_seq = self.rpcclt.asyncqueue("exec", "runcode",
777                                                        (code,), {})
778            elif debugger:
779                debugger.run(code, self.locals)
780            else:
781                exec(code, self.locals)
782        except SystemExit:
783            if not self.tkconsole.closing:
784                if messagebox.askyesno(
785                    "Exit?",
786                    "Do you want to exit altogether?",
787                    default="yes",
788                    parent=self.tkconsole.text):
789                    raise
790                else:
791                    self.showtraceback()
792            else:
793                raise
794        except:
795            if use_subprocess:
796                print("IDLE internal error in runcode()",
797                      file=self.tkconsole.stderr)
798                self.showtraceback()
799                self.tkconsole.endexecuting()
800            else:
801                if self.tkconsole.canceled:
802                    self.tkconsole.canceled = False
803                    print("KeyboardInterrupt", file=self.tkconsole.stderr)
804                else:
805                    self.showtraceback()
806        finally:
807            if not use_subprocess:
808                try:
809                    self.tkconsole.endexecuting()
810                except AttributeError:  # shell may have closed
811                    pass
812
813    def write(self, s):
814        "Override base class method"
815        return self.tkconsole.stderr.write(s)
816
817    def display_port_binding_error(self):
818        messagebox.showerror(
819            "Port Binding Error",
820            "IDLE can't bind to a TCP/IP port, which is necessary to "
821            "communicate with its Python execution server.  This might be "
822            "because no networking is installed on this computer.  "
823            "Run IDLE with the -n command line switch to start without a "
824            "subprocess and refer to Help/IDLE Help 'Running without a "
825            "subprocess' for further details.",
826            parent=self.tkconsole.text)
827
828    def display_no_subprocess_error(self):
829        messagebox.showerror(
830            "Subprocess Connection Error",
831            "IDLE's subprocess didn't make connection.\n"
832            "See the 'Startup failure' section of the IDLE doc, online at\n"
833            "https://docs.python.org/3/library/idle.html#startup-failure",
834            parent=self.tkconsole.text)
835
836    def display_executing_dialog(self):
837        messagebox.showerror(
838            "Already executing",
839            "The Python Shell window is already executing a command; "
840            "please wait until it is finished.",
841            parent=self.tkconsole.text)
842
843
844class PyShell(OutputWindow):
845    from idlelib.squeezer import Squeezer
846
847    shell_title = "IDLE Shell " + python_version()
848
849    # Override classes
850    ColorDelegator = ModifiedColorDelegator
851    UndoDelegator = ModifiedUndoDelegator
852
853    # Override menus
854    menu_specs = [
855        ("file", "_File"),
856        ("edit", "_Edit"),
857        ("debug", "_Debug"),
858        ("options", "_Options"),
859        ("window", "_Window"),
860        ("help", "_Help"),
861    ]
862
863    # Extend right-click context menu
864    rmenu_specs = OutputWindow.rmenu_specs + [
865        ("Squeeze", "<<squeeze-current-text>>"),
866    ]
867    _idx = 1 + len(list(itertools.takewhile(
868        lambda rmenu_item: rmenu_item[0] != "Copy", rmenu_specs)
869    ))
870    rmenu_specs.insert(_idx, ("Copy with prompts",
871                              "<<copy-with-prompts>>",
872                              "rmenu_check_copy"))
873    del _idx
874
875    allow_line_numbers = False
876    user_input_insert_tags = "stdin"
877
878    # New classes
879    from idlelib.history import History
880    from idlelib.sidebar import ShellSidebar
881
882    def __init__(self, flist=None):
883        if use_subprocess:
884            ms = self.menu_specs
885            if ms[2][0] != "shell":
886                ms.insert(2, ("shell", "She_ll"))
887        self.interp = ModifiedInterpreter(self)
888        if flist is None:
889            root = Tk()
890            fixwordbreaks(root)
891            root.withdraw()
892            flist = PyShellFileList(root)
893
894        self.shell_sidebar = None  # initialized below
895
896        OutputWindow.__init__(self, flist, None, None)
897
898        self.usetabs = False
899        # indentwidth must be 8 when using tabs.  See note in EditorWindow:
900        self.indentwidth = 4
901
902        self.sys_ps1 = sys.ps1 if hasattr(sys, 'ps1') else '>>>\n'
903        self.prompt_last_line = self.sys_ps1.split('\n')[-1]
904        self.prompt = self.sys_ps1  # Changes when debug active
905
906        text = self.text
907        text.configure(wrap="char")
908        text.bind("<<newline-and-indent>>", self.enter_callback)
909        text.bind("<<plain-newline-and-indent>>", self.linefeed_callback)
910        text.bind("<<interrupt-execution>>", self.cancel_callback)
911        text.bind("<<end-of-file>>", self.eof_callback)
912        text.bind("<<open-stack-viewer>>", self.open_stack_viewer)
913        text.bind("<<toggle-debugger>>", self.toggle_debugger)
914        text.bind("<<toggle-jit-stack-viewer>>", self.toggle_jit_stack_viewer)
915        text.bind("<<copy-with-prompts>>", self.copy_with_prompts_callback)
916        if use_subprocess:
917            text.bind("<<view-restart>>", self.view_restart_mark)
918            text.bind("<<restart-shell>>", self.restart_shell)
919        self.squeezer = self.Squeezer(self)
920        text.bind("<<squeeze-current-text>>",
921                  self.squeeze_current_text_event)
922
923        self.save_stdout = sys.stdout
924        self.save_stderr = sys.stderr
925        self.save_stdin = sys.stdin
926        from idlelib import iomenu
927        self.stdin = StdInputFile(self, "stdin",
928                                  iomenu.encoding, iomenu.errors)
929        self.stdout = StdOutputFile(self, "stdout",
930                                    iomenu.encoding, iomenu.errors)
931        self.stderr = StdOutputFile(self, "stderr",
932                                    iomenu.encoding, "backslashreplace")
933        self.console = StdOutputFile(self, "console",
934                                     iomenu.encoding, iomenu.errors)
935        if not use_subprocess:
936            sys.stdout = self.stdout
937            sys.stderr = self.stderr
938            sys.stdin = self.stdin
939        try:
940            # page help() text to shell.
941            import pydoc # import must be done here to capture i/o rebinding.
942            # XXX KBK 27Dec07 use text viewer someday, but must work w/o subproc
943            pydoc.pager = pydoc.plainpager
944        except:
945            sys.stderr = sys.__stderr__
946            raise
947        #
948        self.history = self.History(self.text)
949        #
950        self.pollinterval = 50  # millisec
951
952        self.shell_sidebar = self.ShellSidebar(self)
953
954        # Insert UserInputTaggingDelegator at the top of the percolator,
955        # but make calls to text.insert() skip it.  This causes only insert
956        # events generated in Tcl/Tk to go through this delegator.
957        self.text.insert = self.per.top.insert
958        self.per.insertfilter(UserInputTaggingDelegator())
959
960    def ResetFont(self):
961        super().ResetFont()
962
963        if self.shell_sidebar is not None:
964            self.shell_sidebar.update_font()
965
966    def ResetColorizer(self):
967        super().ResetColorizer()
968
969        theme = idleConf.CurrentTheme()
970        tag_colors = {
971          "stdin": {'background': None, 'foreground': None},
972          "stdout": idleConf.GetHighlight(theme, "stdout"),
973          "stderr": idleConf.GetHighlight(theme, "stderr"),
974          "console": idleConf.GetHighlight(theme, "normal"),
975        }
976        for tag, tag_colors_config in tag_colors.items():
977            self.text.tag_configure(tag, **tag_colors_config)
978
979        if self.shell_sidebar is not None:
980            self.shell_sidebar.update_colors()
981
982    def replace_event(self, event):
983        replace.replace(self.text, insert_tags="stdin")
984        return "break"
985
986    def get_standard_extension_names(self):
987        return idleConf.GetExtensions(shell_only=True)
988
989    def get_prompt_text(self, first, last):
990        """Return text between first and last with prompts added."""
991        text = self.text.get(first, last)
992        lineno_range = range(
993            int(float(first)),
994            int(float(last))
995         )
996        prompts = [
997            self.shell_sidebar.line_prompts.get(lineno)
998            for lineno in lineno_range
999        ]
1000        return "\n".join(
1001            line if prompt is None else f"{prompt} {line}"
1002            for prompt, line in zip(prompts, text.splitlines())
1003        ) + "\n"
1004
1005
1006    def copy_with_prompts_callback(self, event=None):
1007        """Copy selected lines to the clipboard, with prompts.
1008
1009        This makes the copied text useful for doc-tests and interactive
1010        shell code examples.
1011
1012        This always copies entire lines, even if only part of the first
1013        and/or last lines is selected.
1014        """
1015        text = self.text
1016        selfirst = text.index('sel.first linestart')
1017        if selfirst is None:  # Should not be possible.
1018            return  # No selection, do nothing.
1019        sellast = text.index('sel.last')
1020        if sellast[-1] != '0':
1021            sellast = text.index("sel.last+1line linestart")
1022        text.clipboard_clear()
1023        prompt_text = self.get_prompt_text(selfirst, sellast)
1024        text.clipboard_append(prompt_text)
1025
1026    reading = False
1027    executing = False
1028    canceled = False
1029    endoffile = False
1030    closing = False
1031    _stop_readline_flag = False
1032
1033    def set_warning_stream(self, stream):
1034        global warning_stream
1035        warning_stream = stream
1036
1037    def get_warning_stream(self):
1038        return warning_stream
1039
1040    def toggle_debugger(self, event=None):
1041        if self.executing:
1042            messagebox.showerror("Don't debug now",
1043                "You can only toggle the debugger when idle",
1044                parent=self.text)
1045            self.set_debugger_indicator()
1046            return "break"
1047        else:
1048            db = self.interp.getdebugger()
1049            if db:
1050                self.close_debugger()
1051            else:
1052                self.open_debugger()
1053
1054    def set_debugger_indicator(self):
1055        db = self.interp.getdebugger()
1056        self.setvar("<<toggle-debugger>>", not not db)
1057
1058    def toggle_jit_stack_viewer(self, event=None):
1059        pass # All we need is the variable
1060
1061    def close_debugger(self):
1062        db = self.interp.getdebugger()
1063        if db:
1064            self.interp.setdebugger(None)
1065            db.close()
1066            if self.interp.rpcclt:
1067                debugger_r.close_remote_debugger(self.interp.rpcclt)
1068            self.resetoutput()
1069            self.console.write("[DEBUG OFF]\n")
1070            self.prompt = self.sys_ps1
1071            self.showprompt()
1072        self.set_debugger_indicator()
1073
1074    def open_debugger(self):
1075        if self.interp.rpcclt:
1076            dbg_gui = debugger_r.start_remote_debugger(self.interp.rpcclt,
1077                                                           self)
1078        else:
1079            dbg_gui = debugger.Debugger(self)
1080        self.interp.setdebugger(dbg_gui)
1081        dbg_gui.load_breakpoints()
1082        self.prompt = "[DEBUG ON]\n" + self.sys_ps1
1083        self.showprompt()
1084        self.set_debugger_indicator()
1085
1086    def debug_menu_postcommand(self):
1087        state = 'disabled' if self.executing else 'normal'
1088        self.update_menu_state('debug', '*tack*iewer', state)
1089
1090    def beginexecuting(self):
1091        "Helper for ModifiedInterpreter"
1092        self.resetoutput()
1093        self.executing = True
1094
1095    def endexecuting(self):
1096        "Helper for ModifiedInterpreter"
1097        self.executing = False
1098        self.canceled = False
1099        self.showprompt()
1100
1101    def close(self):
1102        "Extend EditorWindow.close()"
1103        if self.executing:
1104            response = messagebox.askokcancel(
1105                "Kill?",
1106                "Your program is still running!\n Do you want to kill it?",
1107                default="ok",
1108                parent=self.text)
1109            if response is False:
1110                return "cancel"
1111        self.stop_readline()
1112        self.canceled = True
1113        self.closing = True
1114        return EditorWindow.close(self)
1115
1116    def _close(self):
1117        "Extend EditorWindow._close(), shut down debugger and execution server"
1118        self.close_debugger()
1119        if use_subprocess:
1120            self.interp.kill_subprocess()
1121        # Restore std streams
1122        sys.stdout = self.save_stdout
1123        sys.stderr = self.save_stderr
1124        sys.stdin = self.save_stdin
1125        # Break cycles
1126        self.interp = None
1127        self.console = None
1128        self.flist.pyshell = None
1129        self.history = None
1130        EditorWindow._close(self)
1131
1132    def ispythonsource(self, filename):
1133        "Override EditorWindow method: never remove the colorizer"
1134        return True
1135
1136    def short_title(self):
1137        return self.shell_title
1138
1139    COPYRIGHT = \
1140          'Type "help", "copyright", "credits" or "license()" for more information.'
1141
1142    def begin(self):
1143        self.text.mark_set("iomark", "insert")
1144        self.resetoutput()
1145        if use_subprocess:
1146            nosub = ''
1147            client = self.interp.start_subprocess()
1148            if not client:
1149                self.close()
1150                return False
1151        else:
1152            nosub = ("==== No Subprocess ====\n\n" +
1153                    "WARNING: Running IDLE without a Subprocess is deprecated\n" +
1154                    "and will be removed in a later version. See Help/IDLE Help\n" +
1155                    "for details.\n\n")
1156            sys.displayhook = rpc.displayhook
1157
1158        self.write("Python %s on %s\n%s\n%s" %
1159                   (sys.version, sys.platform, self.COPYRIGHT, nosub))
1160        self.text.focus_force()
1161        self.showprompt()
1162        # User code should use separate default Tk root window
1163        import tkinter
1164        tkinter._support_default_root = True
1165        tkinter._default_root = None
1166        return True
1167
1168    def stop_readline(self):
1169        if not self.reading:  # no nested mainloop to exit.
1170            return
1171        self._stop_readline_flag = True
1172        self.top.quit()
1173
1174    def readline(self):
1175        save = self.reading
1176        try:
1177            self.reading = True
1178            self.top.mainloop()  # nested mainloop()
1179        finally:
1180            self.reading = save
1181        if self._stop_readline_flag:
1182            self._stop_readline_flag = False
1183            return ""
1184        line = self.text.get("iomark", "end-1c")
1185        if len(line) == 0:  # may be EOF if we quit our mainloop with Ctrl-C
1186            line = "\n"
1187        self.resetoutput()
1188        if self.canceled:
1189            self.canceled = False
1190            if not use_subprocess:
1191                raise KeyboardInterrupt
1192        if self.endoffile:
1193            self.endoffile = False
1194            line = ""
1195        return line
1196
1197    def isatty(self):
1198        return True
1199
1200    def cancel_callback(self, event=None):
1201        try:
1202            if self.text.compare("sel.first", "!=", "sel.last"):
1203                return # Active selection -- always use default binding
1204        except:
1205            pass
1206        if not (self.executing or self.reading):
1207            self.resetoutput()
1208            self.interp.write("KeyboardInterrupt\n")
1209            self.showprompt()
1210            return "break"
1211        self.endoffile = False
1212        self.canceled = True
1213        if (self.executing and self.interp.rpcclt):
1214            if self.interp.getdebugger():
1215                self.interp.restart_subprocess()
1216            else:
1217                self.interp.interrupt_subprocess()
1218        if self.reading:
1219            self.top.quit()  # exit the nested mainloop() in readline()
1220        return "break"
1221
1222    def eof_callback(self, event):
1223        if self.executing and not self.reading:
1224            return # Let the default binding (delete next char) take over
1225        if not (self.text.compare("iomark", "==", "insert") and
1226                self.text.compare("insert", "==", "end-1c")):
1227            return # Let the default binding (delete next char) take over
1228        if not self.executing:
1229            self.resetoutput()
1230            self.close()
1231        else:
1232            self.canceled = False
1233            self.endoffile = True
1234            self.top.quit()
1235        return "break"
1236
1237    def linefeed_callback(self, event):
1238        # Insert a linefeed without entering anything (still autoindented)
1239        if self.reading:
1240            self.text.insert("insert", "\n")
1241            self.text.see("insert")
1242        else:
1243            self.newline_and_indent_event(event)
1244        return "break"
1245
1246    def enter_callback(self, event):
1247        if self.executing and not self.reading:
1248            return # Let the default binding (insert '\n') take over
1249        # If some text is selected, recall the selection
1250        # (but only if this before the I/O mark)
1251        try:
1252            sel = self.text.get("sel.first", "sel.last")
1253            if sel:
1254                if self.text.compare("sel.last", "<=", "iomark"):
1255                    self.recall(sel, event)
1256                    return "break"
1257        except:
1258            pass
1259        # If we're strictly before the line containing iomark, recall
1260        # the current line, less a leading prompt, less leading or
1261        # trailing whitespace
1262        if self.text.compare("insert", "<", "iomark linestart"):
1263            # Check if there's a relevant stdin range -- if so, use it.
1264            # Note: "stdin" blocks may include several successive statements,
1265            # so look for "console" tags on the newline before each statement
1266            # (and possibly on prompts).
1267            prev = self.text.tag_prevrange("stdin", "insert")
1268            if (
1269                    prev and
1270                    self.text.compare("insert", "<", prev[1]) and
1271                    # The following is needed to handle empty statements.
1272                    "console" not in self.text.tag_names("insert")
1273            ):
1274                prev_cons = self.text.tag_prevrange("console", "insert")
1275                if prev_cons and self.text.compare(prev_cons[1], ">=", prev[0]):
1276                    prev = (prev_cons[1], prev[1])
1277                next_cons = self.text.tag_nextrange("console", "insert")
1278                if next_cons and self.text.compare(next_cons[0], "<", prev[1]):
1279                    prev = (prev[0], self.text.index(next_cons[0] + "+1c"))
1280                self.recall(self.text.get(prev[0], prev[1]), event)
1281                return "break"
1282            next = self.text.tag_nextrange("stdin", "insert")
1283            if next and self.text.compare("insert lineend", ">=", next[0]):
1284                next_cons = self.text.tag_nextrange("console", "insert lineend")
1285                if next_cons and self.text.compare(next_cons[0], "<", next[1]):
1286                    next = (next[0], self.text.index(next_cons[0] + "+1c"))
1287                self.recall(self.text.get(next[0], next[1]), event)
1288                return "break"
1289            # No stdin mark -- just get the current line, less any prompt
1290            indices = self.text.tag_nextrange("console", "insert linestart")
1291            if indices and \
1292               self.text.compare(indices[0], "<=", "insert linestart"):
1293                self.recall(self.text.get(indices[1], "insert lineend"), event)
1294            else:
1295                self.recall(self.text.get("insert linestart", "insert lineend"), event)
1296            return "break"
1297        # If we're between the beginning of the line and the iomark, i.e.
1298        # in the prompt area, move to the end of the prompt
1299        if self.text.compare("insert", "<", "iomark"):
1300            self.text.mark_set("insert", "iomark")
1301        # If we're in the current input and there's only whitespace
1302        # beyond the cursor, erase that whitespace first
1303        s = self.text.get("insert", "end-1c")
1304        if s and not s.strip():
1305            self.text.delete("insert", "end-1c")
1306        # If we're in the current input before its last line,
1307        # insert a newline right at the insert point
1308        if self.text.compare("insert", "<", "end-1c linestart"):
1309            self.newline_and_indent_event(event)
1310            return "break"
1311        # We're in the last line; append a newline and submit it
1312        self.text.mark_set("insert", "end-1c")
1313        if self.reading:
1314            self.text.insert("insert", "\n")
1315            self.text.see("insert")
1316        else:
1317            self.newline_and_indent_event(event)
1318        self.text.update_idletasks()
1319        if self.reading:
1320            self.top.quit() # Break out of recursive mainloop()
1321        else:
1322            self.runit()
1323        return "break"
1324
1325    def recall(self, s, event):
1326        # remove leading and trailing empty or whitespace lines
1327        s = re.sub(r'^\s*\n', '', s)
1328        s = re.sub(r'\n\s*$', '', s)
1329        lines = s.split('\n')
1330        self.text.undo_block_start()
1331        try:
1332            self.text.tag_remove("sel", "1.0", "end")
1333            self.text.mark_set("insert", "end-1c")
1334            prefix = self.text.get("insert linestart", "insert")
1335            if prefix.rstrip().endswith(':'):
1336                self.newline_and_indent_event(event)
1337                prefix = self.text.get("insert linestart", "insert")
1338            self.text.insert("insert", lines[0].strip(),
1339                             self.user_input_insert_tags)
1340            if len(lines) > 1:
1341                orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0)
1342                new_base_indent  = re.search(r'^([ \t]*)', prefix).group(0)
1343                for line in lines[1:]:
1344                    if line.startswith(orig_base_indent):
1345                        # replace orig base indentation with new indentation
1346                        line = new_base_indent + line[len(orig_base_indent):]
1347                    self.text.insert('insert', '\n' + line.rstrip(),
1348                                     self.user_input_insert_tags)
1349        finally:
1350            self.text.see("insert")
1351            self.text.undo_block_stop()
1352
1353    _last_newline_re = re.compile(r"[ \t]*(\n[ \t]*)?\Z")
1354    def runit(self):
1355        index_before = self.text.index("end-2c")
1356        line = self.text.get("iomark", "end-1c")
1357        # Strip off last newline and surrounding whitespace.
1358        # (To allow you to hit return twice to end a statement.)
1359        line = self._last_newline_re.sub("", line)
1360        input_is_complete = self.interp.runsource(line)
1361        if not input_is_complete:
1362            if self.text.get(index_before) == '\n':
1363                self.text.tag_remove(self.user_input_insert_tags, index_before)
1364            self.shell_sidebar.update_sidebar()
1365
1366    def open_stack_viewer(self, event=None):
1367        if self.interp.rpcclt:
1368            return self.interp.remote_stack_viewer()
1369        try:
1370            sys.last_traceback
1371        except:
1372            messagebox.showerror("No stack trace",
1373                "There is no stack trace yet.\n"
1374                "(sys.last_traceback is not defined)",
1375                parent=self.text)
1376            return
1377        from idlelib.stackviewer import StackBrowser
1378        StackBrowser(self.root, self.flist)
1379
1380    def view_restart_mark(self, event=None):
1381        self.text.see("iomark")
1382        self.text.see("restart")
1383
1384    def restart_shell(self, event=None):
1385        "Callback for Run/Restart Shell Cntl-F6"
1386        self.interp.restart_subprocess(with_cwd=True)
1387
1388    def showprompt(self):
1389        self.resetoutput()
1390
1391        prompt = self.prompt
1392        if self.sys_ps1 and prompt.endswith(self.sys_ps1):
1393            prompt = prompt[:-len(self.sys_ps1)]
1394        self.text.tag_add("console", "iomark-1c")
1395        self.console.write(prompt)
1396
1397        self.shell_sidebar.update_sidebar()
1398        self.text.mark_set("insert", "end-1c")
1399        self.set_line_and_column()
1400        self.io.reset_undo()
1401
1402    def show_warning(self, msg):
1403        width = self.interp.tkconsole.width
1404        wrapper = TextWrapper(width=width, tabsize=8, expand_tabs=True)
1405        wrapped_msg = '\n'.join(wrapper.wrap(msg))
1406        if not wrapped_msg.endswith('\n'):
1407            wrapped_msg += '\n'
1408        self.per.bottom.insert("iomark linestart", wrapped_msg, "stderr")
1409
1410    def resetoutput(self):
1411        source = self.text.get("iomark", "end-1c")
1412        if self.history:
1413            self.history.store(source)
1414        if self.text.get("end-2c") != "\n":
1415            self.text.insert("end-1c", "\n")
1416        self.text.mark_set("iomark", "end-1c")
1417        self.set_line_and_column()
1418        self.ctip.remove_calltip_window()
1419
1420    def write(self, s, tags=()):
1421        try:
1422            self.text.mark_gravity("iomark", "right")
1423            count = OutputWindow.write(self, s, tags, "iomark")
1424            self.text.mark_gravity("iomark", "left")
1425        except:
1426            raise ###pass  # ### 11Aug07 KBK if we are expecting exceptions
1427                           # let's find out what they are and be specific.
1428        if self.canceled:
1429            self.canceled = False
1430            if not use_subprocess:
1431                raise KeyboardInterrupt
1432        return count
1433
1434    def rmenu_check_cut(self):
1435        try:
1436            if self.text.compare('sel.first', '<', 'iomark'):
1437                return 'disabled'
1438        except TclError: # no selection, so the index 'sel.first' doesn't exist
1439            return 'disabled'
1440        return super().rmenu_check_cut()
1441
1442    def rmenu_check_paste(self):
1443        if self.text.compare('insert','<','iomark'):
1444            return 'disabled'
1445        return super().rmenu_check_paste()
1446
1447    def squeeze_current_text_event(self, event=None):
1448        self.squeezer.squeeze_current_text()
1449        self.shell_sidebar.update_sidebar()
1450
1451    def on_squeezed_expand(self, index, text, tags):
1452        self.shell_sidebar.update_sidebar()
1453
1454
1455def fix_x11_paste(root):
1456    "Make paste replace selection on x11.  See issue #5124."
1457    if root._windowingsystem == 'x11':
1458        for cls in 'Text', 'Entry', 'Spinbox':
1459            root.bind_class(
1460                cls,
1461                '<<Paste>>',
1462                'catch {%W delete sel.first sel.last}\n' +
1463                        root.bind_class(cls, '<<Paste>>'))
1464
1465
1466usage_msg = """\
1467
1468USAGE: idle  [-deins] [-t title] [file]*
1469       idle  [-dns] [-t title] (-c cmd | -r file) [arg]*
1470       idle  [-dns] [-t title] - [arg]*
1471
1472  -h         print this help message and exit
1473  -n         run IDLE without a subprocess (DEPRECATED,
1474             see Help/IDLE Help for details)
1475
1476The following options will override the IDLE 'settings' configuration:
1477
1478  -e         open an edit window
1479  -i         open a shell window
1480
1481The following options imply -i and will open a shell:
1482
1483  -c cmd     run the command in a shell, or
1484  -r file    run script from file
1485
1486  -d         enable the debugger
1487  -s         run $IDLESTARTUP or $PYTHONSTARTUP before anything else
1488  -t title   set title of shell window
1489
1490A default edit window will be bypassed when -c, -r, or - are used.
1491
1492[arg]* are passed to the command (-c) or script (-r) in sys.argv[1:].
1493
1494Examples:
1495
1496idle
1497        Open an edit window or shell depending on IDLE's configuration.
1498
1499idle foo.py foobar.py
1500        Edit the files, also open a shell if configured to start with shell.
1501
1502idle -est "Baz" foo.py
1503        Run $IDLESTARTUP or $PYTHONSTARTUP, edit foo.py, and open a shell
1504        window with the title "Baz".
1505
1506idle -c "import sys; print(sys.argv)" "foo"
1507        Open a shell window and run the command, passing "-c" in sys.argv[0]
1508        and "foo" in sys.argv[1].
1509
1510idle -d -s -r foo.py "Hello World"
1511        Open a shell window, run a startup script, enable the debugger, and
1512        run foo.py, passing "foo.py" in sys.argv[0] and "Hello World" in
1513        sys.argv[1].
1514
1515echo "import sys; print(sys.argv)" | idle - "foobar"
1516        Open a shell window, run the script piped in, passing '' in sys.argv[0]
1517        and "foobar" in sys.argv[1].
1518"""
1519
1520def main():
1521    import getopt
1522    from platform import system
1523    from idlelib import testing  # bool value
1524    from idlelib import macosx
1525
1526    global flist, root, use_subprocess
1527
1528    capture_warnings(True)
1529    use_subprocess = True
1530    enable_shell = False
1531    enable_edit = False
1532    debug = False
1533    cmd = None
1534    script = None
1535    startup = False
1536    try:
1537        opts, args = getopt.getopt(sys.argv[1:], "c:deihnr:st:")
1538    except getopt.error as msg:
1539        print(f"Error: {msg}\n{usage_msg}", file=sys.stderr)
1540        sys.exit(2)
1541    for o, a in opts:
1542        if o == '-c':
1543            cmd = a
1544            enable_shell = True
1545        if o == '-d':
1546            debug = True
1547            enable_shell = True
1548        if o == '-e':
1549            enable_edit = True
1550        if o == '-h':
1551            sys.stdout.write(usage_msg)
1552            sys.exit()
1553        if o == '-i':
1554            enable_shell = True
1555        if o == '-n':
1556            print(" Warning: running IDLE without a subprocess is deprecated.",
1557                  file=sys.stderr)
1558            use_subprocess = False
1559        if o == '-r':
1560            script = a
1561            if os.path.isfile(script):
1562                pass
1563            else:
1564                print("No script file: ", script)
1565                sys.exit()
1566            enable_shell = True
1567        if o == '-s':
1568            startup = True
1569            enable_shell = True
1570        if o == '-t':
1571            PyShell.shell_title = a
1572            enable_shell = True
1573    if args and args[0] == '-':
1574        cmd = sys.stdin.read()
1575        enable_shell = True
1576    # process sys.argv and sys.path:
1577    for i in range(len(sys.path)):
1578        sys.path[i] = os.path.abspath(sys.path[i])
1579    if args and args[0] == '-':
1580        sys.argv = [''] + args[1:]
1581    elif cmd:
1582        sys.argv = ['-c'] + args
1583    elif script:
1584        sys.argv = [script] + args
1585    elif args:
1586        enable_edit = True
1587        pathx = []
1588        for filename in args:
1589            pathx.append(os.path.dirname(filename))
1590        for dir in pathx:
1591            dir = os.path.abspath(dir)
1592            if not dir in sys.path:
1593                sys.path.insert(0, dir)
1594    else:
1595        dir = os.getcwd()
1596        if dir not in sys.path:
1597            sys.path.insert(0, dir)
1598    # check the IDLE settings configuration (but command line overrides)
1599    edit_start = idleConf.GetOption('main', 'General',
1600                                    'editor-on-startup', type='bool')
1601    enable_edit = enable_edit or edit_start
1602    enable_shell = enable_shell or not enable_edit
1603
1604    # Setup root.  Don't break user code run in IDLE process.
1605    # Don't change environment when testing.
1606    if use_subprocess and not testing:
1607        NoDefaultRoot()
1608    root = Tk(className="Idle")
1609    root.withdraw()
1610    from idlelib.run import fix_scaling
1611    fix_scaling(root)
1612
1613    # set application icon
1614    icondir = os.path.join(os.path.dirname(__file__), 'Icons')
1615    if system() == 'Windows':
1616        iconfile = os.path.join(icondir, 'idle.ico')
1617        root.wm_iconbitmap(default=iconfile)
1618    elif not macosx.isAquaTk():
1619        if TkVersion >= 8.6:
1620            ext = '.png'
1621            sizes = (16, 32, 48, 256)
1622        else:
1623            ext = '.gif'
1624            sizes = (16, 32, 48)
1625        iconfiles = [os.path.join(icondir, 'idle_%d%s' % (size, ext))
1626                     for size in sizes]
1627        icons = [PhotoImage(master=root, file=iconfile)
1628                 for iconfile in iconfiles]
1629        root.wm_iconphoto(True, *icons)
1630
1631    # start editor and/or shell windows:
1632    fixwordbreaks(root)
1633    fix_x11_paste(root)
1634    flist = PyShellFileList(root)
1635    macosx.setupApp(root, flist)
1636
1637    if enable_edit:
1638        if not (cmd or script):
1639            for filename in args[:]:
1640                if flist.open(filename) is None:
1641                    # filename is a directory actually, disconsider it
1642                    args.remove(filename)
1643            if not args:
1644                flist.new()
1645
1646    if enable_shell:
1647        shell = flist.open_shell()
1648        if not shell:
1649            return # couldn't open shell
1650        if macosx.isAquaTk() and flist.dict:
1651            # On OSX: when the user has double-clicked on a file that causes
1652            # IDLE to be launched the shell window will open just in front of
1653            # the file she wants to see. Lower the interpreter window when
1654            # there are open files.
1655            shell.top.lower()
1656    else:
1657        shell = flist.pyshell
1658
1659    # Handle remaining options. If any of these are set, enable_shell
1660    # was set also, so shell must be true to reach here.
1661    if debug:
1662        shell.open_debugger()
1663    if startup:
1664        filename = os.environ.get("IDLESTARTUP") or \
1665                   os.environ.get("PYTHONSTARTUP")
1666        if filename and os.path.isfile(filename):
1667            shell.interp.execfile(filename)
1668    if cmd or script:
1669        shell.interp.runcommand("""if 1:
1670            import sys as _sys
1671            _sys.argv = {!r}
1672            del _sys
1673            \n""".format(sys.argv))
1674        if cmd:
1675            shell.interp.execsource(cmd)
1676        elif script:
1677            shell.interp.prepend_syspath(script)
1678            shell.interp.execfile(script)
1679    elif shell:
1680        # If there is a shell window and no cmd or script in progress,
1681        # check for problematic issues and print warning message(s) in
1682        # the IDLE shell window; this is less intrusive than always
1683        # opening a separate window.
1684
1685        # Warn if the "Prefer tabs when opening documents" system
1686        # preference is set to "Always".
1687        prefer_tabs_preference_warning = macosx.preferTabsPreferenceWarning()
1688        if prefer_tabs_preference_warning:
1689            shell.show_warning(prefer_tabs_preference_warning)
1690
1691    while flist.inversedict:  # keep IDLE running while files are open.
1692        root.mainloop()
1693    root.destroy()
1694    capture_warnings(False)
1695
1696if __name__ == "__main__":
1697    main()
1698
1699capture_warnings(False)  # Make sure turned off; see issue 18081
1700