Compare commits

..

No commits in common. "c8b8870139890e0d7b296cd5e2e3836f13b9d6b2" and "cef9ceb5e535a810df5d8f6227c8cbca7ecd5d09" have entirely different histories.

665
r2chat.py
View file

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