text: import ast
  import os.path
  import tkinter as tk
  from datetime import datetime
  from tkinter import ttk
  
  from thonny import codeview, get_workbench, ui_utils, THONNY_USER_DIR
  from thonny.base_file_browser import BaseLocalFileBrowser
  from thonny.plugins.coloring import SyntaxColorer
  from thonny.ui_utils import lookup_style_option, CommonDialog
  
  
  class ReplayWindow(CommonDialog):
      def __init__(self):
          super().__init__(get_workbench(), background=lookup_style_option("TFrame", "background"))
          ui_utils.set_zoomed(self, True)
  
          self.main_pw = ReplayerPanedWindow(self, orient=tk.HORIZONTAL, sashwidth=10)
          self.center_pw = ReplayerPanedWindow(self.main_pw, orient=tk.VERTICAL, sashwidth=10)
          self.right_frame = ttk.Frame(self.main_pw)
          self.right_pw = ReplayerPanedWindow(self.right_frame, orient=tk.VERTICAL, sashwidth=10)
          self.editor_notebook = ReplayerEditorNotebook(self.center_pw)
          shell_book = ttk.Notebook(self.main_pw)
          self.shell = ShellFrame(shell_book)
          self.details_frame = EventDetailsFrame(self.right_pw)
          self.log_frame = LogFrame(
              self.right_pw, self.editor_notebook, self.shell, self.details_frame
          )
          self.browser = ReplayerFileBrowser(self.main_pw, self.log_frame)
          self.control_frame = ControlFrame(self.right_frame)
  
          self.main_pw.grid(padx=10, pady=10, sticky=tk.NSEW)
          self.main_pw.add(self.browser, width=200)
          self.main_pw.add(self.center_pw, width=1000)
          self.main_pw.add(self.right_frame, width=200)
          self.center_pw.add(self.editor_notebook, height=700)
          self.center_pw.add(shell_book, height=300)
          shell_book.add(self.shell, text="Shell")
          self.right_pw.grid(sticky=tk.NSEW)
          self.control_frame.grid(sticky=tk.NSEW)
          self.right_pw.add(self.log_frame, height=600)
          self.right_pw.add(self.details_frame, height=200)
          self.right_frame.columnconfigure(0, weight=1)
          self.right_frame.rowconfigure(0, weight=1)
  
          self.columnconfigure(0, weight=1)
          self.rowconfigure(0, weight=1)
  
  
  class ReplayerFileBrowser(BaseLocalFileBrowser):
      def __init__(self, master, log_frame):
          super().__init__(master, True)
          self.log_frame = log_frame
          self.configure(border=1, relief=tk.GROOVE)
  
          user_logs_path = os.path.join(THONNY_USER_DIR, "user_logs")
          if os.path.exists(user_logs_path):
              self.focus_into(user_logs_path)
          else:
              self.focus_into(os.path.expanduser("~"))
  
      def on_double_click(self, event):
          # self.save_current_folder()
          path = self.get_selected_path()
          if path:
              kind = self.get_selected_kind()
              if kind == "dir":
                  self.focus_into(path)
              else:
                  self.log_frame.load_log(path)
  
          return "break"  # avoid default action of opening the node
  
  
  class ControlFrame(ttk.Frame):
      def __init__(self, master, **kw):
          ttk.Frame.__init__(self, master=master, **kw)
  
          self.toggle_button = ttk.Button(self, text="Play")
          self.speed_scale = ttk.Scale(self, from_=1, to=100, orient=tk.HORIZONTAL)
  
          self.toggle_button.grid(row=0, column=0, sticky=tk.NSEW, pady=(10, 0), padx=(0, 5))
          self.speed_scale.grid(row=0, column=1, sticky=tk.NSEW, pady=(10, 0), padx=(5, 0))
  
          self.columnconfigure(1, weight=1)
  
  
  class LogFrame(ui_utils.TreeFrame):
      def __init__(self, master, editor_book, shell, details_frame):
          ui_utils.TreeFrame.__init__(self, master, ("desc", "pause"))
  
          self.tree.heading("desc", text="Event", anchor=tk.W)
          self.tree.heading("pause", text="Pause (sec)", anchor=tk.W)
  
          self.configure(border=1, relief=tk.GROOVE)
  
          self.editor_notebook = editor_book
          self.shell = shell
          self.details_frame = details_frame
          self.all_events = []
          self.last_event_index = -1
          self.loading = False
  
      def load_log(self, filename):
          self._clear_tree()
          self.details_frame._clear_tree()
          self.all_events = []
          self.last_event_index = -1
          self.loading = True
          self.editor_notebook.reset()
          self.shell.reset()
  
          import json
  
          with open(filename, encoding="UTF-8") as f:
              events = json.load(f)
              last_event_time = None
              for event in events:
                  node_id = self.tree.insert("", "end")
                  self.tree.set(node_id, "desc", event["sequence"])
                  if len(event["time"]) == 19:
                      # 0 fraction may have been skipped
                      event["time"] += ".0"
                  event_time = datetime.strptime(event["time"], "%Y-%m-%dT%H:%M:%S.%f")
                  if last_event_time:
                      delta = event_time - last_event_time
                      pause = delta.seconds
                  else:
                      pause = 0
                  self.tree.set(node_id, "pause", str(pause if pause else ""))
                  self.all_events.append(event)
  
                  last_event_time = event_time
  
          self.loading = False
  
      def replay_event(self, event):
          "this should be called with events in correct order"
          # print("log replay", event)
  
          if "text_widget_id" in event:
              if (
                  event.get("text_widget_context", None) == "shell"
                  or event.get("text_widget_class") == "ShellText"
              ):
                  self.shell.replay_event(event)
              else:
                  self.editor_notebook.replay_event(event)
  
      def reset(self):
          self.shell.reset()
          self.editor_notebook.reset()
          self.last_event_index = -1
  
      def on_select(self, event):
          # parameter "event" is here tkinter event
          if self.loading:
              return
          iid = self.tree.focus()
          if iid != "":
              self.select_event(self.tree.index(iid))
  
      def select_event(self, event_index):
          event = self.all_events[event_index]
          self.details_frame.load_event(event)
  
          # here event means logged event
          if event_index > self.last_event_index:
              # replay all events between last replayed event up to and including this event
              while self.last_event_index < event_index:
                  self.replay_event(self.all_events[self.last_event_index + 1])
                  self.last_event_index += 1
  
          elif event_index < self.last_event_index:
              # Undo by reseting and replaying again
              self.reset()
              self.select_event(event_index)
  
  
  class EventDetailsFrame(ui_utils.TreeFrame):
      def __init__(self, master):
          ui_utils.TreeFrame.__init__(self, master, columns=("attribute", "value"))
          self.tree.heading("attribute", text="Attribute", anchor=tk.W)
          self.tree.heading("value", text="Value", anchor=tk.W)
          self.configure(border=1, relief=tk.GROOVE)
  
      def load_event(self, event):
          self._clear_tree()
          for name in self.order_keys(event):
              node_id = self.tree.insert("", "end")
              self.tree.set(node_id, "attribute", name)
              self.tree.set(node_id, "value", event[name])
  
      def order_keys(self, event):
          return event.keys()
  
  
  class ReplayerCodeView(ttk.Frame):
      def __init__(self, master):
          ttk.Frame.__init__(self, master)
  
          self.vbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
          self.vbar.grid(row=0, column=2, sticky=tk.NSEW)
          self.hbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
          self.hbar.grid(row=1, column=0, sticky=tk.NSEW, columnspan=2)
          self.text = codeview.SyntaxText(
              self,
              yscrollcommand=self.vbar.set,
              xscrollcommand=self.hbar.set,
              borderwidth=0,
              font="EditorFont",
              wrap=tk.NONE,
              insertwidth=2,
              # selectborderwidth=2,
              inactiveselectbackground="gray",
              # highlightthickness=0, # TODO: try different in Mac and Linux
              # highlightcolor="gray",
              padx=5,
              pady=5,
              undo=True,
              autoseparators=False,
          )
  
          self.text.grid(row=0, column=1, sticky=tk.NSEW)
          self.hbar["command"] = self.text.xview
          self.vbar["command"] = self.text.yview
          self.columnconfigure(1, weight=1)
          self.rowconfigure(0, weight=1)
  
  
  class ReplayerEditor(ttk.Frame):
      def __init__(self, master):
          ttk.Frame.__init__(self, master)
          self.code_view = ReplayerCodeView(self)
          self.code_view.grid(sticky=tk.NSEW)
          self.columnconfigure(0, weight=1)
          self.rowconfigure(0, weight=1)
  
      def replay_event(self, event):
          if event["sequence"] in ["TextInsert", "TextDelete"]:
              if event["sequence"] == "TextInsert":
                  self.code_view.text.insert(
                      event["index"], event["text"], ast.literal_eval(event["tags"])
                  )
  
              elif event["sequence"] == "TextDelete":
                  if event["index2"] and event["index2"] != "None":
                      self.code_view.text.delete(event["index1"], event["index2"])
                  else:
                      self.code_view.text.delete(event["index1"])
  
              self.see_event(event)
  
      def see_event(self, event):
          for key in ["index", "index1", "index2"]:
              if key in event and event[key] and event[key] != "None":
                  self.code_view.text.see(event[key])
  
      def reset(self):
          self.code_view.text.delete("1.0", "end")
  
  
  class ReplayerEditorProper(ReplayerEditor):
      def __init__(self, master):
          ReplayerEditor.__init__(self, master)
          self.set_colorer()
  
      def set_colorer(self):
          self.colorer = SyntaxColorer(self.code_view.text)
  
      def replay_event(self, event):
          ReplayerEditor.replay_event(self, event)
          # TODO: some problem when doing fast rewind
          # self.colorer.notify_range("1.0", "end")
  
      def reset(self):
          ReplayerEditor.reset(self)
          self.set_colorer()
  
  
  class ReplayerEditorNotebook(ttk.Notebook):
      def __init__(self, master):
          ttk.Notebook.__init__(self, master, padding=0)
          self._editors_by_text_widget_id = {}
  
      def clear(self):
  
          for child in self.winfo_children():
              child.destroy()
  
          self._editors_by_text_widget_id = {}
  
      def get_editor_by_text_widget_id(self, text_widget_id):
          if text_widget_id not in self._editors_by_text_widget_id:
              editor = ReplayerEditorProper(self)
              self.add(editor, text="<untitled>")
              self._editors_by_text_widget_id[text_widget_id] = editor
  
          return self._editors_by_text_widget_id[text_widget_id]
  
      def replay_event(self, event):
          if "text_widget_id" in event:
              editor = self.get_editor_by_text_widget_id(event["text_widget_id"])
              # print(event.editor_id, id(editor), event)
              self.select(editor)
              editor.replay_event(event)
  
              if "filename" in event:
                  self.tab(editor, text=os.path.basename(event["filename"]))
  
      def reset(self):
          for editor in self.winfo_children():
              self.forget(editor)
              editor.destroy()
  
          self._editors_by_text_widget_id = {}
  
  
  class ShellFrame(ReplayerEditor):
      def __init__(self, master):
          ReplayerEditor.__init__(self, master)
  
          # TODO: use same source as shell
          vert_spacing = 10
          io_indent = 16
          self.code_view.text.tag_configure("toplevel", font="EditorFont")
          self.code_view.text.tag_configure("prompt", foreground="purple", font="BoldEditorFont")
          self.code_view.text.tag_configure("command", foreground="black")
          self.code_view.text.tag_configure("version", foreground="DarkGray")
          self.code_view.text.tag_configure("automagic", foreground="DarkGray")
          self.code_view.text.tag_configure(
              "value", foreground="DarkBlue"
          )  # TODO: see also _text_key_press and _text_key_release
          self.code_view.text.tag_configure("error", foreground="Red")
  
          self.code_view.text.tag_configure(
              "io", lmargin1=io_indent, lmargin2=io_indent, rmargin=io_indent, font="IOFont"
          )
          self.code_view.text.tag_configure("stdin", foreground="Blue")
          self.code_view.text.tag_configure("stdout", foreground="Black")
          self.code_view.text.tag_configure("stderr", foreground="Red")
          self.code_view.text.tag_configure("hyperlink", foreground="#3A66DD", underline=True)
  
          self.code_view.text.tag_configure("vertically_spaced", spacing1=vert_spacing)
          self.code_view.text.tag_configure("inactive", foreground="#aaaaaa")
  
  
  class ReplayerPanedWindow(tk.PanedWindow):
      def __init__(self, master=None, cnf={}, **kw):
          cnf = cnf.copy()
          cnf.update(kw)
          cnf["background"] = lookup_style_option("TFrame", "background")
          super().__init__(master=master, cnf=cnf)
  
  
  def load_plugin() -> None:
      def open_replayer():
          win = ReplayWindow()
          ui_utils.show_dialog(win)
  
      get_workbench().set_default("tools.replayer_last_browser_folder", None)
      if get_workbench().get_ui_mode() == "expert":
          get_workbench().add_command(
              "open_replayer", "tools", _("Open replayer..."), open_replayer, group=110
          )
  