diff --git a/r2chat.py b/r2chat.py index 8e8f6c3..20ab7bb 100644 --- a/r2chat.py +++ b/r2chat.py @@ -1,323 +1,342 @@ -import tkinter as tk -from tkinter import scrolledtext, messagebox -from tkinter import ttk, colorchooser, font as tkfont -import os, sys, json -from pathlib import Path -import time -import getpass -from plyer import notification -import pygame as media -import webbrowser -import re - -if sys.platform == "win32": - ROOT = Path("C:/Users/Public") -else: - ROOT = Path("/var/tmp") -CHAT_FILE = ROOT / "r2chat" / "chat.txt" -CONFIG_FILE = Path.home() / "r2chat.config.json" -os.makedirs(os.path.dirname(CHAT_FILE), exist_ok=True) - -REFRESH_INTERVAL = 1000 -COLOR_PATTERN = re.compile(r"\(\((#[0-9a-fA-F]{6})\s*\|\s*([^)]+)\)\)") -status = "online" - -username = getpass.getuser() -media.mixer.init() - -DEFAULT_CONFIG = { - "font_family": "Lucida Console", - "font_size": 11, - "border_width": 2, - "bg_color": "#f0f0f0", - "fg_color": "#1e1e1e", - "user_color": "#0000ff", - "status_join": "{username} is now online.", - "status_leave": "{username} is now offline.", - "status_inactive": "{username} will be right back.", - "status_busy": "{username} is busy." -} - -def load_config(): - if CONFIG_FILE.exists(): - try: - with open(CONFIG_FILE, "r", encoding="utf-8") as f: - user_cfg = json.load(f) - return {**DEFAULT_CONFIG, **user_cfg} # user values override defaults - except Exception as e: - print(f":o config load failed, using defaults instead. error: {e}") - else: - with open(CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(DEFAULT_CONFIG, f, indent=2) - print(f":) created default config at {CONFIG_FILE}") - return DEFAULT_CONFIG.copy() - -class r2chat: - def __init__(self, root): - self.root = root - self.cfg = load_config() - self.root.title(f"r2chat :: {username}") - - self.last_size = 0 - - # configs - font = (self.cfg["font_family"], self.cfg["font_size"]) - bg_color = self.cfg["bg_color"] - fg_color = self.cfg["fg_color"] - - # menubar - self.menubar = tk.Menu(root) - self.root.config(menu=self.menubar) - - ## chat menu - self.chatmenu = tk.Menu(self.menubar, tearoff=0) - self.menubar.add_cascade(label="Chat", menu=self.chatmenu) - self.chatmenu.add_command(label="Registered users", command=lambda: print("")) - self.chatmenu.add_command(label="Leave", command=lambda: self.on_close()) - - ## insert menu - self.chatmenu = tk.Menu(self.menubar, tearoff=0) - self.menubar.add_cascade(label="Insert", menu=self.chatmenu) - self.chatmenu.add_command(label="Emoticon", command=lambda: print("")) - self.chatmenu.add_command(label="GIF", command=lambda: print("")) - - ## status menu - self.chatmenu = tk.Menu(self.menubar, tearoff=0) - self.menubar.add_cascade(label="Status", menu=self.chatmenu) - self.chatmenu.add_command(label="Online (All notifs)", command=lambda: self.setstatus("online")) - self.chatmenu.add_command(label="Inactive (Pings only)", command=lambda: self.setstatus("inactive")) - self.chatmenu.add_command(label="Busy (No notifs)", command=lambda: self.setstatus("busy")) - self.chatmenu.add_command(label="Appear offline", command=lambda: self.setstatus("offline")) - - ## menu buttons - self.menubar.add_command(label="Preferences", command=lambda: self.openprefs()) - self.menubar.add_command(label="Help", command=lambda: webbrowser.open_new_tab("https://forgejo.nishi.boats/Dogo6647/r2chat")) - - # MAIN MENU STUFFS - self.entry = tk.Entry(root, relief='sunken', borderwidth=self.cfg["border_width"]) - self.entry.pack(padx=10, pady=(0, 10), fill=tk.X, side=tk.BOTTOM) - - self.chat_area = scrolledtext.ScrolledText(root, wrap='word', font=font, bg=bg_color, fg=fg_color, relief='ridge', borderwidth=self.cfg["border_width"]) - self.chat_area.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) - self.chat_area.bind("", lambda e: "break") - - self.entry.bind("", self.send_message) - - if not os.path.exists(CHAT_FILE): - open(CHAT_FILE, "a").close() - - self.update_chat() - self.write_system_message(self.cfg["status_join"]) - - def on_close(self): - self.write_system_message(self.cfg["status_leave"]) - self.root.destroy() - - def write_system_message(self, msg): - line = f"---- {msg.format(username=username)} ----\n" - with open(CHAT_FILE, "a", encoding="utf-8") as f: - f.write(line) - - def send_message(self, event=None): - msg = self.entry.get().strip() - if msg: - timestamp = time.strftime("%y/%m/%d %H:%M") - line = f"[{timestamp}] (({self.cfg['user_color']} | {username})): {msg}\n" - try: - with open(CHAT_FILE, "a", encoding="utf-8") as f: - f.write(line) - except Exception as e: - print("oh noes! ", e) - - self.entry.delete(0, tk.END) - - def setstatus(self, new_status): - global status - - if status == new_status: - return - - status = new_status - - if status == "online": - self.write_system_message(msg=self.cfg["status_join"]) - elif status == "offline": - self.write_system_message(msg=self.cfg["status_leave"]) - elif status == "inactive": - self.write_system_message(msg=self.cfg["status_inactive"]) - elif status == "busy": - self.write_system_message(msg=self.cfg["status_busy"]) - - def openprefs(self): - PreferencesWindow(self.root, self.cfg, self.save_prefs) - - def save_prefs(self, new_cfg): - self.cfg = new_cfg - with open(CONFIG_FILE, "w", encoding="utf-8") as f: - json.dump(new_cfg, f, indent=2) - font = (new_cfg["font_family"], new_cfg["font_size"]) - self.chat_area.config(font=font, bg=new_cfg["bg_color"], fg=new_cfg["fg_color"], borderwidth=new_cfg["border_width"]) - self.entry.config(borderwidth=new_cfg["border_width"]) - - def render_text(self, text): - pos = 0 - - for match in COLOR_PATTERN.finditer(text): - start, end = match.span() - color = match.group(1) - content = match.group(2) - - if start > pos: - self.chat_area.insert(tk.END, text[pos:start]) - - tag_name = f"color_{color}" - if not tag_name in self.chat_area.tag_names(): - self.chat_area.tag_config(tag_name, foreground=color) - - self.chat_area.insert(tk.END, content, tag_name) - pos = end - - if pos < len(text): - self.chat_area.insert(tk.END, text[pos:]) - - def update_chat(self): - try: - current_size = os.path.getsize(CHAT_FILE) - - if current_size > self.last_size: - with open(CHAT_FILE, "r", encoding="utf-8") as f: - f.seek(self.last_size) - new_data = f.read() - self.last_size = current_size - - self.chat_area.config(state='normal') - if f"@{username} " in new_data and not "---" in new_data and not status == "busy" and not status == "offline": - media.mixer.Sound("assets/internal/ping.ogg").play() - messagebox.showinfo("Important message!", new_data) - if self.root.wm_state() == 'iconic': - if not f"@{username} " in new_data and not status == "busy" and not status == "inactive": - notification.notify(title="r2chat", message=new_data) - media.mixer.Sound("assets/internal/notif.ogg").play() - self.render_text(new_data) - self.chat_area.config(state='disabled') - self.chat_area.yview(tk.END) - - except Exception as e: - print("oh noes! ", e) - - self.root.after(REFRESH_INTERVAL, self.update_chat) - -class PreferencesWindow: - def __init__(self, parent, cfg, on_save): - self.parent = parent - self.cfg = cfg.copy() - self.on_save = on_save - - self.win = tk.Toplevel(parent) - self.win.title("r2chat prefs") - self.win.geometry("420x520") - self.win.resizable(False, False) - self.win.transient(parent) - self.win.grab_set() - - self.vars = {} - self._build() - - def _build(self): - notebook = ttk.Notebook(self.win) - notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - - self._build_appearance_tab(notebook) - self._build_status_messages_tab(notebook) - - btn_frame = tk.Frame(self.win) - btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) - - tk.Button(btn_frame, text="Save", width=10, command=self._save).pack(side=tk.RIGHT, padx=(4, 0)) - tk.Button(btn_frame, text="Cancel", width=10, command=self.win.destroy).pack(side=tk.RIGHT) - tk.Button(btn_frame, text="Back to defaults", command=self._reset).pack(side=tk.LEFT) - - def _build_appearance_tab(self, notebook): - frame = tk.Frame(notebook) - notebook.add(frame, text="Interface") - - # font family - self._row(frame, 0, "Font family") - font_var = tk.StringVar(value=self.cfg["font_family"]) - self.vars["font_family"] = font_var - families = sorted(tkfont.families()) - font_combo = ttk.Combobox(frame, textvariable=font_var, values=families, state="readonly", width=24) - font_combo.grid(row=0, column=1, padx=10, pady=6, sticky=tk.W) - - # font size - self._row(frame, 1, "Font size") - size_var = tk.IntVar(value=self.cfg["font_size"]) - self.vars["font_size"] = size_var - tk.Spinbox(frame, from_=7, to=24, textvariable=size_var, width=6).grid(row=1, column=1, padx=10, pady=6, sticky=tk.W) - - # border width - self._row(frame, 2, "Frame thickness") - bw_var = tk.IntVar(value=self.cfg["border_width"]) - self.vars["border_width"] = bw_var - tk.Spinbox(frame, from_=0, to=6, textvariable=bw_var, width=6).grid(row=2, column=1, padx=10, pady=6, sticky=tk.W) - - # color pickers - for i, (key, label) in enumerate([ - ("bg_color", "Background color"), - ("fg_color", "Text color"), - ("user_color", "Username color"), - ], start=3): - self._row(frame, i, label) - self._color_picker_row(frame, i, key) - - def _build_status_messages_tab(self, notebook): - frame = tk.Frame(notebook) - notebook.add(frame, text="Status messages") - - tk.Label(frame, text="Use {username} where you want your username to be.", fg="gray", font=("Lucida Console", 9)).grid( - row=0, column=0, columnspan=2, padx=10, pady=(10, 4), sticky=tk.W) - - for i, (key, label) in enumerate([("status_join", "Join message"),("status_leave", "Leave message"),("status_inactive", "Inactive message"),("status_busy", "Busy message")], start=1): - self._row(frame, i, label) - var = tk.StringVar(value=self.cfg[key]) - self.vars[key] = var - tk.Entry(frame, textvariable=var, width=32).grid(row=i, column=1, padx=10, pady=6, sticky=tk.W) - - def _row(self, frame, row, text): - tk.Label(frame, text=text, anchor=tk.W, width=18).grid(row=row, column=0, padx=10, pady=6, sticky=tk.W) - - def _color_picker_row(self, frame, row, key): - color_var = tk.StringVar(value=self.cfg[key]) - self.vars[key] = color_var - - preview = tk.Label(frame, bg=self.cfg[key], width=4, relief="sunken", borderwidth=1) - preview.grid(row=row, column=1, padx=(10, 4), pady=6, sticky=tk.W) - - def pick(v=color_var, p=preview, k=key): - result = colorchooser.askcolor(color=v.get(), title=k) - if result[1]: - v.set(result[1]) - p.config(bg=result[1]) - - tk.Button(frame, text="Choose…", command=pick, width=8).grid(row=row, column=1, padx=(44, 0), pady=6, sticky=tk.W) - - def _save(self): - for key, var in self.vars.items(): - self.cfg[key] = var.get() - self.on_save(self.cfg) - self.win.destroy() - - def _reset(self): - from __main__ import DEFAULT_CONFIG - for key, var in self.vars.items(): - if key in DEFAULT_CONFIG: - var.set(DEFAULT_CONFIG[key]) - -if __name__ == "__main__": - root = tk.Tk() - app = r2chat(root) - root.geometry("500x520") - root.minsize(360, 360) - root.attributes('-topmost', True) - root.protocol("WM_DELETE_WINDOW", app.on_close) - root.mainloop() +import tkinter as tk +from tkinter import scrolledtext, messagebox +from tkinter import ttk, colorchooser, font as tkfont +import os, sys, json +from pathlib import Path +import time +import getpass +from plyer import notification +import pygame as media +import webbrowser +import re +import subprocess + +if sys.platform == "win32": + ROOT = Path("C:/Users/Public") +else: + ROOT = Path("/var/tmp") + +try: + if sys.argv[1]!='chat': + CHANCHAN = sys.argv[1].replace(' ','_').replace('.','-') + CHANNEL = 'chat_'+CHANCHAN + else: + CHANNEL = 'chat' + CHANCHAN = 'chat' +except: + CHANNEL = 'chat' + CHANCHAN = 'chat' + +CHAT_FILE = ROOT / "r2chat" / f"{CHANNEL}.txt" +CONFIG_FILE = Path.home() / "r2chat.config.json" +os.makedirs(os.path.dirname(CHAT_FILE), exist_ok=True) + +REFRESH_INTERVAL = 1000 +COLOR_PATTERN = re.compile(r"\(\((#[0-9a-fA-F]{6})\s*\|\s*([^)]+)\)\)") +status = "online" + +username = getpass.getuser() +media.mixer.init() + +DEFAULT_CONFIG = { + "font_family": "Lucida Console", + "font_size": 11, + "border_width": 2, + "bg_color": "#f0f0f0", + "fg_color": "#1e1e1e", + "user_color": "#0000ff", + "status_join": "{username} is now online.", + "status_leave": "{username} is now offline.", + "status_inactive": "{username} will be right back.", + "status_busy": "{username} is busy." +} + +def load_config(): + if CONFIG_FILE.exists(): + try: + with open(CONFIG_FILE, "r", encoding="utf-8") as f: + user_cfg = json.load(f) + return {**DEFAULT_CONFIG, **user_cfg} # user values override defaults + except Exception as e: + print(f":o config load failed, using defaults instead. error: {e}") + else: + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(DEFAULT_CONFIG, f, indent=2) + print(f":) created default config at {CONFIG_FILE}") + return DEFAULT_CONFIG.copy() + + + +class r2chat: + def __init__(self, root): + self.root = root + self.cfg = load_config() + self.root.title(f"r2chat :: {username} / #{CHANCHAN}") + + self.last_size = 0 + + # configs + font = (self.cfg["font_family"], self.cfg["font_size"]) + bg_color = self.cfg["bg_color"] + fg_color = self.cfg["fg_color"] + + # menubar + self.menubar = tk.Menu(root) + self.root.config(menu=self.menubar) + + ## chat menu + self.chatmenu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Chat", menu=self.chatmenu) + self.chatmenu.add_command(label="Registered users", command=lambda: print("")) + self.chatmenu.add_command(label="Leave", command=lambda: self.on_close()) + + ## insert menu + self.chatmenu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Insert", menu=self.chatmenu) + self.chatmenu.add_command(label="Emoticon", command=lambda: print("")) + self.chatmenu.add_command(label="GIF", command=lambda: print("")) + + ## status menu + self.chatmenu = tk.Menu(self.menubar, tearoff=0) + self.menubar.add_cascade(label="Status", menu=self.chatmenu) + self.chatmenu.add_command(label="Online (All notifs)", command=lambda: self.setstatus("online")) + self.chatmenu.add_command(label="Inactive (Pings only)", command=lambda: self.setstatus("inactive")) + self.chatmenu.add_command(label="Busy (No notifs)", command=lambda: self.setstatus("busy")) + self.chatmenu.add_command(label="Appear offline", command=lambda: self.setstatus("offline")) + + ## menu buttons + self.menubar.add_command(label="Preferences", command=lambda: self.openprefs()) + self.menubar.add_command(label="Help", command=lambda: webbrowser.open_new_tab("https://forgejo.nishi.boats/Dogo6647/r2chat")) + + # MAIN MENU STUFFS + self.entry = tk.Entry(root, relief='sunken', borderwidth=self.cfg["border_width"]) + self.entry.pack(padx=10, pady=(0, 10), fill=tk.X, side=tk.BOTTOM) + + self.chat_area = scrolledtext.ScrolledText(root, wrap='word', font=font, bg=bg_color, fg=fg_color, relief='ridge', borderwidth=self.cfg["border_width"]) + self.chat_area.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) + self.chat_area.bind("", lambda e: "break") + + self.entry.bind("", self.send_message) + + if not os.path.exists(CHAT_FILE): + open(CHAT_FILE, "a").close() + + self.update_chat() + self.write_system_message(self.cfg["status_join"]) + + def on_close(self): + self.write_system_message(self.cfg["status_leave"]) + self.root.destroy() + + def write_system_message(self, msg): + line = f"---- {msg.format(username=username)} ----\n" + with open(CHAT_FILE, "a", encoding="utf-8") as f: + f.write(line) + + def send_message(self, event=None): + msg = self.entry.get().strip() + if msg and msg.startswith("/join #"): + subprocess.Popen(f'pythonw "{os.path.abspath(__file__)}" "{msg[7:len(msg)]}"', shell=True) + msg = '' + self.entry.delete(0, tk.END) + if msg: + timestamp = time.strftime("%y/%m/%d %H:%M") + line = f"[{timestamp}] (({self.cfg['user_color']} | {username})): {msg}\n" + try: + with open(CHAT_FILE, "a", encoding="utf-8") as f: + f.write(line) + except Exception as e: + print("oh noes! ", e) + + self.entry.delete(0, tk.END) + + def setstatus(self, new_status): + global status + + if status == new_status: + return + + status = new_status + + if status == "online": + self.write_system_message(msg=self.cfg["status_join"]) + elif status == "offline": + self.write_system_message(msg=self.cfg["status_leave"]) + elif status == "inactive": + self.write_system_message(msg=self.cfg["status_inactive"]) + elif status == "busy": + self.write_system_message(msg=self.cfg["status_busy"]) + + def openprefs(self): + PreferencesWindow(self.root, self.cfg, self.save_prefs) + + def save_prefs(self, new_cfg): + self.cfg = new_cfg + with open(CONFIG_FILE, "w", encoding="utf-8") as f: + json.dump(new_cfg, f, indent=2) + font = (new_cfg["font_family"], new_cfg["font_size"]) + self.chat_area.config(font=font, bg=new_cfg["bg_color"], fg=new_cfg["fg_color"], borderwidth=new_cfg["border_width"]) + self.entry.config(borderwidth=new_cfg["border_width"]) + + def render_text(self, text): + pos = 0 + + for match in COLOR_PATTERN.finditer(text): + start, end = match.span() + color = match.group(1) + content = match.group(2) + + if start > pos: + self.chat_area.insert(tk.END, text[pos:start]) + + tag_name = f"color_{color}" + if not tag_name in self.chat_area.tag_names(): + self.chat_area.tag_config(tag_name, foreground=color) + + self.chat_area.insert(tk.END, content, tag_name) + pos = end + + if pos < len(text): + self.chat_area.insert(tk.END, text[pos:]) + + def update_chat(self): + try: + current_size = os.path.getsize(CHAT_FILE) + + if current_size > self.last_size: + with open(CHAT_FILE, "r", encoding="utf-8") as f: + f.seek(self.last_size) + new_data = f.read() + self.last_size = current_size + + self.chat_area.config(state='normal') + if f"@{username} " in new_data and not "---" in new_data and not status == "busy" and not status == "offline": + media.mixer.Sound("assets/internal/ping.ogg").play() + messagebox.showinfo("Important message!", new_data) + if self.root.wm_state() == 'iconic': + if not f"@{username} " in new_data and not status == "busy" and not status == "inactive": + notification.notify(title="r2chat", message=new_data) + media.mixer.Sound("assets/internal/notif.ogg").play() + self.render_text(new_data) + self.chat_area.config(state='disabled') + self.chat_area.yview(tk.END) + + except Exception as e: + print("oh noes! ", e) + + self.root.after(REFRESH_INTERVAL, self.update_chat) + +class PreferencesWindow: + def __init__(self, parent, cfg, on_save): + self.parent = parent + self.cfg = cfg.copy() + self.on_save = on_save + + self.win = tk.Toplevel(parent) + self.win.title("r2chat prefs") + self.win.geometry("420x520") + self.win.resizable(False, False) + self.win.transient(parent) + self.win.grab_set() + + self.vars = {} + self._build() + + def _build(self): + notebook = ttk.Notebook(self.win) + notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + self._build_appearance_tab(notebook) + self._build_status_messages_tab(notebook) + + btn_frame = tk.Frame(self.win) + btn_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) + + tk.Button(btn_frame, text="Save", width=10, command=self._save).pack(side=tk.RIGHT, padx=(4, 0)) + tk.Button(btn_frame, text="Cancel", width=10, command=self.win.destroy).pack(side=tk.RIGHT) + tk.Button(btn_frame, text="Back to defaults", command=self._reset).pack(side=tk.LEFT) + + def _build_appearance_tab(self, notebook): + frame = tk.Frame(notebook) + notebook.add(frame, text="Interface") + + # font family + self._row(frame, 0, "Font family") + font_var = tk.StringVar(value=self.cfg["font_family"]) + self.vars["font_family"] = font_var + families = sorted(tkfont.families()) + font_combo = ttk.Combobox(frame, textvariable=font_var, values=families, state="readonly", width=24) + font_combo.grid(row=0, column=1, padx=10, pady=6, sticky=tk.W) + + # font size + self._row(frame, 1, "Font size") + size_var = tk.IntVar(value=self.cfg["font_size"]) + self.vars["font_size"] = size_var + tk.Spinbox(frame, from_=7, to=24, textvariable=size_var, width=6).grid(row=1, column=1, padx=10, pady=6, sticky=tk.W) + + # border width + self._row(frame, 2, "Frame thickness") + bw_var = tk.IntVar(value=self.cfg["border_width"]) + self.vars["border_width"] = bw_var + tk.Spinbox(frame, from_=0, to=6, textvariable=bw_var, width=6).grid(row=2, column=1, padx=10, pady=6, sticky=tk.W) + + # color pickers + for i, (key, label) in enumerate([ + ("bg_color", "Background color"), + ("fg_color", "Text color"), + ("user_color", "Username color"), + ], start=3): + self._row(frame, i, label) + self._color_picker_row(frame, i, key) + + def _build_status_messages_tab(self, notebook): + frame = tk.Frame(notebook) + notebook.add(frame, text="Status messages") + + tk.Label(frame, text="Use {username} where you want your username to be.", fg="gray", font=("Lucida Console", 9)).grid( + row=0, column=0, columnspan=2, padx=10, pady=(10, 4), sticky=tk.W) + + for i, (key, label) in enumerate([("status_join", "Join message"),("status_leave", "Leave message"),("status_inactive", "Inactive message"),("status_busy", "Busy message")], start=1): + self._row(frame, i, label) + var = tk.StringVar(value=self.cfg[key]) + self.vars[key] = var + tk.Entry(frame, textvariable=var, width=32).grid(row=i, column=1, padx=10, pady=6, sticky=tk.W) + + def _row(self, frame, row, text): + tk.Label(frame, text=text, anchor=tk.W, width=18).grid(row=row, column=0, padx=10, pady=6, sticky=tk.W) + + def _color_picker_row(self, frame, row, key): + color_var = tk.StringVar(value=self.cfg[key]) + self.vars[key] = color_var + + preview = tk.Label(frame, bg=self.cfg[key], width=4, relief="sunken", borderwidth=1) + preview.grid(row=row, column=1, padx=(10, 4), pady=6, sticky=tk.W) + + def pick(v=color_var, p=preview, k=key): + result = colorchooser.askcolor(color=v.get(), title=k) + if result[1]: + v.set(result[1]) + p.config(bg=result[1]) + + tk.Button(frame, text="Choose…", command=pick, width=8).grid(row=row, column=1, padx=(44, 0), pady=6, sticky=tk.W) + + def _save(self): + for key, var in self.vars.items(): + self.cfg[key] = var.get() + self.on_save(self.cfg) + self.win.destroy() + + def _reset(self): + from __main__ import DEFAULT_CONFIG + for key, var in self.vars.items(): + if key in DEFAULT_CONFIG: + var.set(DEFAULT_CONFIG[key]) + +if __name__ == "__main__": + root = tk.Tk() + app = r2chat(root) + root.geometry("500x520") + root.minsize(360, 360) + root.attributes('-topmost', True) + root.protocol("WM_DELETE_WINDOW", app.on_close) + root.mainloop()