diff options
author | Patrick Griffis <tingping@tingping.se> | 2017-09-02 17:52:25 -0400 |
---|---|---|
committer | Patrick Griffis <tingping@tingping.se> | 2018-11-09 18:36:59 -0500 |
commit | 706f9bca82d463f6f1bd17d5dc609807e4a1e8a9 (patch) | |
tree | 09f849421105e4202b436af97a94d777c96aa378 /plugins/python/_hexchat.py | |
parent | 643269445530edd0ee6a308f771767ec3f9a1919 (diff) |
python: Rewrite with CFFI
Diffstat (limited to 'plugins/python/_hexchat.py')
-rw-r--r-- | plugins/python/_hexchat.py | 364 |
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) |