diff --git a/pathify.py b/pathify.py new file mode 100644 index 0000000..69419d5 --- /dev/null +++ b/pathify.py @@ -0,0 +1,4 @@ +def chn(a): + return a.replace(' ','_').replace('.','-').replace('\\','/').replace(':','/') +def dir(a) + return a.replace('/','+') \ No newline at end of file diff --git a/r2chat.py b/r2chat.py index 20ab7bb..2d37f07 100644 --- a/r2chat.py +++ b/r2chat.py @@ -10,6 +10,7 @@ import pygame as media import webbrowser import re import subprocess +import pathify if sys.platform == "win32": ROOT = Path("C:/Users/Public") @@ -18,8 +19,8 @@ else: try: if sys.argv[1]!='chat': - CHANCHAN = sys.argv[1].replace(' ','_').replace('.','-') - CHANNEL = 'chat_'+CHANCHAN + CHANCHAN = pathify.chn(sys.argv[1]) + CHANNEL = 'chat_'+pathify.dir(CHANCHAN) else: CHANNEL = 'chat' CHANCHAN = 'chat' @@ -27,7 +28,10 @@ except: CHANNEL = 'chat' CHANCHAN = 'chat' -CHAT_FILE = ROOT / "r2chat" / f"{CHANNEL}.txt" +CHANNELS_DIR = ROOT / "r2chat" +CHAT_FILE = CHANNELS_DIR / f"{CHANNEL}.txt" +INSTANCE_DIR = CHANNELS_DIR / "instance" +os.makedirs(INSTANCE_DIR, exist_ok=True) CONFIG_FILE = Path.home() / "r2chat.config.json" os.makedirs(os.path.dirname(CHAT_FILE), exist_ok=True) @@ -36,6 +40,8 @@ COLOR_PATTERN = re.compile(r"\(\((#[0-9a-fA-F]{6})\s*\|\s*([^)]+)\)\)") status = "online" username = getpass.getuser() +USERFILE = INSTANCE_DIR / f"{username}.json" + media.mixer.init() DEFAULT_CONFIG = { @@ -75,20 +81,28 @@ class r2chat: self.last_size = 0 - # configs + # configs font = (self.cfg["font_family"], self.cfg["font_size"]) bg_color = self.cfg["bg_color"] fg_color = self.cfg["fg_color"] - + + self.user_data = self.load_userdata() + if CHANNEL not in self.user_data["channels"]: + self.user_data["channels"].append(CHANNEL) + + self.user_data["status"] = status + self.save_userdata(self.user_data) + # 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()) + self.chatmenu.add_command(label="Registered users", command=lambda: self.show_users()) + self.chatmenu.add_command(label="Channel list", command=lambda: self.show_channels()) + self.chatmenu.add_command(label="Leave channel", command=lambda: self.on_close()) ## insert menu self.chatmenu = tk.Menu(self.menubar, tearoff=0) @@ -122,10 +136,17 @@ class r2chat: open(CHAT_FILE, "a").close() self.update_chat() - self.write_system_message(self.cfg["status_join"]) + self.write_system_message(f'◉ {self.cfg["status_join"]}') def on_close(self): - self.write_system_message(self.cfg["status_leave"]) + self.write_system_message(f'◌ {self.cfg["status_leave"]}') + + if CHANNEL in self.user_data["channels"]: + self.user_data["channels"].remove(CHANNEL) + if not self.user_data["channels"]: + self.user_data["status"] = "offline" + + self.save_userdata(self.user_data) self.root.destroy() def write_system_message(self, msg): @@ -136,7 +157,8 @@ class r2chat: 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) + channel = msg[7:len(msg)] + self.open_channel(channel) msg = '' self.entry.delete(0, tk.END) if msg: @@ -149,23 +171,116 @@ class r2chat: 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"]) + self.write_system_message(msg=f'((#00af00 | ◉)) {self.cfg["status_join"]}') elif status == "offline": - self.write_system_message(msg=self.cfg["status_leave"]) + self.write_system_message(msg=f'((#000000 | ◌)) {self.cfg["status_leave"]}') elif status == "inactive": - self.write_system_message(msg=self.cfg["status_inactive"]) + self.write_system_message(msg=f'((#ffaf00 | ◍)) {self.cfg["status_inactive"]}') elif status == "busy": - self.write_system_message(msg=self.cfg["status_busy"]) + self.write_system_message(msg=f'((#af0000 | ◎)) {self.cfg["status_busy"]}') + + self.user_data["status"] = status + self.save_userdata(self.user_data) + + def load_userdata(self): + path = USERFILE + if path.exists(): + try: + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except: + pass + return { + "username": username, + "status": "online", + "channels": [], + "last_seen": time.time() + } + + def save_userdata(self, data): + data["last_seen"] = time.time() + with open(USERFILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + def show_users(self): + win = tk.Toplevel(self.root) + win.title("Registered users") + win.geometry("420x320") + + tree = ttk.Treeview(win, columns=("user", "status", "channels", "last_seen"), show="headings") + + tree.heading("user", text="User") + tree.heading("status", text="Status") + tree.heading("channels", text="Channels") + tree.heading("last_seen", text="Last seen") + + tree.column("user", width=80) + tree.column("status", width=60, anchor="center") + tree.column("channels", width=140) + tree.column("last_seen", width=120, anchor="e") + + tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + for file in INSTANCE_DIR.glob("*.json"): + try: + with open(file, "r", encoding="utf-8") as f: + data = json.load(f) + + last_seen = time.strftime("%H:%M:%S", time.localtime(data.get("last_seen", 0))) + channels = ", ".join(data.get("channels", [])) + + tree.insert("", tk.END, values=( + file.stem, + data.get("status", "?"), + channels, + last_seen + )) + except Exception as e: + print(":( user read error:", e) + + def show_channels(self): + win = tk.Toplevel(self.root) + win.title("Channel list") + win.geometry("600x320") + + tree = ttk.Treeview(win, columns=("channel"), show="headings") + tree.heading("channel", text="Channels") + tree.column("channel", width=100) + tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + for file in CHANNELS_DIR.glob("*.txt"): + try: + tree.insert("", tk.END, values=( + file.stem.replace("chat_", "") + )) + except Exception as e: + print(":( instance read error:", e) + + def on_select(event): + selected = tree.focus() + if selected: + channel = tree.item(selected)["values"][0] + win.destroy() + self.open_channel(channel) + + tree.bind("", on_select) + + def open_channel(self, channel): + self.write_system_message(f"((#006fff | ⇄)) {username} is chatting in {channel}.") + if sys.platform == "win32": + subprocess.Popen(f'pythonw "{os.path.abspath(__file__)}" "{channel}"', shell=True) + else: + subprocess.Popen(f'python "{os.path.abspath(__file__)}" "{channel}"', shell=True) def openprefs(self): PreferencesWindow(self.root, self.cfg, self.save_prefs) @@ -203,7 +318,7 @@ class r2chat: try: current_size = os.path.getsize(CHAT_FILE) - if current_size > self.last_size: + 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() @@ -337,6 +452,6 @@ if __name__ == "__main__": app = r2chat(root) root.geometry("500x520") root.minsize(360, 360) - root.attributes('-topmost', True) + #root.attributes('-topmost', True) root.protocol("WM_DELETE_WINDOW", app.on_close) root.mainloop() diff --git a/r2chat_LEGACY.py b/r2chat_LEGACY.py new file mode 100644 index 0000000..8acbd1e --- /dev/null +++ b/r2chat_LEGACY.py @@ -0,0 +1,100 @@ +import tkinter as tk +from tkinter import scrolledtext +import os +import time +import getpass +from json import loads + +CHAT_FILE = "C:/Users/Public/r2chat/chat.txt" +REFRESH_INTERVAL = 1000 + +BORDER_WIDTH = 3 +THEMES = [['white','white','white','black','Basic'],['#000707','#002727','#001717','white','Aqua']] + +username = getpass.getuser() +CONFIG_FILE = f'C:/r2chat/legacy_config/{username}.r2cc' + +if not os.path.exists(CONFIG_FILE): + CFILE_C=open(CONFIG_FILE, "a") + CFILE_C.write('0\n is now online.\n is now offline.') + CFILE_C.close() + +def conf_line(n): + return((open(CONFIG_FILE).read()).split('\n')[n]) + +THEME = int(conf_line(0)) +JOIN_MESSAGE = conf_line(1) +LEAVE_MESSAGE = conf_line(2) + +class r2chat: + def __init__(self, root): + self.root = root + self.root.title(f"r2chat Legacy :: {username}") + + self.last_size = 0 + + self.chat_area = scrolledtext.ScrolledText(root, wrap='word', font=("Lucida Console", 11), bg=THEMES[THEME][1], fg=THEMES[THEME][3], relief='groove', borderwidth=BORDER_WIDTH) + self.chat_area.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) + self.chat_area.bind("", lambda e: "break") + + self.entry = tk.Entry(root, font=("Lucida Console", 11), bg=THEMES[THEME][2], fg=THEMES[THEME][3], relief='groove', borderwidth=BORDER_WIDTH) + self.entry.pack(padx=10, pady=(0, 10), fill=tk.X) + 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(f"{username}{JOIN_MESSAGE}") + + def on_close(self): + self.write_system_message(f"{username}{LEAVE_MESSAGE}") + self.root.destroy() + + def write_system_message(self, msg): + line = f"---- {msg} ----\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}] {username} (legacy): {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 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') + self.chat_area.insert(tk.END, 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) + + +if __name__ == "__main__": + root = tk.Tk() + app = r2chat(root) + root.geometry("500x520") + root.minsize(360, 520) + root.attributes('-topmost', True) + root.protocol("WM_DELETE_WINDOW", app.on_close) + root.configure(bg=THEMES[THEME][0]) + root.mainloop()