diff --git a/contrib/python/elinks_maint.py b/contrib/python/elinks_maint.py new file mode 100644 index 00000000..b6689779 --- /dev/null +++ b/contrib/python/elinks_maint.py @@ -0,0 +1,129 @@ +"""Additional Python code for ELinks maintainers. + +This module is intended for ELinks maintainers. If you modify or add to +the Python APIs in src/scripting/python/*.c and/or contrib/python/hooks.py, +you should update the accompanying docstrings to reflect your changes and +then generate a new version of the file doc/python.txt (which serves as a +reference manual for the browser's Python APIs). The embedded interpreter +can use introspection to regenerate the python.txt document for you; just +copy this file into your ~/.elinks directory and add something like the +following to ~/.elinks/hooks.py: + +import elinks_maint +elinks.bind_key('F2', elinks_maint.generate_python_txt) + +""" + +import inspect +import tempfile +import types + +preface = """\ +Python programmers can customize the behavior of ELinks by creating a Python +hooks module. The embedded Python interpreter provides an internal module +called elinks that can be used by the hooks module to create keystroke +bindings for Python code, obtain information about the document being +viewed, display simple dialog boxes and menus, load documents into the +ELinks cache, or display documents to the user. These two modules are +described below. + +""" + +module_template = """ +MODULE + %s - %s + +DESCRIPTION +%s + +FUNCTIONS +%s +""" + +separator = '-' * 78 + '\n' + +def document_modules(*modules): + """Format the internal documentation found in one or more Python modules.""" + output = [] + for module in modules: + name, doc, namespace = module.__name__, module.__doc__, module.__dict__ + if not name or not namespace: + continue + try: + summary, junk, description = doc.rstrip().split('\n', 2) + except: + summary, description = '?', '(no description available)' + functions = document_functions(namespace) + output.append(module_template % (name, summary, indent(description), + indent(functions))) + return separator.join(output) + +def document_functions(namespace): + """Format the internal documentation for all functions in a namespace.""" + objects = namespace.items() + objects.sort() + output = [] + for name, object in objects: + if name.startswith('_'): + continue + species = type(object) + if species == types.BuiltinFunctionType: + args = '(...)' + elif species == types.FunctionType: + args = inspect.formatargspec(*inspect.getargspec(object)) + else: + continue + description = inspect.getdoc(object) + output.append('%s%s\n%s\n' % (name, args, indent(description))) + return '\n'.join(output) + +def generate_python_txt(): + """Generate documentation for the hooks and elinks modules.""" + import elinks + import hooks + + # Remove anything that doesn't belong in the API documentation. + # + hooks_api_functions = ( + 'follow_url_hook', + 'goto_url_hook', + 'pre_format_html_hook', + 'proxy_for_hook', + 'quit_hook', + ) + for key in hooks.__dict__.keys(): + if key not in hooks_api_functions and not key.startswith('_'): + del hooks.__dict__[key] + hooks.__doc__ = hooks.__doc__.replace('Example Python', 'Python') + + # Generate the documentation. + # + try: + output = separator.join((preface, document_modules(hooks, elinks))) + finally: + # Restore the hooks module to a sane state. + reload(hooks) + + # View the documentation. + # + path = write_tempfile(output) + elinks.open(path) + +def indent(text): + """Return indented text.""" + indent = ' ' * 4 + return '\n'.join([indent + line for line in text.split('\n')]) + +def write_tempfile(text): + """Write a string to a temporary file and return the file's name.""" + output = tempfile.NamedTemporaryFile(prefix='elinks_maint', suffix='.txt') + output.write(text) + output.flush() + _tempfiles[text] = output + return output.name + +# Temp files are stashed in this dictionary to prevent them from being closed +# before ELinks has a chance to read them; they will be automatically deleted +# when the dictionary is garbage-collected at exit time. +# +_tempfiles = {} diff --git a/src/scripting/python/dialogs.c b/src/scripting/python/dialogs.c new file mode 100644 index 00000000..009ede76 --- /dev/null +++ b/src/scripting/python/dialogs.c @@ -0,0 +1,248 @@ +/* Dialog boxes for Python. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "elinks.h" + +#include "bfu/inpfield.h" +#include "bfu/msgbox.h" +#include "intl/gettext/libintl.h" +#include "scripting/python/core.h" +#include "session/session.h" +#include "util/error.h" +#include "util/memlist.h" +#include "util/memory.h" +#include "util/string.h" + +/* Python interface for displaying information to the user. */ + +static char python_info_box_doc[] = +PYTHON_DOCSTRING("info_box(text[, title]) -> None\n\ +\n\ +Display information to the user in a dialog box.\n\ +\n\ +Arguments:\n\ +\n\ +text -- The text to be displayed in the dialog box. This argument can\n\ + be a string or any object that has a string representation as\n\ + returned by str(object).\n\ +\n\ +Optional arguments:\n\ +\n\ +title -- A string containing a title for the dialog box. By default\n\ + the string \"Info\" is used.\n"); + +static PyObject * +python_info_box(PyObject *self, PyObject *args, PyObject *kwargs) +{ + /* [gettext_accelerator_context(python_info_box)] */ + unsigned char *title = N_("Info"); + PyObject *object, *string_object; + unsigned char *text; + static char *kwlist[] = {"text", "title", NULL}; + + if (!python_ses) { + PyErr_SetString(python_elinks_err, "No session"); + return NULL; + } + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|s:info_box", kwlist, + &object, &title)) + return NULL; + + assert(object); + if_assert_failed { + PyErr_SetString(python_elinks_err, N_("Internal error")); + return NULL; + } + + /* + * Get a string representation of the object, then copy that string's + * contents. + */ + string_object = PyObject_Str(object); + if (!string_object) return NULL; + text = (unsigned char *) PyString_AS_STRING(string_object); + if (!text) { + Py_DECREF(string_object); + return NULL; + } + text = stracpy(text); + Py_DECREF(string_object); + if (!text) goto mem_error; + + title = stracpy(title); + if (!title) goto free_text; + + (void) msg_box(python_ses->tab->term, getml(title, NULL), + MSGBOX_NO_INTL | MSGBOX_SCROLLABLE | MSGBOX_FREE_TEXT, + title, ALIGN_LEFT, + text, + NULL, 1, + N_("~OK"), NULL, B_ENTER | B_ESC); + + Py_INCREF(Py_None); + return Py_None; + +free_text: + mem_free(text); + +mem_error: + return PyErr_NoMemory(); +} + +struct python_input_callback_hop { + struct session *ses; + PyObject *callback; +}; + +/* + * C wrapper that invokes Python callbacks for input_dialog() OK button. + * + * This is also used indirectly for the Cancel button, with a NULL @text + * argument. See invoke_input_cancel_callback() below. + */ + +static void +invoke_input_ok_callback(void *data, unsigned char *text) +{ + struct python_input_callback_hop *hop = data; + struct session *saved_python_ses = python_ses; + PyObject *result; + + assert(hop && hop->callback); + if_assert_failed return; + + python_ses = hop->ses; + + /* If @text is NULL, the "s" format will create a None reference. */ + result = PyObject_CallFunction(hop->callback, "s", text); + if (result) + Py_DECREF(result); + else + alert_python_error(); + + Py_DECREF(hop->callback); + mem_free(hop); + + python_ses = saved_python_ses; +} + +/* C wrapper that invokes Python callbacks for input_dialog() cancel button. */ + +static void +invoke_input_cancel_callback(void *data) +{ + invoke_input_ok_callback(data, NULL); +} + +/* Python interface for getting input from the user. */ + +static char python_input_box_doc[] = +PYTHON_DOCSTRING( +"input_box(prompt, callback, title=\"User dialog\", initial=\"\") -> None\n\ +\n\ +Display a dialog box to prompt for user input.\n\ +\n\ +Arguments:\n\ +\n\ +prompt -- A string containing a prompt for the dialog box.\n\ +callback -- A callable object to be called after the dialog is\n\ + finished. It will be called with a single argument, which\n\ + will be either a string provided by the user or else None\n\ + if the user canceled the dialog.\n\ +\n\ +Optional keyword arguments:\n\ +\n\ +title -- A string containing a title for the dialog box. By default\n\ + the string \"User dialog\" is used.\n\ +initial -- A string containing an initial value for the text entry\n\ + field. By default the entry field is initially empty.\n"); + +static PyObject * +python_input_box(PyObject *self, PyObject *args, PyObject *kwargs) +{ + unsigned char *prompt; + PyObject *callback; + unsigned char *title = N_("User dialog"); + unsigned char *initial = NULL; + struct python_input_callback_hop *hop; + static char *kwlist[] = {"prompt", "callback", "title", "initial", NULL}; + + if (!python_ses) { + PyErr_SetString(python_elinks_err, "No session"); + return NULL; + } + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|ss:input_box", + kwlist, &prompt, &callback, &title, + &initial)) + return NULL; + + assert(prompt && callback && title); + if_assert_failed { + PyErr_SetString(python_elinks_err, N_("Internal error")); + return NULL; + } + + prompt = stracpy(prompt); + if (!prompt) goto mem_error; + + title = stracpy(title); + if (!title) goto free_prompt; + + if (initial) { + initial = stracpy(initial); + if (!initial) goto free_title; + } + + hop = mem_alloc(sizeof(*hop)); + if (!hop) goto free_initial; + hop->ses = python_ses; + hop->callback = callback; + Py_INCREF(callback); + + input_dialog(python_ses->tab->term, getml(prompt, title, initial, NULL), + title, prompt, + hop, NULL, + MAX_STR_LEN, initial, 0, 0, NULL, + invoke_input_ok_callback, + invoke_input_cancel_callback); + + Py_INCREF(Py_None); + return Py_None; + +free_initial: + mem_free_if(initial); + +free_title: + mem_free(title); + +free_prompt: + mem_free(prompt); + +mem_error: + return PyErr_NoMemory(); +} + +static PyMethodDef dialogs_methods[] = { + {"info_box", (PyCFunction) python_info_box, + METH_VARARGS | METH_KEYWORDS, + python_info_box_doc}, + + {"input_box", (PyCFunction) python_input_box, + METH_VARARGS | METH_KEYWORDS, + python_input_box_doc}, + + {NULL, NULL, 0, NULL} +}; + +int +python_init_dialogs_interface(PyObject *dict, PyObject *name) +{ + return add_python_methods(dict, name, dialogs_methods); +} diff --git a/src/scripting/python/dialogs.h b/src/scripting/python/dialogs.h new file mode 100644 index 00000000..8f203d15 --- /dev/null +++ b/src/scripting/python/dialogs.h @@ -0,0 +1,8 @@ +#ifndef EL__SCRIPTING_PYTHON_DIALOGS_H +#define EL__SCRIPTING_PYTHON_DIALOGS_H + +#include + +int python_init_dialogs_interface(PyObject *dict, PyObject *name); + +#endif diff --git a/src/scripting/python/document.c b/src/scripting/python/document.c new file mode 100644 index 00000000..c7a1399f --- /dev/null +++ b/src/scripting/python/document.c @@ -0,0 +1,144 @@ +/* Information about current document and current link for Python. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "elinks.h" + +#include "cache/cache.h" +#include "scripting/python/core.h" +#include "session/location.h" +#include "session/session.h" + +/* Python interface to get the current document's body. */ + +static char python_current_document_doc[] = +PYTHON_DOCSTRING("current_document() -> string or None\n\ +\n\ +If a document is being viewed, return its body; otherwise return None.\n"); + +static PyObject * +python_current_document(PyObject *self, PyObject *args) +{ + if (python_ses && have_location(python_ses)) { + struct cache_entry *cached = find_in_cache(cur_loc(python_ses)->vs.uri); + struct fragment *f = cached ? cached->frag.next : NULL; + + if (f) return PyString_FromStringAndSize(f->data, f->length); + } + + Py_INCREF(Py_None); + return Py_None; +} + +/* Python interface to get the current document's header. */ + +static char python_current_header_doc[] = +PYTHON_DOCSTRING("current_header() -> string or None\n\ +\n\ +If a document is being viewed and it has a header, return the header;\n\ +otherwise return None.\n"); + +static PyObject * +python_current_header(PyObject *self, PyObject *args) +{ + if (python_ses && have_location(python_ses)) { + struct cache_entry *cached = find_in_cache(cur_loc(python_ses)->vs.uri); + + if (cached && cached->head) + return PyString_FromString(cached->head); + } + + Py_INCREF(Py_None); + return Py_None; +} + +/* Python interface to get the currently-selected link's URL. */ + +static char python_current_link_url_doc[] = +PYTHON_DOCSTRING("current_link_url() -> string or None\n\ +\n\ +If a link is selected, return its URL; otherwise return None.\n"); + +static PyObject * +python_current_link_url(PyObject *self, PyObject *args) +{ + unsigned char url[MAX_STR_LEN]; + + if (python_ses && get_current_link_url(python_ses, url, MAX_STR_LEN)) + return PyString_FromString(url); + + Py_INCREF(Py_None); + return Py_None; +} + +/* Python interface to get the current document's title. */ + +static char python_current_title_doc[] = +PYTHON_DOCSTRING("current_title() -> string or None\n\ +\n\ +If a document is being viewed, return its title; otherwise return None.\n"); + +static PyObject * +python_current_title(PyObject *self, PyObject *args) +{ + unsigned char title[MAX_STR_LEN]; + + if (python_ses && get_current_title(python_ses, title, MAX_STR_LEN)) + return PyString_FromString(title); + + Py_INCREF(Py_None); + return Py_None; +} + +/* Python interface to get the current document's URL. */ + +static char python_current_url_doc[] = +PYTHON_DOCSTRING("current_url() -> string or None\n\ +\n\ +If a document is being viewed, return its URL; otherwise return None.\n"); + +static PyObject * +python_current_url(PyObject *self, PyObject *args) +{ + unsigned char url[MAX_STR_LEN]; + + if (python_ses && get_current_url(python_ses, url, MAX_STR_LEN)) + return PyString_FromString(url); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef document_methods[] = { + {"current_document", python_current_document, + METH_NOARGS, + python_current_document_doc}, + + {"current_header", python_current_header, + METH_NOARGS, + python_current_header_doc}, + + {"current_link_url", python_current_link_url, + METH_NOARGS, + python_current_link_url_doc}, + + {"current_title", python_current_title, + METH_NOARGS, + python_current_title_doc}, + + {"current_url", python_current_url, + METH_NOARGS, + python_current_url_doc}, + + {NULL, NULL, 0, NULL} +}; + +int +python_init_document_interface(PyObject *dict, PyObject *name) +{ + return add_python_methods(dict, name, document_methods); +} diff --git a/src/scripting/python/document.h b/src/scripting/python/document.h new file mode 100644 index 00000000..63915bed --- /dev/null +++ b/src/scripting/python/document.h @@ -0,0 +1,8 @@ +#ifndef EL__SCRIPTING_PYTHON_DOCUMENT_H +#define EL__SCRIPTING_PYTHON_DOCUMENT_H + +#include + +int python_init_document_interface(PyObject *dict, PyObject *name); + +#endif diff --git a/src/scripting/python/keybinding.c b/src/scripting/python/keybinding.c new file mode 100644 index 00000000..2d6668c1 --- /dev/null +++ b/src/scripting/python/keybinding.c @@ -0,0 +1,198 @@ +/* Keystroke bindings for Python. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include +#include + +#include "elinks.h" + +#include "config/kbdbind.h" +#include "intl/gettext/libintl.h" +#include "main/event.h" +#include "scripting/python/core.h" +#include "session/session.h" +#include "util/error.h" +#include "util/string.h" + +static PyObject *keybindings = NULL; + +/* C wrapper that invokes Python callbacks for bind_key_to_event_name(). */ + +static enum evhook_status +invoke_keybinding_callback(va_list ap, void *data) +{ + PyObject *callback = data; + struct session *saved_python_ses = python_ses; + PyObject *result; + + python_ses = va_arg(ap, struct session *); + + result = PyObject_CallFunction(callback, NULL); + if (result) + Py_DECREF(result); + else + alert_python_error(); + + python_ses = saved_python_ses; + + return EVENT_HOOK_STATUS_NEXT; +} + +/* Check that a keymap name is valid. */ + +static int +keymap_is_valid(const unsigned char *keymap) +{ + enum keymap_id keymap_id; + + for (keymap_id = 0; keymap_id < KEYMAP_MAX; ++keymap_id) + if (!strcmp(keymap, get_keymap_name(keymap_id))) + break; + return (keymap_id != KEYMAP_MAX); +} + +/* Python interface for binding keystrokes to callable objects. */ + +static char python_bind_key_doc[] = +PYTHON_DOCSTRING("bind_key(keystroke, callback[, keymap]) -> None\n\ +\n\ +Bind a keystroke to a callable object.\n\ +\n\ +Arguments:\n\ +\n\ +keystroke -- A string containing a keystroke. The syntax for\n\ + keystrokes is described in the elinkskeys(5) man page.\n\ +callback -- A callable object to be called when the keystroke is\n\ + typed. It will be called without any arguments.\n\ +\n\ +Optional arguments:\n\ +\n\ +keymap -- A string containing the name of a keymap. Valid keymap\n\ + names can be found in the elinkskeys(5) man page. By\n\ + default the \"main\" keymap is used.\n"); + +static PyObject * +python_bind_key(PyObject *self, PyObject *args, PyObject *kwargs) +{ + const unsigned char *keystroke; + PyObject *callback; + unsigned char *keymap = "main"; + PyObject *key_tuple; + PyObject *old_callback; + struct string event_name; + int event_id; + unsigned char *error_msg; + static char *kwlist[] = {"keystroke", "callback", "keymap", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|s:bind_key", kwlist, + &keystroke, &callback, &keymap)) + return NULL; + + assert(keystroke && callback && keymap); + if_assert_failed { + PyErr_SetString(python_elinks_err, N_("Internal error")); + return NULL; + } + + if (!keymap_is_valid(keymap)) { + PyErr_Format(python_elinks_err, "%s \"%s\"", + N_("Unrecognised keymap"), keymap); + return NULL; + } + + /* + * The callback object needs to be kept alive for as long as the + * keystroke is bound, so we stash a reference to it in a dictionary. + * We don't need to use the dictionary to find callbacks; its sole + * purpose is to prevent these objects from being garbage-collected + * by the Python interpreter. + * + * If binding the key fails for any reason after this point then + * we'll need to restore the dictionary to its previous state, which + * is temporarily preserved in @old_callback. + */ + key_tuple = Py_BuildValue("ss", keymap, keystroke); + if (!key_tuple) + return NULL; + old_callback = PyDict_GetItem(keybindings, key_tuple); + Py_XINCREF(old_callback); + if (PyDict_SetItem(keybindings, key_tuple, callback) != 0) { + Py_DECREF(key_tuple); + Py_XDECREF(old_callback); + return NULL; + } + + if (!init_string(&event_name)) { + PyErr_NoMemory(); + goto rollback; + } + if (!add_format_to_string(&event_name, "python-func %p", callback)) { + PyErr_SetFromErrno(python_elinks_err); + done_string(&event_name); + goto rollback; + } + event_id = bind_key_to_event_name(keymap, keystroke, event_name.source, + &error_msg); + done_string(&event_name); + if (error_msg) { + PyErr_SetString(python_elinks_err, error_msg); + goto rollback; + } + + event_id = register_event_hook(event_id, invoke_keybinding_callback, 0, + callback); + if (event_id == EVENT_NONE) { + PyErr_SetString(python_elinks_err, + N_("Error registering event hook")); + goto rollback; + } + + Py_DECREF(key_tuple); + Py_XDECREF(old_callback); + + Py_INCREF(Py_None); + return Py_None; + +rollback: + /* + * If an error occurred, try to restore the keybindings dictionary + * to its previous state. + */ + if (old_callback) { + (void) PyDict_SetItem(keybindings, key_tuple, old_callback); + Py_DECREF(old_callback); + } else { + (void) PyDict_DelItem(keybindings, key_tuple); + } + + Py_DECREF(key_tuple); + return NULL; +} + +static PyMethodDef keybinding_methods[] = { + {"bind_key", (PyCFunction) python_bind_key, + METH_VARARGS | METH_KEYWORDS, + python_bind_key_doc}, + + {NULL, NULL, 0, NULL} +}; + +int +python_init_keybinding_interface(PyObject *dict, PyObject *name) +{ + keybindings = PyDict_New(); + if (!keybindings) return -1; + + return add_python_methods(dict, name, keybinding_methods); +} + +void +python_done_keybinding_interface(void) +{ + Py_XDECREF(keybindings); +} diff --git a/src/scripting/python/keybinding.h b/src/scripting/python/keybinding.h new file mode 100644 index 00000000..e0c73a16 --- /dev/null +++ b/src/scripting/python/keybinding.h @@ -0,0 +1,9 @@ +#ifndef EL__SCRIPTING_PYTHON_KEYBINDING_H +#define EL__SCRIPTING_PYTHON_KEYBINDING_H + +#include + +int python_init_keybinding_interface(PyObject *dict, PyObject *name); +void python_done_keybinding_interface(void); + +#endif diff --git a/src/scripting/python/load.c b/src/scripting/python/load.c new file mode 100644 index 00000000..daecfcb0 --- /dev/null +++ b/src/scripting/python/load.c @@ -0,0 +1,167 @@ +/* Document loading for Python. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "elinks.h" + +#include "cache/cache.h" +#include "intl/gettext/libintl.h" +#include "network/connection.h" +#include "network/state.h" +#include "protocol/uri.h" +#include "scripting/python/core.h" +#include "session/download.h" +#include "session/session.h" +#include "session/task.h" +#include "util/error.h" +#include "util/memory.h" + +struct python_load_uri_callback_hop { + struct session *ses; + PyObject *callback; +}; + +/* C wrapper that invokes Python callbacks for load_uri(). */ + +static void +invoke_load_uri_callback(struct download *download, void *data) +{ + struct python_load_uri_callback_hop *hop = data; + struct session *saved_python_ses = python_ses; + + assert(download); + if_assert_failed { + if (hop && hop->callback) { + Py_DECREF(hop->callback); + } + mem_free_if(hop); + return; + } + + if (is_in_progress_state(download->state)) return; + + assert(hop && hop->callback); + if_assert_failed { + mem_free(download); + mem_free_if(hop); + return; + } + + if (download->cached) { + PyObject *result; + struct fragment *f = download->cached->frag.next; + + python_ses = hop->ses; + + result = PyObject_CallFunction(hop->callback, "ss#", + download->cached->head, + f ? f->data : NULL, + f ? f->length : 0); + if (result) + Py_DECREF(result); + else + alert_python_error(); + } + + Py_DECREF(hop->callback); + mem_free(hop); + mem_free(download); + + python_ses = saved_python_ses; +} + +/* Python interface for loading a document. */ + +static char python_load_doc[] = +PYTHON_DOCSTRING("load(url, callback) -> None\n\ +\n\ +Load a document into the ELinks cache and pass its contents to a\n\ +callable object.\n\ +\n\ +Arguments:\n\ +\n\ +url -- A string containing the URL to load.\n\ +callback -- A callable object to be called after the document has\n\ + been loaded. It will be called with two arguments: the first\n\ + will be a string representing the document's header, or None\n\ + if it has no header; the second will be a string representing\n\ + the document's body, or None if it has no body.\n"); + +static PyObject * +python_load(PyObject *self, PyObject *args) +{ + unsigned char *uristring; + PyObject *callback; + struct uri *uri; + struct download *download; + struct python_load_uri_callback_hop *hop; + + if (!python_ses) { + PyErr_SetString(python_elinks_err, "No session"); + return NULL; + } + + if (!PyArg_ParseTuple(args, "sO:load", &uristring, &callback)) + return NULL; + + assert(uristring && callback); + if_assert_failed { + PyErr_SetString(python_elinks_err, N_("Internal error")); + return NULL; + } + + uri = get_translated_uri(uristring, python_ses->tab->term->cwd); + if (!uri) { + PyErr_SetString(python_elinks_err, N_("Bad URL syntax")); + return NULL; + } + + download = mem_alloc(sizeof(*download)); + if (!download) goto mem_error; + + hop = mem_alloc(sizeof(*hop)); + if (!hop) goto free_download; + hop->ses = python_ses; + hop->callback = callback; + Py_INCREF(callback); + + download->data = hop; + download->callback = (download_callback_T *) invoke_load_uri_callback; + if (load_uri(uri, NULL, download, PRI_MAIN, CACHE_MODE_NORMAL, -1) != 0) { + PyErr_SetString(python_elinks_err, + get_state_message(download->state, + python_ses->tab->term)); + done_uri(uri); + return NULL; + } + + done_uri(uri); + Py_INCREF(Py_None); + return Py_None; + +free_download: + mem_free(download); + +mem_error: + done_uri(uri); + return PyErr_NoMemory(); +} + + +static PyMethodDef load_methods[] = { + {"load", python_load, + METH_VARARGS, + python_load_doc}, + + {NULL, NULL, 0, NULL} +}; + +int +python_init_load_interface(PyObject *dict, PyObject *name) +{ + return add_python_methods(dict, name, load_methods); +} diff --git a/src/scripting/python/load.h b/src/scripting/python/load.h new file mode 100644 index 00000000..3050543b --- /dev/null +++ b/src/scripting/python/load.h @@ -0,0 +1,8 @@ +#ifndef EL__SCRIPTING_PYTHON_LOAD_H +#define EL__SCRIPTING_PYTHON_LOAD_H + +#include + +int python_init_load_interface(PyObject *dict, PyObject *name); + +#endif diff --git a/src/scripting/python/menu.c b/src/scripting/python/menu.c new file mode 100644 index 00000000..a648d626 --- /dev/null +++ b/src/scripting/python/menu.c @@ -0,0 +1,241 @@ +/* Simple menus for Python. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "elinks.h" + +#include "bfu/menu.h" +#include "document/document.h" +#include "document/view.h" +#include "intl/gettext/libintl.h" +#include "scripting/python/core.h" +#include "session/session.h" +#include "terminal/window.h" +#include "util/error.h" +#include "util/memlist.h" +#include "util/memory.h" +#include "util/string.h" +#include "viewer/text/view.h" + +/* C wrapper that invokes Python callbacks for menu items. */ + +static void +invoke_menu_callback(struct terminal *term, void *data, void *ses) +{ + PyObject *callback = data; + struct session *saved_python_ses = python_ses; + PyObject *result; + + python_ses = ses; + + result = PyObject_CallFunction(callback, NULL); + if (result) + Py_DECREF(result); + else + alert_python_error(); + + Py_DECREF(callback); + + python_ses = saved_python_ses; +} + +enum python_menu_type { + PYTHON_MENU_DEFAULT, + PYTHON_MENU_LINK, + PYTHON_MENU_TAB, + PYTHON_MENU_MAX +}; + +/* Python interface for displaying simple menus. */ + +static char python_menu_doc[] = +PYTHON_DOCSTRING("menu(items[, type]) -> None\n\ +\n\ +Display a menu.\n\ +\n\ +Arguments:\n\ +\n\ +items -- A sequence of tuples. Each tuple must have two elements: a\n\ + string containing the name of a menu item, and a callable\n\ + object that will be called without any arguments if the user\n\ + selects that menu item.\n\ +\n\ +Optional arguments:\n\ +\n\ +type -- A constant specifying the type of menu to display. By default\n\ + the menu is displayed at the top of the screen, but if this\n\ + argument's value is the constant elinks.MENU_TAB then the menu\n\ + is displayed in the same location as the ELinks tab menu. If\n\ + its value is the constant elinks.MENU_LINK then the menu is\n\ + displayed in the same location as the ELinks link menu and is\n\ + not displayed unless a link is currently selected.\n"); + +static PyObject * +python_menu(PyObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *items; + enum python_menu_type menu_type = PYTHON_MENU_DEFAULT; + int length, i; + struct menu_item *menu; + struct memory_list *ml = NULL; + static char *kwlist[] = {"items", "type", NULL}; + + if (!python_ses) { + PyErr_SetString(python_elinks_err, "No session"); + return NULL; + } + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|i:menu", + kwlist, &items, &menu_type)) + return NULL; + + assert(items); + if_assert_failed { + PyErr_SetString(python_elinks_err, N_("Internal error")); + return NULL; + } + + if (!PySequence_Check(items)) { + PyErr_SetString(PyExc_TypeError, "Argument must be a sequence"); + return NULL; + } + length = PySequence_Length(items); + if (length == -1) return NULL; + else if (length == 0) goto success; + + if (menu_type < 0 || menu_type >= PYTHON_MENU_MAX) { + PyErr_Format(python_elinks_err, "%s %d", + N_("Bad number"), menu_type); + return NULL; + + } else if (menu_type == PYTHON_MENU_LINK) { + if (!get_current_link(current_frame(python_ses))) + goto success; + + } else if (menu_type == PYTHON_MENU_TAB + && python_ses->status.show_tabs_bar) { + int y; + + if (python_ses->status.show_tabs_bar_at_top) + y = python_ses->status.show_title_bar; + else + y = python_ses->tab->term->height - length + - python_ses->status.show_status_bar - 2; + + set_window_ptr(python_ses->tab, python_ses->tab->xpos, + int_max(y, 0)); + + } else { + set_window_ptr(python_ses->tab, 0, 0); + } + + menu = new_menu(FREE_LIST | FREE_TEXT | NO_INTL); + if (!menu) return PyErr_NoMemory(); + + /* + * Keep track of all the memory we allocate so we'll be able to free + * it in case any error prevents us from displaying the menu. + */ + ml = getml(menu, NULL); + if (!ml) { + mem_free(menu); + return PyErr_NoMemory(); + } + + for (i = 0; i < length; i++) { + PyObject *tuple = PySequence_GetItem(items, i); + PyObject *name, *callback; + unsigned char *contents; + + if (!tuple) goto error; + + if (!PyTuple_Check(tuple)) { + Py_DECREF(tuple); + PyErr_SetString(PyExc_TypeError, + "Argument must be sequence of tuples"); + goto error; + } + name = PyTuple_GetItem(tuple, 0); + callback = PyTuple_GetItem(tuple, 1); + Py_DECREF(tuple); + if (!name || !callback) goto error; + + contents = (unsigned char *) PyString_AsString(name); + if (!contents) goto error; + + contents = stracpy(contents); + if (!contents) { + PyErr_NoMemory(); + goto error; + } + add_one_to_ml(&ml, contents); + + /* + * FIXME: We need to increment the reference counts for + * callbacks so they won't be garbage-collected by the Python + * interpreter before they're called. But for any callback + * that isn't called (because the user doesn't select the + * corresponding menu item) we'll never have an opportunity + * to decrement the reference count again, so this code leaks + * references. It probably can't be fixed without changes to + * the menu machinery in bfu/menu.c, e.g. to call an arbitrary + * clean-up function when a menu is destroyed. + * + * The good news is that in a typical usage case, where the + * callback objects wouldn't be garbage-collected anyway until + * the Python interpreter exits, this makes no difference at + * all. But it's not strictly correct, and it could leak memory + * in more elaborate usage where callback objects are created + * and thrown away on the fly. + */ + Py_INCREF(callback); + add_to_menu(&menu, contents, NULL, ACT_MAIN_NONE, + invoke_menu_callback, callback, 0); + } + + do_menu(python_ses->tab->term, menu, python_ses, 1); + +success: + mem_free_if(ml); + + Py_INCREF(Py_None); + return Py_None; + +error: + freeml(ml); + return NULL; +} + +static PyMethodDef menu_methods[] = { + {"menu", (PyCFunction) python_menu, + METH_VARARGS | METH_KEYWORDS, + python_menu_doc}, + + {NULL, NULL, 0, NULL} +}; + +static int +add_constant(PyObject *dict, const char *key, int value) +{ + PyObject *constant = PyInt_FromLong(value); + int result; + + if (!constant) return -1; + result = PyDict_SetItemString(dict, key, constant); + Py_DECREF(constant); + + return result; +} + +int +python_init_menu_interface(PyObject *dict, PyObject *name) +{ + if (add_constant(dict, "MENU_LINK", PYTHON_MENU_LINK) != 0) return -1; + if (add_constant(dict, "MENU_TAB", PYTHON_MENU_TAB) != 0) return -1; + + return add_python_methods(dict, name, menu_methods); +} diff --git a/src/scripting/python/menu.h b/src/scripting/python/menu.h new file mode 100644 index 00000000..013047d3 --- /dev/null +++ b/src/scripting/python/menu.h @@ -0,0 +1,8 @@ +#ifndef EL__SCRIPTING_PYTHON_MENU_H +#define EL__SCRIPTING_PYTHON_MENU_H + +#include + +int python_init_menu_interface(PyObject *dict, PyObject *name); + +#endif diff --git a/src/scripting/python/open.c b/src/scripting/python/open.c new file mode 100644 index 00000000..ffb0cd85 --- /dev/null +++ b/src/scripting/python/open.c @@ -0,0 +1,92 @@ +/* Document viewing for Python. */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include + +#include "elinks.h" + +#include "intl/gettext/libintl.h" +#include "protocol/uri.h" +#include "scripting/python/core.h" +#include "session/task.h" +#include "terminal/tab.h" +#include "util/error.h" + +/* Python interface for viewing a document. */ + +static char python_open_doc[] = +PYTHON_DOCSTRING("open(url, new_tab=False, background=False) -> None\n\ +\n\ +View a document in either the current tab or a new tab.\n\ +\n\ +Arguments:\n\ +\n\ +url -- A string containing the URL to view.\n\ +\n\ +Optional keyword arguments:\n\ +\n\ +new_tab -- By default the URL is opened in the current tab. If this\n\ + argument's value is the boolean True then the URL is instead\n\ + opened in a new tab.\n\ +background -- By default a new tab is opened in the foreground. If\n\ + this argument's value is the boolean True then a new tab is\n\ + instead opened in the background. This argument is ignored\n\ + unless new_tab's value is True.\n"); + +static PyObject * +python_open(PyObject *self, PyObject *args, PyObject *kwargs) +{ + unsigned char *url; + int new_tab = 0, background = 0; + struct uri *uri; + static char *kwlist[] = {"url", "new_tab", "background", NULL}; + + if (!python_ses) { + PyErr_SetString(python_elinks_err, "No session"); + return NULL; + } + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|ii:open", + kwlist, &url, + &new_tab, &background)) + return NULL; + + assert(url); + if_assert_failed { + PyErr_SetString(python_elinks_err, N_("Internal error")); + return NULL; + } + + uri = get_translated_uri(url, python_ses->tab->term->cwd); + if (!uri) { + PyErr_SetString(python_elinks_err, N_("Bad URL syntax")); + return NULL; + } + + if (new_tab) + open_uri_in_new_tab(python_ses, uri, background, 0); + else + goto_uri(python_ses, uri); + + done_uri(uri); + + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef open_methods[] = { + {"open", (PyCFunction) python_open, + METH_VARARGS | METH_KEYWORDS, + python_open_doc}, + + {NULL, NULL, 0, NULL} +}; + +int +python_init_open_interface(PyObject *dict, PyObject *name) +{ + return add_python_methods(dict, name, open_methods); +} diff --git a/src/scripting/python/open.h b/src/scripting/python/open.h new file mode 100644 index 00000000..bbcf5da4 --- /dev/null +++ b/src/scripting/python/open.h @@ -0,0 +1,8 @@ +#ifndef EL__SCRIPTING_PYTHON_OPEN_H +#define EL__SCRIPTING_PYTHON_OPEN_H + +#include + +int python_init_open_interface(PyObject *dict, PyObject *name); + +#endif