CloudBot icon indicating copy to clipboard operation
CloudBot copied to clipboard

add functions to colors.py like bold(), color(), italic()

Open ctian1 opened this issue 9 years ago • 16 comments

I seem to be really attached to Willie.. I think it could be a good idea to have the formatting module Willie has:

# coding=utf8
"""*Availability: 4.5+*

The formatting module includes functions to apply IRC formatting to text."""
# Copyright 2014, Edward D. Powell, embolalia.net
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals
import sys
if sys.version_info.major >= 3:
    unicode = str

# Color names are as specified at http://www.mirc.com/colors.html

CONTROL_NORMAL = '\x0f'
"""The control code to reset formatting"""
CONTROL_COLOR = '\x03'
"""The control code to start or end color formatting"""
CONTROL_UNDERLINE = '\x1f'
"""The control code to start or end underlining"""
CONTROL_BOLD = '\x02'
"""The control code to start or end bold formatting"""
CONTROL_ITALIC = '\x1D'
"""The control code to start or end italic formatting"""


# TODO when we can move to 3.3+ completely, make this an Enum.
class colors:
    WHITE = '00'
    BLACK = '01'
    BLUE = '02'
    NAVY = BLUE
    GREEN = '03'
    RED = '04'
    BROWN = '05'
    MAROON = BROWN
    PURPLE = '06'
    ORANGE = '07'
    OLIVE = ORANGE
    YELLOW = '08'
    LIGHT_GREEN = '09'
    LIME = LIGHT_GREEN
    TEAL = '10'
    LIGHT_CYAN = '11'
    CYAN = LIGHT_CYAN
    LIGHT_BLUE = '12'
    ROYAL = LIGHT_BLUE
    PINK = '13'
    LIGHT_PURPLE = PINK
    FUCHSIA = PINK
    #TODO GREY and/or GRAY?
    GREY = '14'
    LIGHT_GREY = '15'
    SILVER = LIGHT_GREY


def _get_color(color):
    if color is None:
        return None

    # You can pass an int or string of the code
    try:
        color = int(color)
    except ValueError:
        pass
    if isinstance(color, int):
        if color > 99:
            raise ValueError('Can not specify a color above 99.')
        return unicode(color).rjust(2, '0')

    # You can also pass the name of the color
    color_name = color.upper()
    color_dict = colors.__dict__
    try:
        return color_dict[color_name]
    except KeyError:
        raise ValueError('Unknown color name {}'.format(color))


def color(text, fg=None, bg=None):
    """Return the text, with the given colors applied in IRC formatting.

    The color can be a string of the color name, or an integer between 0 and
    99. The known color names can be found in the `colors` class of this
    module."""
    if not fg and not bg:
        return text

    fg = _get_color(fg)
    bg = _get_color(bg)

    if not bg:
        text = ''.join([CONTROL_COLOR, fg, text, CONTROL_COLOR])
    else:
        text = ''.join([CONTROL_COLOR, fg, ',', bg, text, CONTROL_COLOR])
    return text


def bold(text):
    """Return the text, with bold IRC formatting."""
    return ''.join([CONTROL_BOLD, text, CONTROL_BOLD])


def underline(text):
    """Return the text, with underline IRC formatting."""
    return ''.join([CONTROL_UNDERLINE, text, CONTROL_UNDERLINE])


def italic(text):
    """Return the text, with italic IRC formatting."""
    return ''.join([CONTROL_ITALIC, text, CONTROL_ITALIC])

ctian1 avatar Jan 15 '15 03:01 ctian1

The new colors.py util module can do this, and more. It's not the exact same kind of syntax, though.

from cloudbot import hook
from cloudbot.util import colors

@hook.command()
def my_command():
    string = "$(bold)This is bold$(clear) but this is not, however $(bold,red)this is bold and red!$(clear)"
    returns colors.parse(string)

dmptrluke avatar Jan 15 '15 03:01 dmptrluke

I've seen that, but IMHO it seems kind of hard to use in some situations

it can be useful when building strings, but what if:

