Re: [patch] additional functionality for Python backend
"M. Levinson" <[email protected]>
| Newsgroups | gmane.comp.web.links |
|---|---|
| Message-ID | <[email protected]> |
On Sep 23, 2006, Kalle Olavi Niemitalo writes:
>"M. Levinson" <levinsm at users.sourceforge.net> writes:
>> +static char python_info_box_doc[] = \
>> +PYTHON_DOCSTRING("info_box(text[, title]) -> None\n\
>
>The backslash after the equals sign looks unnecessary,
>although it shouldn't hurt either.
Of course, I think my brain must have been in Python mode rather than
C mode when I typed that....
>> +#define PYTHON_DOCSTRING(str) ("")
>> +#endif
>
>Please remove the parentheses from the macro.
>static char python_info_box_doc[] = (""); is not standard C
>and is not accepted by Tiny C Compiler 0.9.23.
And for this one I have no excuse. Fixed in the attached version - thanks!
_______________________________________________
elinks-dev mailing list
[email protected]
http://linuxfromscratch.org/mailman/listinfo/elinks-dev
python-backend.diff
(text/x-patch, 78.5 KB)
src/scripting/python/Makefile | 11 +
src/scripting/python/dialogs.h | 8 +
src/scripting/python/dialogs.c | 248 ++++++++++++++++++++++
src/scripting/python/document.h | 8 +
src/scripting/python/document.c | 144 +++++++++++++
src/scripting/python/keybinding.h | 9 +
src/scripting/python/keybinding.c | 198 +++++++++++++++++
src/scripting/python/load.h | 8 +
src/scripting/python/load.c | 167 +++++++++++++++
src/scripting/python/menu.h | 8 +
src/scripting/python/menu.c | 241 +++++++++++++++++++++
src/scripting/python/open.h | 8 +
src/scripting/python/open.c | 92 ++++++++
src/scripting/python/python.c | 3
src/scripting/python/core.h | 19 +-
src/scripting/python/core.c | 172 +++++++++++----
src/scripting/python/hooks.c | 426 +++++++++++++++++--------------------
contrib/python/hooks.py | 326 +++++++++++++++++++++++-----
doc/python.txt | 253 ++++++++++++++++++++++
contrib/python/README.Python | 8 -
contrib/python/elinks_maint.py | 129 +++++++++++
21 files changed, 2149 insertions(+), 337 deletions(-)
diff --git a/src/scripting/python/Makefile b/src/scripting/python/Makefile
index 486eddd..a84ed9e 100644
--- a/src/scripting/python/Makefile
+++ b/src/scripting/python/Makefile
@@ -3,6 +3,15 @@ include $(top_builddir)/Makefile.config
INCLUDES += $(PYTHON_CFLAGS)
-OBJS = python.o hooks.o core.o
+OBJS = \
+ core.o \
+ dialogs.o \
+ document.o \
+ hooks.o \
+ keybinding.o \
+ load.o \
+ menu.o \
+ open.o \
+ python.o
include $(top_srcdir)/Makefile.lib
diff --git a/src/scripting/python/dialogs.h b/src/scripting/python/dialogs.h
new file mode 100644
index 0000000..8f203d1
--- /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 <Python.h>
+
+int python_init_dialogs_interface(PyObject *dict, PyObject *name);
+
+#endif
diff --git a/src/scripting/python/dialogs.c b/src/scripting/python/dialogs.c
new file mode 100644
index 0000000..009ede7
--- /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 <Python.h>
+
+#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/document.h b/src/scripting/python/document.h
new file mode 100644
index 0000000..63915be
--- /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 <Python.h>
+
+int python_init_document_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 0000000..c7a1399
--- /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 <Python.h>
+
+#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/keybinding.h b/src/scripting/python/keybinding.h
new file mode 100644
index 0000000..e0c73a1
--- /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 <Python.h>
+
+int python_init_keybinding_interface(PyObject *dict, PyObject *name);
+void python_done_keybinding_interface(void);
+
+#endif
diff --git a/src/scripting/python/keybinding.c b/src/scripting/python/keybinding.c
new file mode 100644
index 0000000..2d6668c
--- /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 <Python.h>
+
+#include <stdarg.h>
+#include <string.h>
+
+#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/load.h b/src/scripting/python/load.h
new file mode 100644
index 0000000..3050543
--- /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 <Python.h>
+
+int python_init_load_interface(PyObject *dict, PyObject *name);
+
+#endif
diff --git a/src/scripting/python/load.c b/src/scripting/python/load.c
new file mode 100644
index 0000000..daecfcb
--- /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 <Python.h>
+
+#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/menu.h b/src/scripting/python/menu.h
new file mode 100644
index 0000000..013047d
--- /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 <Python.h>
+
+int python_init_menu_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 0000000..a648d62
--- /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 <Python.h>
+
+#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/open.h b/src/scripting/python/open.h
new file mode 100644
index 0000000..bbcf5da
--- /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 <Python.h>
+
+int python_init_open_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 0000000..ffb0cd8
--- /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 <Python.h>
+
+#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/python.c b/src/scripting/python/python.c
index cf9e0c0..e1bc374 100644
--- a/src/scripting/python/python.c
+++ b/src/scripting/python/python.c
@@ -4,11 +4,12 @@ #ifdef HAVE_CONFIG_H
#include "config.h"
#endif
-#include "scripting/python/core.h"
+#include <Python.h>
#include "elinks.h"
#include "main/module.h"
+#include "scripting/python/core.h"
#include "scripting/python/hooks.h"
diff --git a/src/scripting/python/core.h b/src/scripting/python/core.h
index 4450313..4f07938 100644
--- a/src/scripting/python/core.h
+++ b/src/scripting/python/core.h
@@ -2,11 +2,26 @@
#ifndef EL__SCRIPTING_PYTHON_CORE_H
#define EL__SCRIPTING_PYTHON_CORE_H
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
struct module;
-struct session;
-void alert_python_error(struct session *ses);
+extern struct session *python_ses;
+extern PyObject *python_elinks_err;
+
+void alert_python_error(void);
+
void init_python(struct module *module);
void cleanup_python(struct module *module);
+int add_python_methods(PyObject *dict, PyObject *name, PyMethodDef *methods);
+
+#ifndef CONFIG_SMALL
+#define PYTHON_DOCSTRING(str) str
+#else
+#define PYTHON_DOCSTRING(str) ""
+#endif
+
#endif
diff --git a/src/scripting/python/core.c b/src/scripting/python/core.c
index 83eb2c3..05de1da 100644
--- a/src/scripting/python/core.c
+++ b/src/scripting/python/core.c
@@ -5,37 +5,42 @@ #include "config.h"
#endif
#include <Python.h>
+#include <osdefs.h>
-#include <stdio.h>
#include <stdlib.h>
#include "elinks.h"
#include "config/home.h"
#include "main/module.h"
-#include "scripting/scripting.h"
#include "scripting/python/core.h"
+#include "scripting/python/dialogs.h"
+#include "scripting/python/document.h"
+#include "scripting/python/keybinding.h"
+#include "scripting/python/load.h"
+#include "scripting/python/menu.h"
+#include "scripting/python/open.h"
#include "scripting/python/python.h"
+#include "scripting/scripting.h"
+#include "session/session.h"
#include "util/env.h"
-#include "util/file.h"
#include "util/string.h"
+struct session *python_ses = NULL;
-PyObject *pDict = NULL, *pModule = NULL;
+PyObject *python_elinks_err = NULL;
+PyObject *python_hooks = NULL;
/* Error reporting. */
void
-alert_python_error(struct session *ses)
+alert_python_error(void)
{
unsigned char *msg = "(no traceback available)";
PyObject *err_type = NULL, *err_value = NULL, *err_traceback = NULL;
PyObject *tb_module = NULL;
- PyObject *tb_dict;
- PyObject *format_function;
PyObject *msg_list = NULL;
PyObject *empty_string = NULL;
- PyObject *join_method = NULL;
PyObject *msg_string = NULL;
unsigned char *temp;
@@ -46,17 +51,15 @@ alert_python_error(struct session *ses)
*/
PyErr_Fetch(&err_type, &err_value, &err_traceback);
PyErr_NormalizeException(&err_type, &err_value, &err_traceback);
- if (!err_traceback) goto end;
+ if (!err_type) goto end;
tb_module = PyImport_ImportModule("traceback");
if (!tb_module) goto end;
- tb_dict = PyModule_GetDict(tb_module);
- format_function = PyDict_GetItemString(tb_dict, "format_exception");
- if (!format_function || !PyCallable_Check(format_function)) goto end;
-
- msg_list = PyObject_CallFunction(format_function, "OOO",
- err_type, err_value, err_traceback);
+ msg_list = PyObject_CallMethod(tb_module, "format_exception", "OOO",
+ err_type,
+ err_value ? err_value : Py_None,
+ err_traceback ? err_traceback : Py_None);
if (!msg_list) goto end;
/*
@@ -67,17 +70,14 @@ alert_python_error(struct session *ses)
empty_string = PyString_FromString("");
if (!empty_string) goto end;
- join_method = PyObject_GetAttrString(empty_string, "join");
- if (!join_method || !PyCallable_Check(join_method)) goto end;
-
- msg_string = PyObject_CallFunction(join_method, "O", msg_list);
+ msg_string = PyObject_CallMethod(empty_string, "join", "O", msg_list);
if (!msg_string) goto end;
- temp = (unsigned char *)PyString_AsString(msg_string);
+ temp = (unsigned char *) PyString_AsString(msg_string);
if (temp) msg = temp;
end:
- report_scripting_error(&python_scripting_module, ses, msg);
+ report_scripting_error(&python_scripting_module, python_ses, msg);
Py_XDECREF(err_type);
Py_XDECREF(err_value);
@@ -85,31 +85,69 @@ end:
Py_XDECREF(tb_module);
Py_XDECREF(msg_list);
Py_XDECREF(empty_string);
- Py_XDECREF(join_method);
Py_XDECREF(msg_string);
/* In case another error occurred while reporting the original error: */
PyErr_Clear();
}
-void
-cleanup_python(struct module *module)
-{
- if (Py_IsInitialized()) {
- Py_XDECREF(pDict);
- Py_XDECREF(pModule);
- Py_Finalize();
- }
+/* Prepend ELinks directories to Python's search path. */
+
+static int
+set_python_search_path(void)
+{
+ struct string new_python_path, *okay;
+ unsigned char *old_python_path;
+ int result = -1;
+
+ if (!init_string(&new_python_path)) return result;
+
+ old_python_path = (unsigned char *) getenv("PYTHONPATH");
+ if (old_python_path)
+ okay = add_format_to_string(&new_python_path, "%s%c%s%c%s",
+ elinks_home, DELIM, CONFDIR,
+ DELIM, old_python_path);
+ else
+ okay = add_format_to_string(&new_python_path, "%s%c%s",
+ elinks_home, DELIM, CONFDIR);
+ if (okay) result = env_set("PYTHONPATH", new_python_path.source, -1);
+ done_string(&new_python_path);
+ return result;
}
+/* Module-level documentation for the Python interpreter's elinks module. */
+
+static char module_doc[] =
+PYTHON_DOCSTRING("Interface to the ELinks web browser.\n\
+\n\
+Functions:\n\
+\n\
+bind_key() -- Bind a keystroke to a callable object.\n\
+current_document() -- Return the body of the document being viewed.\n\
+current_header() -- Return the header of the document being viewed.\n\
+current_link_url() -- Return the URL of the currently selected link.\n\
+current_title() -- Return the title of the document being viewed.\n\
+current_url() -- Return the URL of the document being viewed.\n\
+info_box() -- Display information to the user.\n\
+input_box() -- Prompt for user input.\n\
+load() -- Load a document into the ELinks cache.\n\
+menu() -- Display a menu.\n\
+open() -- View a document.\n\
+\n\
+Exception classes:\n\
+\n\
+error -- Errors internal to ELinks.\n\
+\n\
+Other public objects:\n\
+\n\
+home -- A string containing the pathname of the ~/.elinks directory.\n");
+
void
init_python(struct module *module)
{
- unsigned char *python_path = straconcat(elinks_home, ":", CONFDIR, NULL);
+ PyObject *elinks_module, *module_dict, *module_name;
- if (!python_path) return;
- env_set("PYTHONPATH", python_path, -1);
- mem_free(python_path);
+ if (set_python_search_path() != 0) return;
/* Treat warnings as errors so they can be caught and handled;
* otherwise they would be printed to stderr.
@@ -123,12 +161,66 @@ init_python(struct module *module)
PySys_AddWarnOption("error");
Py_Initialize();
- pModule = PyImport_ImportModule("hooks");
- if (pModule) {
- pDict = PyModule_GetDict(pModule);
- Py_INCREF(pDict);
- } else {
- alert_python_error(NULL);
+ elinks_module = Py_InitModule3("elinks", NULL, module_doc);
+ if (!elinks_module) goto python_error;
+
+ if (PyModule_AddStringConstant(elinks_module, "home", elinks_home) != 0)
+ goto python_error;
+
+ python_elinks_err = PyErr_NewException("elinks.error", NULL, NULL);
+ if (!python_elinks_err) goto python_error;
+
+ if (PyModule_AddObject(elinks_module, "error", python_elinks_err) != 0)
+ goto python_error;
+
+ module_dict = PyModule_GetDict(elinks_module);
+ if (!module_dict) goto python_error;
+ module_name = PyString_FromString("elinks");
+ if (!module_name) goto python_error;
+
+ if (python_init_dialogs_interface(module_dict, module_name) != 0
+ || python_init_document_interface(module_dict, module_name) != 0
+ || python_init_keybinding_interface(module_dict, module_name) != 0
+ || python_init_load_interface(module_dict, module_name) != 0
+ || python_init_menu_interface(module_dict, module_name) != 0
+ || python_init_open_interface(module_dict, module_name) != 0)
+ goto python_error;
+
+ python_hooks = PyImport_ImportModule("hooks");
+ if (!python_hooks) goto python_error;
+
+ return;
+
+python_error:
+ alert_python_error();
+}
+
+void
+cleanup_python(struct module *module)
+{
+ if (Py_IsInitialized()) {
+ python_done_keybinding_interface();
+ Py_XDECREF(python_hooks);
+ Py_Finalize();
+ }
+}
+
+/* Add methods to a Python extension module. */
+
+int
+add_python_methods(PyObject *dict, PyObject *name, PyMethodDef *methods)
+{
+ PyMethodDef *method;
+
+ for (method = methods; method && method->ml_name; method++) {
+ PyObject *function = PyCFunction_NewEx(method, NULL, name);
+ int result;
+
+ if (!function) return -1;
+ result = PyDict_SetItemString(dict, method->ml_name, function);
+ Py_DECREF(function);
+ if (result != 0) return -1;
}
+ return 0;
}
diff --git a/src/scripting/python/hooks.c b/src/scripting/python/hooks.c
index af9b618..23a07f0 100644
--- a/src/scripting/python/hooks.c
+++ b/src/scripting/python/hooks.c
@@ -1,225 +1,201 @@
-/* Python scripting hooks */
-
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-
-
-#include "elinks.h"
-
-#include "cache/cache.h"
-#include "main/event.h"
-#include "protocol/uri.h"
-#include "scripting/python/core.h"
-#include "scripting/python/hooks.h"
-#include "session/location.h"
-#include "session/session.h"
-#include "util/string.h"
-
-#undef _POSIX_C_SOURCE
-#include <Python.h>
-
-/* The events that will trigger the functions below and what they are expected
- * to do is explained in doc/events.txt */
-
-extern PyObject *pDict;
-extern PyObject *pModule;
-
-static void
-do_script_hook_goto_url(struct session *ses, unsigned char **url)
-{
- PyObject *pFunc = PyDict_GetItemString(pDict, "goto_url_hook");
-
- if (pFunc && PyCallable_Check(pFunc)) {
- PyObject *pValue;
- unsigned char *current_url;
-
- if (!ses || !have_location(ses)) {
- current_url = NULL;
- } else {
- current_url = struri(cur_loc(ses)->vs.uri);
- }
-
- pValue = PyObject_CallFunction(pFunc, "ss", *url, current_url);
- if (pValue) {
- if (pValue != Py_None) {
- const unsigned char *str;
- unsigned char *new_url;
-
- str = PyString_AsString(pValue);
- if (str) {
- new_url = stracpy((unsigned char *)str);
- if (new_url) mem_free_set(url, new_url);
- }
- }
- Py_DECREF(pValue);
- } else {
- alert_python_error(ses);
- }
- }
-}
-
-static enum evhook_status
-script_hook_goto_url(va_list ap, void *data)
-{
- unsigned char **url = va_arg(ap, unsigned char **);
- struct session *ses = va_arg(ap, struct session *);
-
- if (pDict && *url)
- do_script_hook_goto_url(ses, url);
-
- return EVENT_HOOK_STATUS_NEXT;
-}
-
-static void
-do_script_hook_follow_url(struct session *ses, unsigned char **url)
-{
- PyObject *pFunc = PyDict_GetItemString(pDict, "follow_url_hook");
-
- if (pFunc && PyCallable_Check(pFunc)) {
- PyObject *pValue = PyObject_CallFunction(pFunc, "s", *url);
- if (pValue) {
- if (pValue != Py_None) {
- const unsigned char *str;
- unsigned char *new_url;
-
- str = PyString_AsString(pValue);
- if (str) {
- new_url = stracpy((unsigned char *)str);
- if (new_url) mem_free_set(url, new_url);
- }
- }
- Py_DECREF(pValue);
- } else {
- alert_python_error(ses);
- }
- }
-}
-
-static enum evhook_status
-script_hook_follow_url(va_list ap, void *data)
-{
- unsigned char **url = va_arg(ap, unsigned char **);
- struct session *ses = va_arg(ap, struct session *);
-
- if (pDict && *url)
- do_script_hook_follow_url(ses, url);
-
- return EVENT_HOOK_STATUS_NEXT;
-}
-
-static void
-do_script_hook_pre_format_html(struct session *ses, unsigned char *url,
- struct cache_entry *cached,
- struct fragment *fragment)
-{
- PyObject *pFunc = PyDict_GetItemString(pDict, "pre_format_html_hook");
-
- if (pFunc && PyCallable_Check(pFunc)) {
- PyObject *pValue = PyObject_CallFunction(pFunc, "ss#", url,
- fragment->data,
- fragment->length);
-
- if (pValue) {
- if (pValue != Py_None) {
- const unsigned char *str;
- int len;
-
- str = PyString_AsString(pValue);
- if (str) {
- len = PyString_Size(pValue);
- add_fragment(cached, 0, str, len);
- normalize_cache_entry(cached, len);
- }
- }
- Py_DECREF(pValue);
- } else {
- alert_python_error(ses);
- }
- }
-}
-
-static enum evhook_status
-script_hook_pre_format_html(va_list ap, void *data)
-{
- struct session *ses = va_arg(ap, struct session *);
- struct cache_entry *cached = va_arg(ap, struct cache_entry *);
- struct fragment *fragment = get_cache_fragment(cached);
- unsigned char *url = struri(cached->uri);
-
- if (pDict && ses && url && cached->length && *fragment->data)
- do_script_hook_pre_format_html(ses, url, cached, fragment);
-
- return EVENT_HOOK_STATUS_NEXT;
-}
-
-static inline void
-do_script_hook_get_proxy(unsigned char **new_proxy_url, unsigned char *url)
-{
- PyObject *pFunc = PyDict_GetItemString(pDict, "proxy_for_hook");
-
- if (pFunc && PyCallable_Check(pFunc)) {
- PyObject *pValue = PyObject_CallFunction(pFunc, "s", url);
-
- if (pValue) {
- if (pValue != Py_None) {
- const unsigned char *str;
- unsigned char *new_url;
-
- str = PyString_AsString(pValue);
- if (str) {
- new_url = stracpy((unsigned char *)str);
- if (new_url) mem_free_set(new_proxy_url,
- new_url);
- }
- }
- Py_DECREF(pValue);
- } else {
- alert_python_error(NULL);
- }
- }
-}
-
-static enum evhook_status
-script_hook_get_proxy(va_list ap, void *data)
-{
- unsigned char **new_proxy_url = va_arg(ap, unsigned char **);
- unsigned char *url = va_arg(ap, unsigned char *);
-
- if (pDict && new_proxy_url && url)
- do_script_hook_get_proxy(new_proxy_url, url);
-
- return EVENT_HOOK_STATUS_NEXT;
-}
-
-static void
-do_script_hook_quit(void)
-{
- PyObject *pFunc = PyDict_GetItemString(pDict, "quit_hook");
-
- if (pFunc && PyCallable_Check(pFunc)) {
- PyObject *pValue = PyObject_CallFunction(pFunc, NULL);
-
- if (pValue) {
- Py_DECREF(pValue);
- } else {
- alert_python_error(NULL);
- }
- }
-}
-
-static enum evhook_status
-script_hook_quit(va_list ap, void *data)
-{
- if (pDict) do_script_hook_quit();
- return EVENT_HOOK_STATUS_NEXT;
-}
-
-struct event_hook_info python_scripting_hooks[] = {
- { "goto-url", 0, script_hook_goto_url, NULL },
- { "follow-url", 0, script_hook_follow_url, NULL },
- { "pre-format-html", 0, script_hook_pre_format_html, NULL },
- { "get-proxy", 0, script_hook_get_proxy, NULL },
- { "quit", 0, script_hook_quit, NULL },
- NULL_EVENT_HOOK_INFO,
-};
+/* Python scripting hooks */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <Python.h>
+
+#include <stdarg.h>
+#include <string.h>
+
+#include "elinks.h"
+
+#include "cache/cache.h"
+#include "main/event.h"
+#include "protocol/uri.h"
+#include "scripting/python/core.h"
+#include "session/location.h"
+#include "session/session.h"
+#include "util/memory.h"
+#include "util/string.h"
+
+extern PyObject *python_hooks;
+
+/*
+ * A utility function for script_hook_url() and script_hook_get_proxy():
+ * Free a char * and replace it with the contents of a Python string.
+ * (Py_None is ignored.)
+ */
+
+static PyObject *
+replace_with_python_string(unsigned char **dest, PyObject *object)
+{
+ unsigned char *str;
+
+ if (object == Py_None) return object;
+
+ str = (unsigned char *) PyString_AsString(object);
+ if (!str) return NULL;
+
+ str = stracpy(str);
+ if (!str) return PyErr_NoMemory();
+
+ mem_free_set(dest, str);
+ return object;
+}
+
+/* Call a Python hook for a goto-url or follow-url event. */
+
+static enum evhook_status
+script_hook_url(va_list ap, void *data)
+{
+ unsigned char **url = va_arg(ap, unsigned char **);
+ struct session *ses = va_arg(ap, struct session *);
+ char *method = data;
+ struct session *saved_python_ses = python_ses;
+ PyObject *result;
+
+ evhook_use_params(url && ses);
+
+ if (!python_hooks || !url || !*url
+ || !PyObject_HasAttrString(python_hooks, method))
+ return EVENT_HOOK_STATUS_NEXT;
+
+ python_ses = ses;
+
+ /*
+ * Historical note: The only reason the goto and follow hooks are
+ * treated differently is to maintain backwards compatibility for
+ * people who already have a goto_url_hook() function in hooks.py
+ * that expects a second argument. If we were starting over from
+ * scratch, we could treat the goto and follow hooks identically and
+ * simply pass @url as the sole argument in both cases; the Python
+ * code for the goto hook no longer needs its @current_url argument
+ * since it could instead determine the current URL by calling the
+ * Python interpreter's elinks.current_url() function.
+ */
+ if (!strcmp(method, "goto_url_hook")) {
+ unsigned char *current_url = NULL;
+
+ if (python_ses && have_location(python_ses))
+ current_url = struri(cur_loc(ses)->vs.uri);
+
+ result = PyObject_CallMethod(python_hooks, method, "ss", *url,
+ current_url);
+ } else {
+ result = PyObject_CallMethod(python_hooks, method, "s", *url);
+ }
+
+ if (!result || !replace_with_python_string(url, result))
+ alert_python_error();
+
+ Py_XDECREF(result);
+
+ python_ses = saved_python_ses;
+
+ return EVENT_HOOK_STATUS_NEXT;
+}
+
+/* Call a Python hook for a pre-format-html event. */
+
+static enum evhook_status
+script_hook_pre_format_html(va_list ap, void *data)
+{
+ struct session *ses = va_arg(ap, struct session *);
+ struct cache_entry *cached = va_arg(ap, struct cache_entry *);
+ struct fragment *fragment = get_cache_fragment(cached);
+ unsigned char *url = struri(cached->uri);
+ char *method = "pre_format_html_hook";
+ struct session *saved_python_ses = python_ses;
+ PyObject *result;
+ int success = 0;
+
+ evhook_use_params(ses && cached);
+
+ if (!python_hooks || !cached->length || !*fragment->data
+ || !PyObject_HasAttrString(python_hooks, method))
+ return EVENT_HOOK_STATUS_NEXT;
+
+ python_ses = ses;
+
+ result = PyObject_CallMethod(python_hooks, method, "ss#", url,
+ fragment->data, fragment->length);
+ if (!result) goto error;
+
+ if (result != Py_None) {
+ unsigned char *str;
+ int len;
+
+ if (PyString_AsStringAndSize(result, (char **) &str, &len) != 0)
+ goto error;
+
+ (void) add_fragment(cached, 0, str, len);
+ normalize_cache_entry(cached, len);
+ }
+
+ success = 1;
+
+error:
+ if (!success) alert_python_error();
+
+ Py_XDECREF(result);
+
+ python_ses = saved_python_ses;
+
+ return EVENT_HOOK_STATUS_NEXT;
+}
+
+/* Call a Python hook for a get-proxy event. */
+
+static enum evhook_status
+script_hook_get_proxy(va_list ap, void *data)
+{
+ unsigned char **proxy = va_arg(ap, unsigned char **);
+ unsigned char *url = va_arg(ap, unsigned char *);
+ char *method = "proxy_for_hook";
+ PyObject *result;
+
+ evhook_use_params(proxy && url);
+
+ if (!python_hooks || !proxy || !url
+ || !PyObject_HasAttrString(python_hooks, method))
+ return EVENT_HOOK_STATUS_NEXT;
+
+ result = PyObject_CallMethod(python_hooks, method, "s", url);
+
+ if (!result || !replace_with_python_string(proxy, result))
+ alert_python_error();
+
+ Py_XDECREF(result);
+
+ return EVENT_HOOK_STATUS_NEXT;
+}
+
+/* Call a Python hook for a quit event. */
+
+static enum evhook_status
+script_hook_quit(va_list ap, void *data)
+{
+ char *method = "quit_hook";
+ PyObject *result;
+
+ if (!python_hooks || !PyObject_HasAttrString(python_hooks, method))
+ return EVENT_HOOK_STATUS_NEXT;
+
+ result = PyObject_CallMethod(python_hooks, method, NULL);
+ if (!result) alert_python_error();
+
+ Py_XDECREF(result);
+
+ return EVENT_HOOK_STATUS_NEXT;
+}
+
+struct event_hook_info python_scripting_hooks[] = {
+ { "goto-url", 0, script_hook_url, "goto_url_hook" },
+ { "follow-url", 0, script_hook_url, "follow_url_hook" },
+ { "pre-format-html", 0, script_hook_pre_format_html, NULL },
+ { "get-proxy", 0, script_hook_get_proxy, NULL },
+ { "quit", 0, script_hook_quit, NULL },
+ NULL_EVENT_HOOK_INFO,
+};
diff --git a/contrib/python/hooks.py b/contrib/python/hooks.py
index 34a751b..78caf03 100644
--- a/contrib/python/hooks.py
+++ b/contrib/python/hooks.py
@@ -1,60 +1,266 @@
-import re
-import sys
-
-dumbprefixes = {
- "7th" : "http://7thguard.net/",
- "b" : "http://babelfish.altavista.com/babelfish/tr/",
- "bz" : "http://bugzilla.elinks.cz/",
- "bug" : "http://bugzilla.elinks.cz/",
- "d" : "http://www.dict.org/",
- "g" : "http://www.google.com/",
- "gg" : "http://www.google.com/",
- "go" : "http://www.google.com/",
- "fm" : "http://www.freshmeat.net/",
- "sf" : "http://www.sourceforge.net/",
- "dbug" : "http://bugs.debian.org/",
- "dpkg" : "http://packages.debian.org/",
- "pycur" : "http://www.python.org/doc/current/",
- "pydev" : "http://www.python.org/dev/doc/devel/",
- "pyhelp" : "http://starship.python.net/crew/theller/pyhelp.cgi",
- "pyvault" : "http://www.vex.net/parnassus/",
- "e2" : "http://www.everything2.org/",
- "sd" : "http://www.slashdot.org/"
-}
-
-cygwin = re.compile("cygwin\.com")
-cygwin_sub1 = re.compile('<body bgcolor="#000000" color="#000000"')
-cygwin_sub2 = '<body bgcolor="#ffffff" color="#000000"'
-mbank = re.compile('^https://www\.mbank\.com\.pl/ib_navibar_3\.asp')
-mbank_sub1 = re.compile('<td valign="top"><img')
-mbank_sub2 = '<tr><td valign="top"><img'
-google_redirect = re.compile('^http://www\.google\.com/url\?sa=D&q=(.*)')
-
-def goto_url_hook(url, current_url):
- global dumbprefixes
-
- if dumbprefixes.has_key(url):
- return dumbprefixes[url];
- else:
- return None
-
-def follow_url_hook(url):
- m = google_redirect.search(url)
- if m:
- return m.group(1)
- return None
-
-def pre_format_html_hook(url, html):
- if cygwin.search(url):
- html2 = cygwin_sub1.sub(cygwin_sub2, html)
- return html2
- if mbank.search(url):
- html2 = mbank_sub1.sub(mbank_sub2, html)
- return html2
- return None
-
-def proxy_for_hook(url):
- return None
-
-def quit_hook():
- return None
+"""Example Python hooks for ELinks.
+
+If ELinks is compiled with an embedded Python interpreter, it will try
+to import a Python module called hooks when the browser starts up. To
+use Python code from within ELinks, create a file called hooks.py in
+the ~/.elinks directory, or in the system-wide configuration directory
+(defined when ELinks was compiled), or in the standard Python search path.
+An example hooks.py file can be found in the contrib/python directory of
+the ELinks source distribution.
+
+The hooks module may implement any of several functions that will be
+called automatically by ELinks in appropriate circumstances; it may also
+bind ELinks keystrokes to callable Python objects so that arbitrary Python
+code can be invoked at the whim of the user.
+
+Functions that will be automatically called by ELinks (if they're defined):
+
+follow_url_hook() -- Rewrite a URL for a link that's about to be followed.
+goto_url_hook() -- Rewrite a URL received from a "Go to URL" dialog box.
+pre_format_html_hook() -- Rewrite a document's body before it's formatted.
+proxy_for_hook() -- Determine what proxy server to use for a given URL.
+quit_hook() -- Clean up before ELinks exits.
+
+"""
+
+import elinks
+
+dumbprefixes = {
+ "7th" : "http://7thguard.net/",
+ "b" : "http://babelfish.altavista.com/babelfish/tr/",
+ "bz" : "http://bugzilla.elinks.cz/",
+ "bug" : "http://bugzilla.elinks.cz/",
+ "d" : "http://www.dict.org/",
+ "g" : "http://www.google.com/",
+ "gg" : "http://www.google.com/",
+ "go" : "http://www.google.com/",
+ "fm" : "http://www.freshmeat.net/",
+ "sf" : "http://www.sourceforge.net/",
+ "dbug" : "http://bugs.debian.org/",
+ "dpkg" : "http://packages.debian.org/",
+ "pycur" : "http://www.python.org/doc/current/",
+ "pydev" : "http://www.python.org/dev/doc/devel/",
+ "pyhelp" : "http://starship.python.net/crew/theller/pyhelp.cgi",
+ "pyvault" : "http://www.vex.net/parnassus/",
+ "e2" : "http://www.everything2.org/",
+ "sd" : "http://www.slashdot.org/"
+}
+
+def goto_url_hook(url, current_url):
+ """Rewrite a URL that was entered in a "Go to URL" dialog box.
+
+ This function should return a URL for ELinks to follow, or None if
+ ELinks should follow the original URL.
+
+ Arguments:
+
+ url -- The URL provided by the user.
+ current_url -- The URL of the document being viewed, or None if no
+ document is being viewed.
+
+ """
+ if url in dumbprefixes:
+ return dumbprefixes[url]
+
+def follow_url_hook(url):
+ """Rewrite a URL for a link that's about to be followed.
+
+ This function should return a URL for ELinks to follow, or None if
+ ELinks should follow the original URL.
+
+ Arguments:
+
+ url -- The URL of the link.
+
+ """
+ google_redirect = 'http://www.google.com/url?sa=D&q='
+ if url.startswith(google_redirect):
+ return url.replace(google_redirect, '')
+
+def pre_format_html_hook(url, html):
+ """Rewrite the body of a document before it's formatted.
+
+ This function should return a string for ELinks to format, or None
+ if ELinks should format the original document body. It can be used
+ to repair bad HTML, alter the layout or colors of a document, etc.
+
+ Arguments:
+
+ url -- The URL of the document.
+ html -- The body of the document.
+
+ """
+ if "cygwin.com" in url:
+ return html.replace('<body bgcolor="#000000" color="#000000"',
+ '<body bgcolor="#ffffff" color="#000000"')
+ elif url.startswith("https://www.mbank.com.pl/ib_navibar_3.asp"):
+ return html.replace('<td valign="top"><img',
+ '<tr><td valign="top"><img')
+
+def proxy_for_hook(url):
+ """Determine what proxy server to use for a given URL.
+
+ This function should return a string of the form "hostname:portnumber"
+ identifying a proxy server to use, or an empty string if the request
+ shouldn't use any proxy server, or None if ELinks should use its
+ default proxy server.
+
+ Arguments:
+
+ url -- The URL that is about to be followed.
+
+ """
+ pass
+
+def quit_hook():
+ """Clean up before ELinks exits.
+
+ This function should handle any clean-up tasks that need to be
+ performed before ELinks exits. Its return value is ignored.
+
+ """
+ pass
+
+
+# The rest of this file demonstrates some of the functionality available
+# within hooks.py through the embedded Python interpreter's elinks module.
+# Full documentation for the elinks module (as well as the hooks module)
+# can be found in doc/python.txt in the ELinks source distribution.
+
+
+# This class shows how to use elinks.input_box() and elinks.open(). It
+# creates a dialog box to prompt the user for a URL, and provides a callback
+# function that opens the URL in a new tab.
+#
+class goto_url_in_new_tab:
+ """Prompter that opens a given URL in a new tab."""
+ def __init__(self):
+ """Prompt for a URL."""
+ self.current_location = elinks.current_url()
+ elinks.input_box("Enter URL", self._callback, title="Go to URL")
+ def _callback(self, url):
+ """Open the given URL in a new tab."""
+ if 'goto_url_hook' in globals():
+ # Mimic the standard "Go to URL" dialog by calling goto_url_hook().
+ url = goto_url_hook(url, self.current_location) or url
+ if url:
+ elinks.open(url, new_tab=True)
+# The elinks.bind_key() function can be used to create a keystroke binding
+# that will instantiate the class.
+#
+elinks.bind_key("Ctrl-g", goto_url_in_new_tab)
+
+
+# This class can be used to enter arbitrary Python expressions for the embedded
+# interpreter to evaluate. (Note that it can only evalute Python expressions,
+# not execute arbitrary Python code.) The callback function will use
+# elinks.info_box() to display the results.
+#
+class simple_console:
+ """Simple console for passing expressions to the Python interpreter."""
+ def __init__(self):
+ """Prompt for a Python expression."""
+ elinks.input_box("Enter Python expression", self._callback, "Console")
+ def _callback(self, input):
+ """Evalute input and display the result (unless it's None)."""
+ if input is None:
+ # The user canceled.
+ return
+ result = eval(input)
+ if result is not None:
+ elinks.info_box(result, "Result")
+elinks.bind_key("F5", simple_console)
+
+
+# If you edit ~/.elinks/hooks.py while the browser is running, you can use
+# this function to make your changes take effect immediately (without having
+# to restart ELinks).
+#
+def reload_hooks():
+ """Reload the ELinks Python hooks."""
+ import hooks
+ reload(hooks)
+elinks.bind_key("F6", reload_hooks)
+
+
+# This example demonstrates how to create a menu by providing a sequence of
+# (string, function) tuples specifying each menu item.
+#
+def menu():
+ """Let the user choose from a menu of Python functions."""
+ items = (
+ ("~Go to URL in new tab", goto_url_in_new_tab),
+ ("Simple Python ~console", simple_console),
+ ("~Reload Python hooks", reload_hooks),
+ ("Read my favorite RSS/ATOM ~feeds", feedreader),
+ )
+ elinks.menu(items)
+elinks.bind_key("F4", menu)
+
+
+# Finally, a more elaborate demonstration: If you install the add-on Python
+# module from http://feedparser.org/ this class can use it to parse a list
+# of your favorite RSS/ATOM feeds, figure out which entries you haven't seen
+# yet, open each of those entries in a new tab, and report statistics on what
+# it fetched. The class is instantiated by pressing the "!" key.
+#
+# This class demonstrates the elinks.load() function, which can be used to
+# load a document into the ELinks cache without displaying it to the user;
+# the document's contents are passed to a Python callback function.
+
+my_favorite_feeds = (
+ "http://rss.gmane.org/messages/excerpts/gmane.comp.web.elinks.user",
+ # ... add any other RSS or ATOM feeds that interest you ...
+ # "http://elinks.cz/news.rss",
+ # "http://www.python.org/channews.rdf",
+)
+
+class feedreader:
+
+ """RSS/ATOM feed reader."""
+
+ def __init__(self, feeds=my_favorite_feeds):
+ """Constructor."""
+ self._results = {}
+ self._feeds = feeds
+ for feed in feeds:
+ elinks.load(feed, self._callback)
+
+ def _callback(self, header, body):
+ """Read a feed, identify unseen entries, and open them in new tabs."""
+ import anydbm
+ import feedparser # you need to get this module from feedparser.org
+ import os
+
+ if not body:
+ return
+ seen = anydbm.open(os.path.join(elinks.home, "rss.seen"), "c")
+ feed = feedparser.parse(body)
+ new = 0
+ errors = 0
+ for entry in feed.entries:
+ try:
+ if not seen.has_key(entry.link):
+ elinks.open(entry.link, new_tab=True)
+ seen[entry.link] = ""
+ new += 1
+ except:
+ errors += 1
+ seen.close()
+ self._tally(feed.channel.title, new, errors)
+
+ def _tally(self, title, new, errors):
+ """Record and report feed statistics."""
+ self._results[title] = (new, errors)
+ if len(self._results) == len(self._feeds):
+ feeds = self._results.keys()
+ feeds.sort()
+ width = max([len(title) for title in feeds])
+ fmt = "%*s new entries: %2d errors: %2d\n"
+ summary = ""
+ for title in feeds:
+ new, errors = self._results[title]
+ summary += fmt % (-width, title, new, errors)
+ elinks.info_box(summary, "Feed Statistics")
+
+elinks.bind_key("!", feedreader)
diff --git a/doc/python.txt b/doc/python.txt
new file mode 100644
index 0000000..a12b9b2
--- /dev/null
+++ b/doc/python.txt
@@ -0,0 +1,253 @@
+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
+ hooks - Python hooks for ELinks.
+
+DESCRIPTION
+ If ELinks is compiled with an embedded Python interpreter, it will try
+ to import a Python module called hooks when the browser starts up. To
+ use Python code from within ELinks, create a file called hooks.py in
+ the ~/.elinks directory, or in the system-wide configuration directory
+ (defined when ELinks was compiled), or in the standard Python search path.
+ An example hooks.py file can be found in the contrib/python directory of
+ the ELinks source distribution.
+
+ The hooks module may implement any of several functions that will be
+ called automatically by ELinks in appropriate circumstances; it may also
+ bind ELinks keystrokes to callable Python objects so that arbitrary Python
+ code can be invoked at the whim of the user.
+
+ Functions that will be automatically called by ELinks (if they're defined):
+
+ follow_url_hook() -- Rewrite a URL for a link that's about to be followed.
+ goto_url_hook() -- Rewrite a URL received from a "Go to URL" dialog box.
+ pre_format_html_hook() -- Rewrite a document's body before it's formatted.
+ proxy_for_hook() -- Determine what proxy server to use for a given URL.
+ quit_hook() -- Clean up before ELinks exits.
+
+FUNCTIONS
+ follow_url_hook(url)
+ Rewrite a URL for a link that's about to be followed.
+
+ This function should return a URL for ELinks to follow, or None if
+ ELinks should follow the original URL.
+
+ Arguments:
+
+ url -- The URL of the link.
+
+ goto_url_hook(url, current_url)
+ Rewrite a URL that was entered in a "Go to URL" dialog box.
+
+ This function should return a URL for ELinks to follow, or None if
+ ELinks should follow the original URL.
+
+ Arguments:
+
+ url -- The URL provided by the user.
+ current_url -- The URL of the document being viewed, or None if no
+ document is being viewed.
+
+ pre_format_html_hook(url, html)
+ Rewrite the body of a document before it's formatted.
+
+ This function should return a string for ELinks to format, or None
+ if ELinks should format the original document body. It can be used
+ to repair bad HTML, alter the layout or colors of a document, etc.
+
+ Arguments:
+
+ url -- The URL of the document.
+ html -- The body of the document.
+
+ proxy_for_hook(url)
+ Determine what proxy server to use for a given URL.
+
+ This function should return a string of the form "hostname:portnumber"
+ identifying a proxy server to use, or an empty string if the request
+ shouldn't use any proxy server, or None if ELinks should use its
+ default proxy server.
+
+ Arguments:
+
+ url -- The URL that is about to be followed.
+
+ quit_hook()
+ Clean up before ELinks exits.
+
+ This function should handle any clean-up tasks that need to be
+ performed before ELinks exits. Its return value is ignored.
+
+------------------------------------------------------------------------------
+
+MODULE
+ elinks - Interface to the ELinks web browser.
+
+DESCRIPTION
+ Functions:
+
+ bind_key() -- Bind a keystroke to a callable object.
+ current_document() -- Return the body of the document being viewed.
+ current_header() -- Return the header of the document being viewed.
+ current_link_url() -- Return the URL of the currently selected link.
+ current_title() -- Return the title of the document being viewed.
+ current_url() -- Return the URL of the document being viewed.
+ info_box() -- Display information to the user.
+ input_box() -- Prompt for user input.
+ load() -- Load a document into the ELinks cache.
+ menu() -- Display a menu.
+ open() -- View a document.
+
+ Exception classes:
+
+ error -- Errors internal to ELinks.
+
+ Other public objects:
+
+ home -- A string containing the pathname of the ~/.elinks directory.
+
+FUNCTIONS
+ bind_key(...)
+ bind_key(keystroke, callback[, keymap]) -> None
+
+ Bind a keystroke to a callable object.
+
+ Arguments:
+
+ keystroke -- A string containing a keystroke. The syntax for
+ keystrokes is described in the elinkskeys(5) man page.
+ callback -- A callable object to be called when the keystroke is
+ typed. It will be called without any arguments.
+
+ Optional arguments:
+
+ keymap -- A string containing the name of a keymap. Valid keymap
+ names can be found in the elinkskeys(5) man page. By
+ default the "main" keymap is used.
+
+ current_document(...)
+ current_document() -> string or None
+
+ If a document is being viewed, return its body; otherwise return None.
+
+ current_header(...)
+ current_header() -> string or None
+
+ If a document is being viewed and it has a header, return the header;
+ otherwise return None.
+
+ current_link_url(...)
+ current_link_url() -> string or None
+
+ If a link is selected, return its URL; otherwise return None.
+
+ current_title(...)
+ current_title() -> string or None
+
+ If a document is being viewed, return its title; otherwise return None.
+
+ current_url(...)
+ current_url() -> string or None
+
+ If a document is being viewed, return its URL; otherwise return None.
+
+ info_box(...)
+ info_box(text[, title]) -> None
+
+ Display information to the user in a dialog box.
+
+ Arguments:
+
+ text -- The text to be displayed in the dialog box. This argument can
+ be a string or any object that has a string representation as
+ returned by str(object).
+
+ Optional arguments:
+
+ title -- A string containing a title for the dialog box. By default
+ the string "Info" is used.
+
+ input_box(...)
+ input_box(prompt, callback, title="User dialog", initial="") -> None
+
+ Display a dialog box to prompt for user input.
+
+ Arguments:
+
+ prompt -- A string containing a prompt for the dialog box.
+ callback -- A callable object to be called after the dialog is
+ finished. It will be called with a single argument, which
+ will be either a string provided by the user or else None
+ if the user canceled the dialog.
+
+ Optional keyword arguments:
+
+ title -- A string containing a title for the dialog box. By default
+ the string "User dialog" is used.
+ initial -- A string containing an initial value for the text entry
+ field. By default the entry field is initially empty.
+
+ load(...)
+ load(url, callback) -> None
+
+ Load a document into the ELinks cache and pass its contents to a
+ callable object.
+
+ Arguments:
+
+ url -- A string containing the URL to load.
+ callback -- A callable object to be called after the document has
+ been loaded. It will be called with two arguments: the first
+ will be a string representing the document's header, or None
+ if it has no header; the second will be a string representing
+ the document's body, or None if it has no body.
+
+ menu(...)
+ menu(items[, type]) -> None
+
+ Display a menu.
+
+ Arguments:
+
+ items -- A sequence of tuples. Each tuple must have two elements: a
+ string containing the name of a menu item, and a callable
+ object that will be called without any arguments if the user
+ selects that menu item.
+
+ Optional arguments:
+
+ type -- A constant specifying the type of menu to display. By default
+ the menu is displayed at the top of the screen, but if this
+ argument's value is the constant elinks.MENU_TAB then the menu
+ is displayed in the same location as the ELinks tab menu. If
+ its value is the constant elinks.MENU_LINK then the menu is
+ displayed in the same location as the ELinks link menu and is
+ not displayed unless a link is currently selected.
+
+ open(...)
+ open(url, new_tab=False, background=False) -> None
+
+ View a document in either the current tab or a new tab.
+
+ Arguments:
+
+ url -- A string containing the URL to view.
+
+ Optional keyword arguments:
+
+ new_tab -- By default the URL is opened in the current tab. If this
+ argument's value is the boolean True then the URL is instead
+ opened in a new tab.
+ background -- By default a new tab is opened in the foreground. If
+ this argument's value is the boolean True then a new tab is
+ instead opened in the background. This argument is ignored
+ unless new_tab's value is True.
+
diff --git a/contrib/python/README.Python b/contrib/python/README.Python
deleted file mode 100644
index 862b60a..0000000
--- a/contrib/python/README.Python
+++ /dev/null
@@ -1,8 +0,0 @@
-If you want to use Python scripting with ELinks, add --with-python to the
-configure invocation and copy hooks.py to your ~/.elinks directory.
-
-If configure cannot find Python you can supply a path, e.g.
---with-python=/usr/local/bin if your Python binary is in /usr/local/bin, etc.
-
-For the present hooks.py is not very usable. You are welcome to make it better.
-Good Luck!
diff --git a/contrib/python/elinks_maint.py b/contrib/python/elinks_maint.py
new file mode 100644
index 0000000..b668977
--- /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 = {}