summary refs log tree commit diff stats
path: root/plugins/python/_hexchat.py
diff options
context:
space:
mode:
authorPatrick Griffis <tingping@tingping.se>2017-09-02 17:52:25 -0400
committerPatrick Griffis <tingping@tingping.se>2018-11-09 18:36:59 -0500
commit706f9bca82d463f6f1bd17d5dc609807e4a1e8a9 (patch)
tree09f849421105e4202b436af97a94d777c96aa378 /plugins/python/_hexchat.py
parent643269445530edd0ee6a308f771767ec3f9a1919 (diff)
python: Rewrite with CFFI
Diffstat (limited to 'plugins/python/_hexchat.py')
-rw-r--r--plugins/python/_hexchat.py364
1 files changed, 364 insertions, 0 deletions
diff --git a/plugins/python/_hexchat.py b/plugins/python/_hexchat.py
new file mode 100644
index 00000000..52b3ec14
--- /dev/null
+++ b/plugins/python/_hexchat.py
@@ -0,0 +1,364 @@
+from contextlib import contextmanager
+import inspect
+import sys
+from _hexchat_embedded import ffi, lib
+
+__all__ = [
+    'EAT_ALL', 'EAT_HEXCHAT', 'EAT_NONE', 'EAT_PLUGIN', 'EAT_XCHAT',
+    'PRI_HIGH', 'PRI_HIGHEST', 'PRI_LOW', 'PRI_LOWEST', 'PRI_NORM',
+    '__doc__', '__version__', 'command', 'del_pluginpref', 'emit_print',
+    'find_context', 'get_context', 'get_info',
+    'get_list', 'get_lists', 'get_pluginpref', 'get_prefs', 'hook_command',
+    'hook_print', 'hook_print_attrs', 'hook_server', 'hook_server_attrs',
+    'hook_timer', 'hook_unload', 'list_pluginpref', 'nickcmp', 'prnt',
+    'set_pluginpref', 'strip', 'unhook',
+]
+
+__doc__ = 'HexChat Scripting Interface'
+__version__ = (2, 0)
+__license__ = 'GPL-2.0+'
+
+EAT_NONE = 0
+EAT_HEXCHAT = 1
+EAT_XCHAT = EAT_HEXCHAT
+EAT_PLUGIN = 2
+EAT_ALL = EAT_HEXCHAT | EAT_PLUGIN
+
+PRI_LOWEST = -128
+PRI_LOW = -64
+PRI_NORM = 0
+PRI_HIGH = 64
+PRI_HIGHEST = 127
+
+
+# We need each module to be able to reference their parent plugin
+# which is a bit tricky since they all share the exact same module.
+# Simply navigating up to what module called it seems to actually
+# be a fairly reliable and simple method of doing so if ugly.
+def __get_current_plugin():
+    frame = inspect.stack()[1][0]
+    while '__plugin' not in frame.f_globals:
+        frame = frame.f_back
+        assert frame is not None
+    return frame.f_globals['__plugin']
+
+
+# Keeping API compat
+if sys.version_info[0] is 2:
+    def __decode(string):
+        return string
+else:
+    def __decode(string):
+        return string.decode()
+
+
+# ------------ API ------------
+def prnt(string):
+    lib.hexchat_print(lib.ph, string.encode())
+
+
+def emit_print(event_name, *args, **kwargs):
+    time = kwargs.pop('time', 0)  # For py2 compat
+    cargs = []
+    for i in range(4):
+        arg = args[i].encode() if len(args) > i else b''
+        cstring = ffi.new('char[]', arg)
+        cargs.append(cstring)
+    if time is 0:
+        return lib.hexchat_emit_print(lib.ph, event_name.encode(), *cargs)
+    else:
+        attrs = lib.hexchat_event_attrs_create(lib.ph)
+        attrs.server_time_utc = time
+        ret = lib.hexchat_emit_print(lib.ph, attrs, event_name.encode(), *cargs)
+        lib.hexchat_event_attrs_free(lib.ph, attrs)
+        return ret
+
+
+def command(command):
+    lib.hexchat_command(lib.ph, command.encode())
+
+
+def nickcmp(string1, string2):
+    return lib.hexchat_nickcmp(lib.ph, string1.encode(), string2.encode())
+
+
+def strip(text, length=-1, flags=3):
+    stripped = lib.hexchat_strip(lib.ph, text.encode(), length, flags)
+    ret = __decode(ffi.string(stripped))
+    lib.hexchat_free(lib.ph, stripped)
+    return ret
+
+
+def get_info(name):
+    ret = lib.hexchat_get_info(lib.ph, name.encode())
+    if ret == ffi.NULL:
+        return None
+    if name in ('gtkwin_ptr', 'win_ptr'):
+        # Surely there is a less dumb way?
+        ptr = repr(ret).rsplit(' ', 1)[1][:-1]
+        return ptr
+    return __decode(ffi.string(ret))
+
+
+def get_prefs(name):
+    string_out = ffi.new('char**')
+    int_out = ffi.new('int*')
+    type = lib.hexchat_get_prefs(lib.ph, name.encode(), string_out, int_out)
+    if type is 0:
+        return None
+    elif type is 1:
+        return __decode(ffi.string(string_out[0]))
+    elif type is 2 or type is 3:  # XXX: 3 should be a bool, but keeps API
+        return int_out[0]
+    else:
+        assert False
+
+
+def __cstrarray_to_list(arr):
+    i = 0
+    ret = []
+    while arr[i] != ffi.NULL:
+        ret.append(ffi.string(arr[i]))
+        i += 1
+    return ret
+
+
+__FIELD_CACHE = {}
+
+
+def __get_fields(name):
+    return __FIELD_CACHE.setdefault(name,
+                                    __cstrarray_to_list(lib.hexchat_list_fields(lib.ph, name)))
+
+
+__FIELD_PROPERTY_CACHE = {}
+
+
+def __cached_decoded_str(string):
+    return __FIELD_PROPERTY_CACHE.setdefault(string, __decode(string))
+
+
+def get_lists():
+    return [__cached_decoded_str(field) for field in __get_fields(b'lists')]
+
+
+class ListItem:
+    def __init__(self, name):
+        self._listname = name
+
+    def __repr__(self):
+        return '<{} list item at {}>'.format(self._listname, id(self))
+
+
+def get_list(name):
+    # XXX: This function is extremely inefficient and could be interators and
+    # lazily loaded properties, but for API compat we stay slow
+    orig_name = name
+    name = name.encode()
+
+    if name not in __get_fields(b'lists'):
+        raise KeyError('list not available')
+
+    list_ = lib.hexchat_list_get(lib.ph, name)
+    if list_ == ffi.NULL:
+        return None
+
+    ret = []
+    fields = __get_fields(name)
+
+    def string_getter(field):
+        string = lib.hexchat_list_str(lib.ph, list_, field)
+        if string != ffi.NULL:
+            return __decode(ffi.string(string))
+        return ''
+
+    def ptr_getter(field):
+        if field == b'context':
+            ptr = lib.hexchat_list_str(lib.ph, list_, field)
+            ctx = ffi.cast('hexchat_context*', ptr)
+            return Context(ctx)
+        return None
+
+    getters = {
+        ord('s'): string_getter,
+        ord('i'): lambda field: lib.hexchat_list_int(lib.ph, list_, field),
+        ord('t'): lambda field: lib.hexchat_list_time(lib.ph, list_, field),
+        ord('p'): ptr_getter,
+    }
+
+    while lib.hexchat_list_next(lib.ph, list_) is 1:
+        item = ListItem(orig_name)
+        for field in fields:
+            getter = getters.get(ord(field[0]))
+            if getter is not None:
+                field_name = field[1:]
+                setattr(item, __cached_decoded_str(field_name), getter(field_name))
+        ret.append(item)
+
+    lib.hexchat_list_free(lib.ph, list_)
+    return ret
+
+
+def hook_command(command, callback, userdata=None, priority=PRI_NORM, help=None):
+    plugin = __get_current_plugin()
+    hook = plugin.add_hook(callback, userdata)
+    handle = lib.hexchat_hook_command(lib.ph, command.encode(), priority, lib._on_command_hook,
+                                      help.encode() if help is not None else ffi.NULL,
+                                      hook.handle)
+    hook.hexchat_hook = handle
+    return id(hook)
+
+
+def hook_print(name, callback, userdata=None, priority=PRI_NORM):
+    plugin = __get_current_plugin()
+    hook = plugin.add_hook(callback, userdata)
+    handle = lib.hexchat_hook_print(lib.ph, name.encode(), priority, lib._on_print_hook,
+                                    hook.handle)
+    hook.hexchat_hook = handle
+    return id(hook)
+
+
+def hook_print_attrs(name, callback, userdata=None, priority=PRI_NORM):
+    plugin = __get_current_plugin()
+    hook = plugin.add_hook(callback, userdata)
+    handle = lib.hexchat_hook_print_attrs(lib.ph, name.encode(), priority, lib._on_print_attrs_hook,
+                                          hook.handle)
+    hook.hexchat_hook = handle
+    return id(hook)
+
+
+def hook_server(name, callback, userdata=None, priority=PRI_NORM):
+    plugin = __get_current_plugin()
+    hook = plugin.add_hook(callback, userdata)
+    handle = lib.hexchat_hook_server(lib.ph, name.encode(), priority, lib._on_server_hook,
+                                     hook.handle)
+    hook.hexchat_hook = handle
+    return id(hook)
+
+
+def hook_server_attrs(name, callback, userdata=None, priority=PRI_NORM):
+    plugin = __get_current_plugin()
+    hook = plugin.add_hook(callback, userdata)
+    handle = lib.hexchat_hook_server_attrs(lib.ph, name.encode(), priority, lib._on_server_attrs_hook,
+                                           hook.handle)
+    hook.hexchat_hook = handle
+    return id(hook)
+
+
+def hook_timer(timeout, callback, userdata=None):
+    plugin = __get_current_plugin()
+    hook = plugin.add_hook(callback, userdata)
+    handle = lib.hexchat_hook_timer(lib.ph, timeout, lib._on_timer_hook, hook.handle)
+    hook.hexchat_hook = handle
+    return id(hook)
+
+
+def hook_unload(callback, userdata=None):
+    plugin = __get_current_plugin()
+    hook = plugin.add_hook(callback, userdata, is_unload=True)
+    return id(hook)
+
+
+def unhook(handle):
+    plugin = __get_current_plugin()
+    return plugin.remove_hook(handle)
+
+
+def set_pluginpref(name, value):
+    if isinstance(value, str):
+        return bool(lib.hexchat_pluginpref_set_str(lib.ph, name.encode(), value.encode()))
+    elif isinstance(value, int):
+        return bool(lib.hexchat_pluginpref_set_int(lib.ph, name.encode(), value))
+    else:
+        # XXX: This should probably raise but this keeps API
+        return False
+
+
+def get_pluginpref(name):
+    name = name.encode()
+    string_out = ffi.new('char[512]')
+    if lib.hexchat_pluginpref_get_str(lib.ph, name, string_out) is not 1:
+        return None
+
+    string = ffi.string(string_out)
+    # This API stores everything as a string so we have to figure out what
+    # its actual type was supposed to be.
+    if len(string) > 12:  # Can't be a number
+        return __decode(string)
+
+    number = lib.hexchat_pluginpref_get_int(lib.ph, name)
+    if number == -1 and string != b'-1':
+        return __decode(string)
+
+    return number
+
+
+def del_pluginpref(name):
+    return bool(lib.hexchat_pluginpref_delete(lib.ph, name.encode()))
+
+
+def list_pluginpref():
+    prefs_str = ffi.new('char[4096]')
+    if lib.hexchat_pluginpref_list(lib.ph, prefs_str) is 1:
+        return __decode(prefs_str).split(',')
+    return []
+
+
+class Context:
+    def __init__(self, ctx):
+        self._ctx = ctx
+
+    def __eq__(self, value):
+        if not isinstance(value, Context):
+            return False
+        return self._ctx == value._ctx
+
+    @contextmanager
+    def __change_context(self):
+        old_ctx = lib.hexchat_get_context(lib.ph)
+        if not self.set():
+            # XXX: Behavior change, previously used wrong context
+            lib.hexchat_print(lib.ph,
+                              b'Context object refers to closed context, ignoring call')
+            return
+        yield
+        lib.hexchat_set_context(lib.ph, old_ctx)
+
+    def set(self):
+        # XXX: API addition, C plugin silently ignored failure
+        return bool(lib.hexchat_set_context(lib.ph, self._ctx))
+
+    def prnt(self, string):
+        with self.__change_context():
+            prnt(string)
+
+    def emit_print(self, event_name, *args, **kwargs):
+        time = kwargs.pop('time', 0)  # For py2 compat
+        with self.__change_context():
+            return emit_print(event_name, *args, time=time)
+
+    def command(self, string):
+        with self.__change_context():
+            command(string)
+
+    def get_info(self, name):
+        with self.__change_context():
+            return get_info(name)
+
+    def get_list(self, name):
+        with self.__change_context():
+            return get_list(name)
+
+
+def get_context():
+    ctx = lib.hexchat_get_context(lib.ph)
+    return Context(ctx)
+
+
+def find_context(server=None, channel=None):
+    server = server.encode() if server is not None else ffi.NULL
+    channel = channel.encode() if channel is not None else ffi.NULL
+    ctx = lib.hexchat_find_context(lib.ph, server, channel)
+    if ctx == ffi.NULL:
+        return None
+    return Context(ctx)