Customizable asciis
This commit is contained in:
parent
b09e675189
commit
e9349091bd
|
@ -0,0 +1,52 @@
|
||||||
|
{
|
||||||
|
"areas":[
|
||||||
|
{
|
||||||
|
"name":"words",
|
||||||
|
"string":"",
|
||||||
|
"x":18,
|
||||||
|
"y":3,
|
||||||
|
"width":16,
|
||||||
|
"height":5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"name",
|
||||||
|
"string":"code, c",
|
||||||
|
"format": "\"{}\"",
|
||||||
|
"props":[
|
||||||
|
"centered"
|
||||||
|
],
|
||||||
|
"x":3,
|
||||||
|
"y":9,
|
||||||
|
"width":13,
|
||||||
|
"height":1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"title",
|
||||||
|
"string":"LICENSED TO CHAT",
|
||||||
|
"props":[
|
||||||
|
"centered",
|
||||||
|
"upper"
|
||||||
|
],
|
||||||
|
"x":2,
|
||||||
|
"y":1,
|
||||||
|
"width":32,
|
||||||
|
"height":1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"expr",
|
||||||
|
"string":"exp 09-23-2017",
|
||||||
|
"x":19,
|
||||||
|
"y":9,
|
||||||
|
"width":14,
|
||||||
|
"height":1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"expr2",
|
||||||
|
"string":"DSM-IV: 299.80",
|
||||||
|
"x":19,
|
||||||
|
"y":10,
|
||||||
|
"width":14,
|
||||||
|
"height":1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
0,1XxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX
|
||||||
|
0,1x ?x
|
||||||
|
0,1x ??????????????? ?x
|
||||||
|
0,4x4 0? 0,1/0,4`--0,1-0,4'0,1- \0,4 0,1 0,4? 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 ?0,4x
|
||||||
|
0,4x4,1 0?0,4(0,1_0,4 0,1 0,4_0,1 \_0,4\0,1 0,4 0,1? 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 ?0,4x
|
||||||
|
0,4x ? 0,1`0,4o'0,1/ 0,4'0,1o` 0,4&0,1)0,4 0,1? 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1?0,4x
|
||||||
|
0,4x 0,1? 0,4\0,1`- 0,4 0,1 0,4|0,1 0,4 0,1? 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1?0,4x
|
||||||
|
0,4x0,1 0,4?0,1 0,4 0,1\`~'0,4 0,1 0,4/0,1\ 0,4 0,1? 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 ?x
|
||||||
|
0,4x0,1 ?0,4 0,1 0,4 `0,1---0,4'0,1 \_0,4? 0,1 0,4 0,1 0,4 0,1 0,4 0,1 0,4 0,1 ?0,4x
|
||||||
|
0,1x ? ? [exp 09-23-2017] ?x
|
||||||
|
0,1x ??????????????? [DSM-IV: 299.80] ?x
|
||||||
|
0,1XxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX
|
|
@ -5,16 +5,24 @@
|
||||||
:synopsis: Spam chat with awesome ascii texts
|
:synopsis: Spam chat with awesome ascii texts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pyircbot.modulebase import ModuleBase, hook
|
from pyircbot.modulebase import ModuleBase, hook, command
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from glob import iglob
|
from glob import iglob
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from time import sleep
|
from time import sleep
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
|
from textwrap import wrap
|
||||||
|
|
||||||
|
|
||||||
RE_ASCII_FNAME = re.compile(r'^[a-zA-Z0-9\-_]+$')
|
RE_ASCII_FNAME = re.compile(r'^[a-zA-Z0-9\-_]+$')
|
||||||
|
IRC_COLOR = "\x03"
|
||||||
|
|
||||||
|
|
||||||
|
def is_numeric(char):
|
||||||
|
i = ord(char)
|
||||||
|
return i >= 48 and i <= 57
|
||||||
|
|
||||||
|
|
||||||
class ASCII(ModuleBase):
|
class ASCII(ModuleBase):
|
||||||
|
@ -23,18 +31,12 @@ class ASCII(ModuleBase):
|
||||||
self.running_asciis = defaultdict(lambda: None)
|
self.running_asciis = defaultdict(lambda: None)
|
||||||
self.killed_channels = defaultdict(lambda: False)
|
self.killed_channels = defaultdict(lambda: False)
|
||||||
|
|
||||||
@hook("PRIVMSG")
|
# @hook("PRIVMSG")
|
||||||
def listen_msg(self, msg):
|
@command("listascii")
|
||||||
|
def cmd_listascii(self, cmd, msg):
|
||||||
"""
|
"""
|
||||||
Handle commands
|
List available asciis
|
||||||
:param msg: Message object to inspect
|
|
||||||
"""
|
"""
|
||||||
# Ignore PMs
|
|
||||||
if not msg.args[0].startswith("#"):
|
|
||||||
return
|
|
||||||
|
|
||||||
# provide a listing of available asciis
|
|
||||||
if self.bot.messageHasCommand(".listascii", msg.trailing):
|
|
||||||
fnames = [os.path.basename(f).split(".", 2)[0] for f in iglob(os.path.join(self.getFilePath(), "*.txt"))]
|
fnames = [os.path.basename(f).split(".", 2)[0] for f in iglob(os.path.join(self.getFilePath(), "*.txt"))]
|
||||||
fnames.sort()
|
fnames.sort()
|
||||||
|
|
||||||
|
@ -45,11 +47,12 @@ class ASCII(ModuleBase):
|
||||||
self.bot.act_PRIVMSG(msg.args[0], "...and {} more".format(len(fnames) - self.config.get("list_max")))
|
self.bot.act_PRIVMSG(msg.args[0], "...and {} more".format(len(fnames) - self.config.get("list_max")))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@command("ascii", require_args=True)
|
||||||
|
def cmd_ascii(self, cmd, msg):
|
||||||
|
# import ipdb
|
||||||
|
# ipdb.set_trace()
|
||||||
# Send out an ascii
|
# Send out an ascii
|
||||||
cmd = self.bot.messageHasCommand(".ascii", msg.trailing, requireArgs=True)
|
if self.channel_busy(msg.args[0]):
|
||||||
if self.bot.messageHasCommand(".ascii", msg.trailing):
|
|
||||||
# Prevent parallel spamming in same channel
|
|
||||||
if self.running_asciis[msg.args[0]]:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Prevent parallel spamming in different channels
|
# Prevent parallel spamming in different channels
|
||||||
|
@ -60,38 +63,189 @@ class ASCII(ModuleBase):
|
||||||
if not RE_ASCII_FNAME.match(ascii_name):
|
if not RE_ASCII_FNAME.match(ascii_name):
|
||||||
return
|
return
|
||||||
|
|
||||||
ascii_path = self.getFilePath(ascii_name + ".txt")
|
hilight = cmd.args[0] + ": " if self.config.get("allow_hilight", False) and len(cmd.args) >= 1 else False
|
||||||
if os.path.exists(ascii_path):
|
try:
|
||||||
args = [ascii_path, msg.args[0]]
|
self.send_to_channel(msg.args[0], self.load_ascii(ascii_name), prefix=hilight)
|
||||||
if self.config.get("allow_hilight", False) and len(cmd.args) >= 1:
|
except FileNotFoundError:
|
||||||
args.append(cmd.args.pop(0))
|
|
||||||
self.running_asciis[msg.args[0]] = Thread(target=self.print_ascii, args=args, daemon=True)
|
|
||||||
self.running_asciis[msg.args[0]].start()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# stop running asciis
|
@command("stopascii")
|
||||||
if self.bot.messageHasCommand(".stopascii", msg.trailing):
|
def cmd_stopascii(self, cmd, msg):
|
||||||
|
"""
|
||||||
|
Command to stop the running ascii in a given channel
|
||||||
|
"""
|
||||||
if self.running_asciis[msg.args[0]]:
|
if self.running_asciis[msg.args[0]]:
|
||||||
self.killed_channels[msg.args[0]] = True
|
self.killed_channels[msg.args[0]] = True
|
||||||
|
|
||||||
def print_ascii(self, ascii_path, channel, hilight=None):
|
def channel_busy(self, channel_name):
|
||||||
|
"""
|
||||||
|
Prevent parallel spamming in same channel
|
||||||
|
"""
|
||||||
|
if self.running_asciis[channel_name]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_to_channel(self, channel, lines, **kwargs):
|
||||||
|
self.running_asciis[channel] = Thread(target=self.print_lines, daemon=True,
|
||||||
|
args=[channel, lines], kwargs=kwargs)
|
||||||
|
self.running_asciis[channel].start()
|
||||||
|
|
||||||
|
def load_ascii(self, ascii_name):
|
||||||
|
"""
|
||||||
|
Loads contents of ascii from disk by name
|
||||||
|
:return: list of string lines
|
||||||
|
"""
|
||||||
|
with open(self.getFilePath(ascii_name + ".txt"), "rb") as f:
|
||||||
|
return [i.rstrip() for i in f.read().decode("UTF-8", errors="ignore").split("\n")]
|
||||||
|
|
||||||
|
def print_lines(self, channel, lines, prefix=None):
|
||||||
"""
|
"""
|
||||||
Print the contents of ascii_path to channel
|
Print the contents of ascii_path to channel
|
||||||
:param ascii_path: file path to the ascii art file to read and print
|
:param ascii_path: file path to the ascii art file to read and print
|
||||||
:param channel: channel name to print to
|
:param channel: channel name to print to
|
||||||
"""
|
"""
|
||||||
delay = self.config.get("line_delay")
|
delay = self.config.get("line_delay")
|
||||||
with open(ascii_path, "rb") as f:
|
|
||||||
content = [i.rstrip() for i in f.read().decode("UTF-8", errors="ignore").split("\n")]
|
for line in lines:
|
||||||
for line in content:
|
|
||||||
if self.killed_channels[channel]:
|
if self.killed_channels[channel]:
|
||||||
break
|
break
|
||||||
if not line:
|
if not line:
|
||||||
line = " "
|
line = " "
|
||||||
if hilight:
|
if prefix:
|
||||||
line = "{}: {}".format(hilight, line)
|
line = "{}{}".format(prefix, line)
|
||||||
self.bot.act_PRIVMSG(channel, line)
|
self.bot.act_PRIVMSG(channel, line)
|
||||||
if delay:
|
if delay:
|
||||||
sleep(delay)
|
sleep(delay)
|
||||||
del self.running_asciis[channel]
|
del self.running_asciis[channel]
|
||||||
del self.killed_channels[channel]
|
del self.killed_channels[channel]
|
||||||
|
|
||||||
|
@command("asciiedit", require_args=True)
|
||||||
|
def cmd_asciiedit(self, cmd, msg):
|
||||||
|
ascii_name = cmd.args.pop(0)
|
||||||
|
try:
|
||||||
|
with open(self.getFilePath(ascii_name + ".json")) as f:
|
||||||
|
ascii_infos = json.load(f)
|
||||||
|
ascii_lines = self.load_ascii(ascii_name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
|
|
||||||
|
template_data = " ".join(cmd.args).split("|")
|
||||||
|
|
||||||
|
for i in range(0, min(len(ascii_infos["areas"]), len(template_data))):
|
||||||
|
ascii_infos["areas"][i]["string"] = template_data[i]
|
||||||
|
|
||||||
|
lines = self.edit_ascii(ascii_lines, ascii_infos)
|
||||||
|
self.send_to_channel(msg.args[0], lines)
|
||||||
|
|
||||||
|
def edit_ascii(self, lines, info_dict):
|
||||||
|
asci = AsciiEdit(lines)
|
||||||
|
# Per region in the metadata
|
||||||
|
for area in info_dict["areas"]:
|
||||||
|
message = area.get("format", "{}").format(area["string"])
|
||||||
|
x = area["x"]
|
||||||
|
y = area["y"]
|
||||||
|
# Format the text to the area's width
|
||||||
|
boxed_lines = wrap(message, area["width"])
|
||||||
|
# Write each line
|
||||||
|
for line in boxed_lines:
|
||||||
|
line_x = x
|
||||||
|
props = area.get("props", [])
|
||||||
|
# move the X over for centered text
|
||||||
|
if "centered" in props:
|
||||||
|
padding = (area["width"] - len(line)) / 2
|
||||||
|
line_x += padding
|
||||||
|
if "upper" in props:
|
||||||
|
line = line.upper()
|
||||||
|
if "lower" in props:
|
||||||
|
line = line.lower()
|
||||||
|
if "blacktext" in props:
|
||||||
|
asci.set_charcolor(1)
|
||||||
|
# Ensure anything in the template will be covered
|
||||||
|
# line = line + " " * (area["width"] - len(line))
|
||||||
|
# Write the formatted line
|
||||||
|
asci.goto(line_x, y)
|
||||||
|
asci.write(line)
|
||||||
|
# Check if height limit is passed
|
||||||
|
y += 1
|
||||||
|
if y >= area["y"] + area["height"]:
|
||||||
|
break
|
||||||
|
return asci.getlines()
|
||||||
|
|
||||||
|
|
||||||
|
class AsciiEdit(object):
|
||||||
|
"""
|
||||||
|
Semi-smart ascii editing library
|
||||||
|
"""
|
||||||
|
def __init__(self, lines):
|
||||||
|
self.lines = [list(line) for line in lines]
|
||||||
|
self.x = 0
|
||||||
|
self.virtual_x = 0
|
||||||
|
self.y = 0
|
||||||
|
self.charcolor = None
|
||||||
|
|
||||||
|
def goto(self, x, y):
|
||||||
|
"""
|
||||||
|
This method moves the internal pointer to the passed X and Y coordinate. The origin is the top left corner.
|
||||||
|
The literal coordinates (self.x and self.y) point to where in the data the pointer is. Y is simple, self.y is
|
||||||
|
always the data and rendered Y coordinate. X has invisible-when-rendered formatting data, so the self.virtual_x
|
||||||
|
attribute tracks where the cursor is in the rendered output; self.x tracks the location in the actual data.
|
||||||
|
:param x: x coordinate
|
||||||
|
:type x: int
|
||||||
|
:param y: y coordinate
|
||||||
|
:type y: int
|
||||||
|
"""
|
||||||
|
# Immediately set the x and y pointers
|
||||||
|
self.virtual_x = x
|
||||||
|
self.y = y
|
||||||
|
# Calculate the x position in-data by increasing self.x to the right until real_x == target x
|
||||||
|
self.x = 0
|
||||||
|
real_x = 0
|
||||||
|
while real_x <= x:
|
||||||
|
# Scan throw any repeated color codes
|
||||||
|
while self.lines[self.y][self.x] == IRC_COLOR:
|
||||||
|
# Scan forward until color code is crossed
|
||||||
|
self.x += 1 # the escape char
|
||||||
|
if is_numeric(self._getchar()):
|
||||||
|
self.x += 1 # color code 1st digit
|
||||||
|
if is_numeric(self._getchar()):
|
||||||
|
self.x += 1 # color code 2nd digit
|
||||||
|
# Check for background color code after comma
|
||||||
|
if self._getchar() == "," and is_numeric(self._getchar(self.x + 1)):
|
||||||
|
self.x += 2 # comma and background code digit 1
|
||||||
|
if is_numeric(self._getchar()):
|
||||||
|
self.x += 1 # color code digit 2
|
||||||
|
real_x += 1
|
||||||
|
if real_x <= x:
|
||||||
|
self.x += 1
|
||||||
|
|
||||||
|
def set_charcolor(self, color):
|
||||||
|
self.charcolor = color
|
||||||
|
|
||||||
|
def write(self, text):
|
||||||
|
"""
|
||||||
|
Write a single line of text to the ascii
|
||||||
|
"""
|
||||||
|
for char in text:
|
||||||
|
self._putchar(self.x, self.y, char)
|
||||||
|
self.goto(self.virtual_x + 1, self.y)
|
||||||
|
|
||||||
|
def getlines(self):
|
||||||
|
"""
|
||||||
|
Return the rendered ascii, formatted as a list of string lines
|
||||||
|
"""
|
||||||
|
return [''.join(line) for line in self.lines]
|
||||||
|
|
||||||
|
def _getchar(self, x=None, y=None):
|
||||||
|
if x is None:
|
||||||
|
x = self.x
|
||||||
|
if y is None:
|
||||||
|
y = self.y
|
||||||
|
return self.lines[y][x]
|
||||||
|
|
||||||
|
def _putchar(self, x, y, char):
|
||||||
|
if self.charcolor is not None and char != " ":
|
||||||
|
self.lines[y].pop(x)
|
||||||
|
for char in list((IRC_COLOR + str(self.charcolor) + char)[::-1]):
|
||||||
|
self.lines[y].insert(x, char)
|
||||||
|
else:
|
||||||
|
self.lines[y][x] = char
|
||||||
|
|
Loading…
Reference in New Issue