def command(text, reply):
    msg = "$(bold)Adding option '$(clear){}$(bold)' to database."
    reply(colors.parse(msg))

Now, if a user were to add an option such as '$(red)asdf', they could wreck the colors and stuff.

I'm just saying, I thought it was handy to able to do things like reply(bold("[Settings] ") + italic(input) + color(" has now been changed to 0", 4))

ctian1 avatar Jan 15 '15 03:01 ctian1

Well, in that specific situation you can use $(bold) again to toggle it off (mirc colors are weird), but I get your feedback. Perhaps I can add some manual functions to colors.py like bold(str) and color(str, colorname).

edit: Ah, yes, sorry. I didn't get your post at first, yeah, users can inject colors. But you can simply run colors.strip(str) on the input to remove all color formatting from it if needed, but it might not be ideal.

dmptrluke avatar Jan 15 '15 03:01 dmptrluke

Also, you can get the color formatting codes with colors.get_color("red") or colors.get_formatting("bold")

I can still add the manual functions if you can think of a good use-case for them though.

dmptrluke avatar Jan 15 '15 04:01 dmptrluke

I use those functions a lot;

for example, in my minecraft server status script for willie, I did this somewhere;

msg = "{0} is " + bold("up") + " with {1} players and a latency of {2}ms, running {3}, and has an MOTD of \"{4}\"."
bot.say(msg.format(bold(trigger.group(2).split()[0]), players, latency, version, colorize(motd)))

doing that without bold() and stuff would probably need..

msg = colors.parse("{0} is $(bold)up$(clear) with {1} players and a latency of {2} ms, running {3}, and has an MOTD of \"{4}\".")
bot.say(msg.format(color.get_formatting("bold") + trigger.group(2).split()[0] + color.get_formatting("bold"), players, latency, version, colorize(motd)))

which isn't too bad, but particularly bold(test) vs. color.get_formatting("bold") + text + color.get_formatting("bold") seems particularly painful, and similar with italic and underline.

Edit: now that I look at it, the top example is pretty bad. lemme find a better one

ctian1 avatar Jan 15 '15 04:01 ctian1

ehhh

dmptrluke avatar Jan 15 '15 04:01 dmptrluke

Better example:

in another part of my minecraft script, querying users returns a message like

$(bold)[Pangea/956e407b-d332-4df6-b0b7-08873fa7446d]: PAID: $(green)true $(clear)$(bold)| MIGRATED: $(clear)$(green)true$(clear)$(bold) | SKIN: $(clear)http://textures.minecraft.net/texture/3b42c78aa592a3110f02b23c76f325b9d4cc8ab72523884e73fc0c0649758

which should turn out like (but with colors)

[Pangea/956e407b-d332-4df6-b0b7-08873fa7446d]: PAID: true | MIGRATED: true | SKIN: http://textures.minecraft.net/texture/3b42c78aa592a3110f02b23c76f325b9d4cc8ab72523884e73fc0c0649758

the code for this was:

query = str(bold("[" + user + "/" + str(uuid.UUID(id)) + "]") + ": " + bold("PAID:") + " {0} | " + bold("MIGRATED:") + " {1}").format(paid, migrated)

hm... with colors.py..

query = str(color.get_formatting("bold") + "[" + user + "/" + str(uuid.UUID(id)) + "]" + color.get_formatting("clear") + ": " + color.get_formatting("bold") + "PAID: " + color.get_formatting("clear") and much more I don't even want to finish right now ;-; it's that painful

ctian1 avatar Jan 15 '15 04:01 ctian1

  • [ ] underline(text)

    should also be added

ctian1 avatar Jan 15 '15 04:01 ctian1

Or you could use colors.parse(), and the first example is hardly a fair comparison, it adds colors when example 2 doesn't have them! But I'll add the functions you want, but colors are going to be complex to get working in an entirely logical way - MIRC colors are not something you can just turn on and off like an HTML tag, they can only be cleared with RESET, which also clears bold and other formatting codes.

So a function-based format for colors wont really work how you would expect.

dmptrluke avatar Jan 15 '15 04:01 dmptrluke

Hmm.. I guess you have a point say I have a template like $(green)asdf$(clear),04asdf this would show the first 'asdf' as green, then ',04asdf' as default color. with colorfunctions, you'd do: color("asdf", 3) + ",04asdf" but then this breaks because since it doesn't use the actual color reset ($(clear)) and inserts another color code symbol, it interprets the reset as the start of a new color section.

cloudbot version: (where [c] is color code, [r] is reset) [c]3asdf[r],04asdf functions: [c]3asdf[c],04asdf -------------^ this is interpreted as a color code

so I'm not so sure anymore.

the bold/underline/italic functions should work perfectly because the control characters for those can start and end, which won't affect colors.

colors can also be ended by the color control code, but again if the text after that begins with some numbers it can unintentionally start a new color section.

hopefully all of this wasn't too confusing

any ideas for a better solution to this?

ctian1 avatar Jan 15 '15 04:01 ctian1

I wasn't sure if colors ended with the color control code or not, but if they do, it makes things simplier. As for the issue or ,04asdf, it seems like something that will very rarely happen, and I'm not sure theres really anything we can do about it. Perhaps we can include a invisible/null character just after the color codes of color() to fix it? I'm not sure what character would be suitable, but I'm sure theres one we can use.

dmptrluke avatar Jan 15 '15 04:01 dmptrluke

With the injecting colors, wouldn't that be fixed if you ran the format string through colors.parse before filling in with user-provided values?

daboross avatar Jan 15 '15 05:01 daboross

My utility was mostly meant to add added readability for the programmer. \x0306 doesn't directly help you notice it is actually purple. And calling functions also does interfere with readability.

It certainly won't hurt to add aliases for things like bold() underline() etc, it is mostly left in the developers hand on how they want to extend on it, mine is just a base implementation.

Also, keep in mind you can chain formattings and colours in parse(), i.e.

mystr = color.parse("my string $(red, blue, bold)stuff$(clear) more string")

whereas

mystr = "my string" + color.bold(color.color("stuff", color.RED, color.BLUE)) + "more string"

just doesn't read very nicely (at least in my opinion).

Whatever the case, get_formatting() and get_color() are not functions that were intended to be called directly, but they do exist so that others can extend on it.

Zarthus avatar Jan 15 '15 22:01 Zarthus

This probably works :P

from cloudbot import hook
from cloudbot.util import colors

@hook.command()
def my_command(text):
    # remove all colors from user input
    text = colors.strip_all(text)
    # build the output
    string = "$(bold)User Input:$(clear) {}".format(text)
    returns colors.parse(string)

This has the advantage of stripping all MIRC and CloudBot formatting from the input string.

dmptrluke avatar Jan 15 '15 22:01 dmptrluke

What if there's some odd chance someone needs to use $(something like this) for something not color involved? Won't those be broken?

Also, I'm more worried about making dynamic colors, like:

if user.paid:
    paid = color("true", GREEN)
else:
    paid = color("false", RED)

With .parse:

if user.paid:
    paid = "$(green){}$(clear)".format("true")
else:
    paid = "$(red){}$(clear)".format("false")

I suppose one hackery way we can solve this is adding two bold characters after a color code used to reset colors, which will ensure that there isn't another numeric color value after the color symbol. But that seems too hackery.

ctian1 avatar Jan 16 '15 00:01 ctian1

Like said on IRC what the code currently does doesn't support escaping of yet, but could probably be implemented easily. It wasn't added yet because it wasn't a priority feature for me.

What do you mean with dynamic colours? I don't think that'll be a problem at all.

paid = color("true" if user.paid else "false", "green" if user.paid else "red")

paid = "$({}){}$(clear)".format("green" if user.paid else "red", "true" if user.paid else "false")

paid = getColor("green" if user.paid else "red") + "{}$(clear)".format("true" if user.paid else "false")

The code would look the same in Willie, no?

Zarthus avatar Jan 16 '15 08:01 Zarthus