Compare commits
2 commits
cef9ceb5e5
...
c8b8870139
| Author | SHA1 | Date | |
|---|---|---|---|
| c8b8870139 | |||
| b6501b9719 |
1 changed files with 342 additions and 323 deletions
665
r2chat.py
665
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("<Key>", lambda e: "break")
|
||||
|
||||
self.entry.bind("<Return>", 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("<Key>", lambda e: "break")
|
||||
|
||||
self.entry.bind("<Return>", 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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue