From b3ed8bc35c6716b4828415ef65e54d89101a9a37 Mon Sep 17 00:00:00 2001 From: jakob Date: Tue, 16 May 2017 20:31:23 -0400 Subject: [PATCH] Finalized and documented the Python scripting interface. --- .gitignore | 1 + apidoc/python/Makefile | 20 ++++ apidoc/python/source/conf.py | 157 +++++++++++++++++++++++++++ apidoc/python/source/examples.rst | 8 ++ apidoc/python/source/fortune.py | 32 ++++++ apidoc/python/source/index.rst | 9 ++ apidoc/python/source/intro.rst | 12 +++ apidoc/python/source/reference.rst | 73 +++++++++++++ cfg/checks/python.mk | 10 +- doc/toxic.conf.5 | 11 +- doc/toxic.conf.5.asc | 3 + misc/toxic.conf.example | 4 +- src/api.c | 72 ++++++++++++- src/api.h | 8 +- src/execute.c | 6 ++ src/help.c | 45 ++++++++ src/help.h | 3 + src/python_api.c | 168 +++++++++++++++++++++++++++-- src/python_api.h | 6 +- src/settings.c | 14 +++ src/settings.h | 1 + src/toxic.c | 2 + src/windows.c | 6 ++ src/windows.h | 1 + 24 files changed, 649 insertions(+), 23 deletions(-) create mode 100644 apidoc/python/Makefile create mode 100644 apidoc/python/source/conf.py create mode 100644 apidoc/python/source/examples.rst create mode 100644 apidoc/python/source/fortune.py create mode 100644 apidoc/python/source/index.rst create mode 100644 apidoc/python/source/intro.rst create mode 100644 apidoc/python/source/reference.rst diff --git a/.gitignore b/.gitignore index 3ee8312..b1b9759 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ stamp-h1 build/toxic build/*.o build/*.d +apidoc/python/build diff --git a/apidoc/python/Makefile b/apidoc/python/Makefile new file mode 100644 index 0000000..57b5e76 --- /dev/null +++ b/apidoc/python/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = toxic_api +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/apidoc/python/source/conf.py b/apidoc/python/source/conf.py new file mode 100644 index 0000000..da3166b --- /dev/null +++ b/apidoc/python/source/conf.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# toxic_api documentation build configuration file, created by +# sphinx-quickstart on Tue May 16 08:58:24 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'toxic_api' +copyright = '2017, Jakob Kreuze' +author = 'Jakob Kreuze' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.7.2' +# The full version, including alpha/beta/rc tags. +release = '0.7.2' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'toxic_apidoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'toxic_api.tex', 'toxic\\_api Documentation', + 'Jakob Kreuze', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'toxic_api', 'toxic_api Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'toxic_api', 'toxic_api Documentation', + author, 'toxic_api', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/apidoc/python/source/examples.rst b/apidoc/python/source/examples.rst new file mode 100644 index 0000000..f1646d2 --- /dev/null +++ b/apidoc/python/source/examples.rst @@ -0,0 +1,8 @@ +============ +API Examples +============ + +Fortune +======= +.. literalinclude:: fortune.py + :language: python diff --git a/apidoc/python/source/fortune.py b/apidoc/python/source/fortune.py new file mode 100644 index 0000000..64ff4fd --- /dev/null +++ b/apidoc/python/source/fortune.py @@ -0,0 +1,32 @@ +import toxic_api +import random + +FORTUNES = [ + "A bug in the code is worth two in the documentation.", + "A bug in the hand is better than one as yet undetected.", + "\"A debugged program is one for which you have not yet found the " + "conditions that make it fail.\" -- Jerry Ogdin" +] + +def send_fortune(args): + """Callback function that sends the contact of the current window a + given number of random fortunes. + """ + if len(args) != 1: + toxic_api.display("Only one argument allowed!") + return + + try: + count = int(args[0]) + except ValueError: + toxic_api.display("Argument must be a number!") + + name = toxic_api.get_nick() + + toxic_api.send("%s has decided to send you %d fortunes:" % (name, count)) + for _ in range(count): + toxic_api.send(random.choice(FORTUNES)) + + +toxic_api.register("/fortune", "Send a fortune to the contact of the current " + "window", send_fortune) diff --git a/apidoc/python/source/index.rst b/apidoc/python/source/index.rst new file mode 100644 index 0000000..3252451 --- /dev/null +++ b/apidoc/python/source/index.rst @@ -0,0 +1,9 @@ +Toxic Scripting Interface Documentation +======================================= + +.. toctree:: + :maxdepth: 2 + + intro + reference + examples diff --git a/apidoc/python/source/intro.rst b/apidoc/python/source/intro.rst new file mode 100644 index 0000000..d3052f3 --- /dev/null +++ b/apidoc/python/source/intro.rst @@ -0,0 +1,12 @@ +========================= +Toxic Scripting Interface +========================= + +A Python scripting interface to `Toxic `_. + + +Getting Started +=============== +Toxic is compiled with Python support by default. To access the scripting interface, simply import "toxic_api" in your script. + +Scripts can be run by issuing "/run " from toxic, or placing them in the "autorun_path" from your toxic configuration file. diff --git a/apidoc/python/source/reference.rst b/apidoc/python/source/reference.rst new file mode 100644 index 0000000..18644b1 --- /dev/null +++ b/apidoc/python/source/reference.rst @@ -0,0 +1,73 @@ +============= +API Reference +============= + +Messages +======== +.. function:: display(msg) + + Display a message to the user through the current window. + + :param msg: The message to display. + :type msg: string + :rtype: none + +.. function:: send(msg) + + Send a message to the user specified by the currently open conversation. + + :param msg: The message to display. + :type msg: string + :rtype: none + + +State +===== +.. function:: get_nick() + + Return the user's current nickname. + + :rtype: string + +.. function:: get_status() + + Return the user's current status. 0 indicates online and available, 1 indicates away, and 2 indicates busy. + + :rtype: int + +.. function:: get_status_message() + + Return the user's current status message. + + :rtype: string + +.. function:: get_all_friends() + + Return a list of all the user's friends. + + :rtype: list of (string, string) tuples containing the nickname followed by their public key + + +Commands +======== +.. function:: execute(command, class) + + Execute the given command, where a class of 0 indicates a global command, 1 indicates a chat command, and 2 indicates a groupchat command. + + :param command: The command to execute. + :type command: string + :param class: The class of the command. + :type class: int + :rtype: none + +.. function:: register(command, help, callback) + + Register a callback to be executed whenever command is run. The callback function will be called with one argument, a list of arguments from when the user calls the command. + + :param command: The command to listen for. + :type command: string + :param help: A description of the command to be shown in the help menu. + :type help: string + :param callback: The function to be called. + :type callback: callable + :rtype: none diff --git a/cfg/checks/python.mk b/cfg/checks/python.mk index 35ff2f6..50a1623 100644 --- a/cfg/checks/python.mk +++ b/cfg/checks/python.mk @@ -1,17 +1,15 @@ # Variables for Python scripting support -PYTHON_LIBS = python3 +PYTHON3_LIBS = python3 PYTHON_CFLAGS = -DPYTHON PYTHON_OBJ = api.o python_api.o # Check if we can build Python scripting support -CHECK_PYTHON_LIBS = $(shell $(PKG_CONFIG) --exists $(PYTHON_LIBS) || echo -n "error") -ifneq ($(CHECK_PYTHON_LIBS), error) +CHECK_PYTHON3_LIBS = $(shell $(PKG_CONFIG) --exists $(PYTHON3_LIBS) || echo -n "error") +ifneq ($(CHECK_PYTHON3_LIBS), error) LDFLAGS += $(shell python3-config --ldflags) CFLAGS += $(PYTHON_CFLAGS) $(shell python3-config --includes) OBJ += $(PYTHON_OBJ) else ifneq ($(MAKECMDGOALS), clean) - MISSING_AUDIO_LIBS = $(shell for lib in $(PYTHON_LIBS) ; do if ! $(PKG_CONFIG) --exists $$lib ; then echo $$lib ; fi ; done) $(warning WARNING -- Toxic will be compiled without Python scripting support) - $(warning WARNING -- You need these libraries for Python scripting support) - $(warning WARNING -- $(MISSING_AUDIO_LIBS)) + $(warning WARNING -- You need python3 installed for Python scripting support) endif diff --git a/doc/toxic.conf.5 b/doc/toxic.conf.5 index 7af3bf5..85f338c 100644 --- a/doc/toxic.conf.5 +++ b/doc/toxic.conf.5 @@ -1,13 +1,13 @@ '\" t .\" Title: toxic.conf .\" Author: [see the "AUTHORS" section] -.\" Generator: DocBook XSL Stylesheets v1.78.1 -.\" Date: 2016-07-21 +.\" Generator: DocBook XSL Stylesheets v1.79.1 +.\" Date: 2016-09-20 .\" Manual: Toxic Manual .\" Source: toxic __VERSION__ .\" Language: English .\" -.TH "TOXIC\&.CONF" "5" "2016\-07\-21" "toxic __VERSION__" "Toxic Manual" +.TH "TOXIC\&.CONF" "5" "2016\-09\-20" "toxic __VERSION__" "Toxic Manual" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -227,6 +227,11 @@ Default path for downloads\&. String value\&. Absolute path for downloaded files Path for your avatar (file must be a \&.png and cannot exceed 16\&.3 KiB) .RE .PP +\fBautorun_path\fR +.RS 4 +Path for any scripts that should be run on startup +.RE +.PP \fBchatlogs_path\fR .RS 4 Default path for chatlogs\&. String value\&. Absolute path for chatlog files\&. diff --git a/doc/toxic.conf.5.asc b/doc/toxic.conf.5.asc index dd931ec..f11e705 100644 --- a/doc/toxic.conf.5.asc +++ b/doc/toxic.conf.5.asc @@ -143,6 +143,9 @@ OPTIONS *avatar_path*;; Path for your avatar (file must be a .png and cannot exceed 16.3 KiB) + *autorun_path*;; + Path for any scripts that should be run on startup + *chatlogs_path*;; Default path for chatlogs. String value. Absolute path for chatlog files. diff --git a/misc/toxic.conf.example b/misc/toxic.conf.example index 9e5242b..cffe2c0 100644 --- a/misc/toxic.conf.example +++ b/misc/toxic.conf.example @@ -87,6 +87,9 @@ tox = { // Path for your avatar (file must be a .png and cannot exceed 64 KiB) // avatar_path="/home/USERNAME/Pictures/youravatar.png"; + // Path for scripts that should be run on startup + // autorun_path="/home/USERNAME/toxic_scripts/"; + // Path for chatlogs // chatlogs_path="/home/USERNAME/toxic_chatlogs/"; }; @@ -118,4 +121,3 @@ keys = { toggle_peerlist="Ctrl+b"; toggle_paste_mode="Ctrl+T"; }; - diff --git a/src/api.c b/src/api.c index 2948536..da23ce1 100644 --- a/src/api.c +++ b/src/api.c @@ -1,7 +1,7 @@ /* api.c * * - * Copyright (C) 2017 Toxic All Rights Reserved. + * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * @@ -20,6 +20,7 @@ * */ +#include #include #include @@ -27,7 +28,10 @@ #include "execute.h" #include "friendlist.h" #include "line_info.h" +#include "message_queue.h" +#include "misc_tools.h" #include "python_api.h" +#include "settings.h" #include "windows.h" Tox *user_tox; @@ -35,12 +39,13 @@ static WINDOW *cur_window; static ToxWindow *self_window; extern FriendsList Friends; +extern struct user_settings *user_settings; void api_display(const char * const msg) { if (msg == NULL) return; - + self_window = get_active_window(); line_info_add(self_window, NULL, NULL, NULL, SYS_MSG, 0, 0, msg); } @@ -56,6 +61,7 @@ char *api_get_nick(void) if (name == NULL) return NULL; tox_self_get_name(user_tox, name); + name[len] = '\0'; return (char *) name; } @@ -74,12 +80,45 @@ char *api_get_status_message(void) return (char *) status; } +void api_send(const char *msg) +{ + if (msg == NULL || self_window->chatwin->cqueue == NULL) + return; + char *name = api_get_nick(); + char timefrmt[TIME_STR_SIZE]; + get_time_str(timefrmt, sizeof(timefrmt)); + self_window = get_active_window(); + line_info_add(self_window, timefrmt, name, NULL, OUT_MSG, 0, 0, "%s", msg); + free(name); + cqueue_add(self_window->chatwin->cqueue, msg, strlen(msg), OUT_MSG, + self_window->chatwin->hst->line_end->id + 1); +} + void api_execute(const char *input, int mode) { + self_window = get_active_window(); execute(cur_window, self_window, user_tox, input, mode); } -/* TODO: Register command */ +int do_plugin_command(int num_args, char (*args)[MAX_STR_SIZE]) +{ + return do_python_command(num_args, args); +} + +int num_registered_handlers(void) +{ + return python_num_registered_handlers(); +} + +int help_max_width(void) +{ + return python_help_max_width(); +} + +void draw_handler_help(WINDOW *win) +{ + python_draw_handler_help(win); +} void cmd_run(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX_STR_SIZE]) { @@ -107,3 +146,30 @@ void cmd_run(WINDOW *window, ToxWindow *self, Tox *m, int argc, char (*argv)[MAX run_python(fp, argv[1]); fclose(fp); } + +void invoke_autoruns(WINDOW *window, ToxWindow *self) +{ + struct dirent *dir; + char abspath_buf[PATH_MAX + 1]; + size_t path_len; + DIR *d = opendir(user_settings->autorun_path); + FILE *fp; + if (d == NULL) + return; + + cur_window = window; + self_window = self; + + while ((dir = readdir(d)) != NULL) { + path_len = strlen(dir->d_name); + if (!strcmp(dir->d_name + path_len - 3, ".py")) { + snprintf(abspath_buf, PATH_MAX + 1, "%s%s", user_settings->autorun_path, dir->d_name); + fp = fopen(abspath_buf, "r"); + if (fp == NULL) + continue; + run_python(fp, abspath_buf); + fclose(fp); + } + } + closedir(d); +} diff --git a/src/api.h b/src/api.h index 3c7b51a..b0d137f 100644 --- a/src/api.h +++ b/src/api.h @@ -1,7 +1,7 @@ /* api.h * * - * Copyright (C) 2014 Toxic All Rights Reserved. + * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * @@ -31,6 +31,12 @@ FriendsList api_get_friendslist(void); char *api_get_nick(void); TOX_USER_STATUS api_get_status(void); char *api_get_status_message(void); +void api_send(const char *msg); void api_execute(const char *input, int mode); +int do_plugin_command(int num_args, char (*args)[MAX_STR_SIZE]); +int num_registered_handlers(void); +int help_max_width(void); +void draw_handler_help(WINDOW *win); +void invoke_autoruns(WINDOW *w, ToxWindow *self); #endif /* #define API_H */ diff --git a/src/execute.c b/src/execute.c index 6b0f4c2..607ad4d 100644 --- a/src/execute.c +++ b/src/execute.c @@ -33,6 +33,7 @@ #include "line_info.h" #include "misc_tools.h" #include "notify.h" +#include "api.h" struct cmd_func { const char *name; @@ -196,5 +197,10 @@ void execute(WINDOW *w, ToxWindow *self, Tox *m, const char *input, int mode) if (do_command(w, self, m, num_args, global_commands, args) == 0) return; +#ifdef PYTHON + if (do_plugin_command(num_args, args) == 0) + return; +#endif + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, "Invalid command."); } diff --git a/src/help.c b/src/help.c index b8c193e..9110064 100644 --- a/src/help.c +++ b/src/help.c @@ -26,8 +26,13 @@ #include "toxic.h" #include "help.h" #include "misc_tools.h" +#include "api.h" +#ifdef PYTHON +#define HELP_MENU_HEIGHT 10 +#else #define HELP_MENU_HEIGHT 9 +#endif /* PYTHON */ #define HELP_MENU_WIDTH 26 void help_init_menu(ToxWindow *self) @@ -95,6 +100,13 @@ static void help_draw_menu(ToxWindow *self) wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, "oup commands\n"); +#ifdef PYTHON + wattron(win, A_BOLD | COLOR_PAIR(BLUE)); + wprintw(win, " p"); + wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); + wprintw(win, "lugin commands\n"); +#endif /* PYTHON */ + wattron(win, A_BOLD | COLOR_PAIR(BLUE)); wprintw(win, " f"); wattroff(win, A_BOLD | COLOR_PAIR(BLUE)); @@ -286,6 +298,26 @@ static void help_draw_group(ToxWindow *self) wrefresh(win); } +#ifdef PYTHON +static void help_draw_plugin(ToxWindow *self) +{ + WINDOW *win = self->help->win; + + wmove(win, 1, 1); + + wattron(win, A_BOLD | COLOR_PAIR(RED)); + wprintw(win, "Plugin commands:\n"); + wattroff(win, A_BOLD | COLOR_PAIR(RED)); + + draw_handler_help(win); + + help_draw_bottom_menu(win); + + box(win, ACS_VLINE, ACS_HLINE); + wrefresh(win); +} +#endif + static void help_draw_contacts(ToxWindow *self) { WINDOW *win = self->help->win; @@ -347,6 +379,13 @@ void help_onKey(ToxWindow *self, wint_t key) self->help->type = HELP_GROUP; break; +#ifdef PYTHON + case 'p': + help_init_window(self, 4 + num_registered_handlers(), help_max_width()); + self->help->type = HELP_PLUGIN; + break; +#endif + case 'f': help_init_window(self, 10, 80); self->help->type = HELP_CONTACTS; @@ -392,5 +431,11 @@ void help_onDraw(ToxWindow *self) case HELP_GROUP: help_draw_group(self); break; + +#ifdef PYTHON + case HELP_PLUGIN: + help_draw_plugin(self); + break; +#endif /* PYTHON */ } } diff --git a/src/help.h b/src/help.h index e5680f2..b33ffba 100644 --- a/src/help.h +++ b/src/help.h @@ -33,6 +33,9 @@ typedef enum { HELP_GROUP, HELP_KEYS, HELP_CONTACTS, +#ifdef PYTHON + HELP_PLUGIN, +#endif } HELP_TYPES; void help_onDraw(ToxWindow *self); diff --git a/src/python_api.c b/src/python_api.c index a5ccfb2..34960f1 100644 --- a/src/python_api.c +++ b/src/python_api.c @@ -1,7 +1,7 @@ /* python_api.c * * - * Copyright (C) 2017 Toxic All Rights Reserved. + * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * @@ -24,7 +24,14 @@ #include "api.h" -extern Tox *user_tox; +extern Tox *user_tox; + +struct python_registered_func { + char *name; + char *help; + PyObject *callback; + struct python_registered_func *next; +} python_commands = {0}; static PyObject *python_api_display(PyObject *self, PyObject *args) { @@ -74,6 +81,35 @@ static PyObject *python_api_get_status_message(PyObject *self, PyObject *args) return ret; } +static PyObject *python_api_get_all_friends(PyObject *self, PyObject *args) +{ + size_t i, ii; + FriendsList friends; + PyObject *cur, *ret; + char pubkey_buf[TOX_PUBLIC_KEY_SIZE * 2 + 1]; + if (!PyArg_ParseTuple(args, "")) + return NULL; + friends = api_get_friendslist(); + ret = PyList_New(0); + for (i = 0; i < friends.num_friends; i++) { + for (ii = 0; ii < TOX_PUBLIC_KEY_SIZE; ii++) + snprintf(pubkey_buf + ii * 2, 3, "%02X", friends.list[i].pub_key[ii] & 0xff); + pubkey_buf[TOX_PUBLIC_KEY_SIZE * 2] = '\0'; + cur = Py_BuildValue("(s,s)", friends.list[i].name, pubkey_buf); + PyList_Append(ret, cur); + } + return ret; +} + +static PyObject *python_api_send(PyObject *self, PyObject *args) +{ + const char *msg; + if (!PyArg_ParseTuple(args, "s", &msg)) + return NULL; + api_send(msg); + return Py_None; +} + static PyObject *python_api_execute(PyObject *self, PyObject *args) { int mode; @@ -84,19 +120,70 @@ static PyObject *python_api_execute(PyObject *self, PyObject *args) return Py_None; } +static PyObject *python_api_register(PyObject *self, PyObject *args) +{ + struct python_registered_func *cur; + size_t command_len, help_len; + const char *command, *help; + PyObject *callback; + if (!PyArg_ParseTuple(args, "ssO:register_command", &command, &help, &callback)) + return NULL; + if (!PyCallable_Check(callback)) { + PyErr_SetString(PyExc_TypeError, "Parameter must be callable"); + return NULL; + } + if (command[0] != '/') { + PyErr_SetString(PyExc_TypeError, "Command must be prefixed with a '/'"); + return NULL; + } + for (cur = &python_commands; ; cur = cur->next) { + if (cur->name != NULL && !strcmp(command, cur->name)) { + Py_XDECREF(cur->callback); + Py_XINCREF(callback); + cur->callback = callback; + break; + } + if (cur->next == NULL) { + Py_XINCREF(callback); + cur->next = malloc(sizeof(struct python_registered_func)); + if (cur->next == NULL) + return PyErr_NoMemory(); + command_len = strlen(command); + cur->next->name = malloc(command_len + 1); + if (cur->next->name == NULL) + return PyErr_NoMemory(); + strncpy(cur->next->name, command, command_len + 1); + help_len = strlen(help); + cur->next->help = malloc(help_len + 1); + if (cur->next->help == NULL) + return PyErr_NoMemory(); + strncpy(cur->next->help, help, help_len + 1); + cur->next->callback = callback; + cur->next->next = NULL; + break; + } + } + Py_INCREF(Py_None); + return Py_None; +} + static PyMethodDef ToxicApiMethods[] = { - {"display", python_api_display, METH_VARARGS, "Display a message to the primary prompt"}, - {"get_nick", python_api_get_nick, METH_VARARGS, "Return the user's current nickname"}, + {"display", python_api_display, METH_VARARGS, "Display a message to the current prompt"}, + {"get_nick", python_api_get_nick, METH_VARARGS, "Return the user's current nickname"}, + {"get_status", python_api_get_status, METH_VARARGS, "Returns the user's current status"}, {"get_status_message", python_api_get_status_message, METH_VARARGS, "Return the user's current status message"}, - {"execute", python_api_execute, METH_VARARGS, "Execute a command like `/nick`"}, - {NULL, NULL, 0, NULL}, + {"get_all_friends", python_api_get_all_friends, METH_VARARGS, "Return all of the user's friends"}, + {"send", python_api_send, METH_VARARGS, "Send the message to the current user"}, + {"execute", python_api_execute, METH_VARARGS, "Execute a command like `/nick`"}, + {"register", python_api_register, METH_VARARGS, "Register a command like `/nick` to a Python function"}, + {NULL, NULL, 0, NULL}, }; static struct PyModuleDef toxic_api_module = { PyModuleDef_HEAD_INIT, "toxic_api", - NULL, /* TODO: Module documentation. */ - -1, /* TODO: Assumption that no per-interpreter state is maintained. */ + NULL, + -1, ToxicApiMethods }; @@ -107,6 +194,15 @@ PyMODINIT_FUNC PyInit_toxic_api(void) void terminate_python(void) { + struct python_registered_func *cur, *old; + if (python_commands.name != NULL) + free(python_commands.name); + for (cur = python_commands.next; cur != NULL;) { + old = cur; + cur = cur->next; + free(old->name); + free(old); + } Py_FinalizeEx(); } @@ -124,3 +220,59 @@ void run_python(FILE *fp, char *path) { PyRun_SimpleFile(fp, path); } + +int do_python_command(int num_args, char (*args)[MAX_STR_SIZE]) +{ + int i; + PyObject *callback_args, *args_strings; + struct python_registered_func *cur; + for (cur = &python_commands; cur != NULL; cur = cur->next) { + if (cur->name == NULL) + continue; + if (!strcmp(args[0], cur->name)) { + args_strings = PyList_New(0); + for (i = 1; i < num_args; i++) + PyList_Append(args_strings, Py_BuildValue("s", args[i])); + callback_args = PyTuple_Pack(1, args_strings); + if (PyObject_CallObject(cur->callback, callback_args) == NULL) + api_display("Exception raised in callback function"); + return 0; + } + } + return 1; +} + +int python_num_registered_handlers(void) +{ + int n = 0; + struct python_registered_func *cur; + for (cur = &python_commands; cur != NULL; cur = cur->next) { + if (cur->name != NULL) + n++; + } + return n; +} + +int python_help_max_width(void) +{ + size_t tmp; + int max = 0; + struct python_registered_func *cur; + for (cur = &python_commands; cur != NULL; cur = cur->next) { + if (cur->name != NULL) { + tmp = strlen(cur->help); + max = tmp > max ? tmp : max; + } + } + max = max > 50 ? 50 : max; + return 37 + max; +} + +void python_draw_handler_help(WINDOW *win) +{ + struct python_registered_func *cur; + for (cur = &python_commands; cur != NULL; cur = cur->next) { + if (cur->name != NULL) + wprintw(win, " %-29s: %.50s\n", cur->name, cur->help); + } +} diff --git a/src/python_api.h b/src/python_api.h index e0585a4..c9ff4c7 100644 --- a/src/python_api.h +++ b/src/python_api.h @@ -1,7 +1,7 @@ /* python_api.h * * - * Copyright (C) 2017 Toxic All Rights Reserved. + * Copyright (C) 2017 Jakob Kreuze * * This file is part of Toxic. * @@ -29,5 +29,9 @@ PyMODINIT_FUNC PyInit_toxic_api(void); void terminate_python(void); void init_python(Tox *m); void run_python(FILE *fp, char *path); +int do_python_command(int num_args, char (*args)[MAX_STR_SIZE]); +int python_num_registered_handlers(void); +int python_help_max_width(void); +void python_draw_handler_help(WINDOW *win); #endif /* #define PYTHON_API_H */ diff --git a/src/settings.c b/src/settings.c index 6979337..4a00b07 100644 --- a/src/settings.c +++ b/src/settings.c @@ -179,12 +179,14 @@ static const struct tox_strings { const char *download_path; const char *chatlogs_path; const char *avatar_path; + const char *autorun_path; const char *password_eval; } tox_strings = { "tox", "download_path", "chatlogs_path", "avatar_path", + "autorun_path", "password_eval", }; @@ -418,6 +420,18 @@ int settings_load(struct user_settings *s, const char *patharg) s->avatar_path[0] = '\0'; } +#ifdef PYTHON + if ( config_setting_lookup_string(setting, tox_strings.autorun_path, &str) ) { + snprintf(s->autorun_path, sizeof(s->autorun_path), "%s", str); + int len = strlen(str); + + if (len >= sizeof(s->autorun_path) - 2) + s->autorun_path[0] = '\0'; + else if (s->autorun_path[len - 1] != '/') + strcat(&s->autorun_path[len - 1], "/"); + } +#endif + if ( config_setting_lookup_string(setting, tox_strings.password_eval, &str) ) { snprintf(s->password_eval, sizeof(s->password_eval), "%s", str); int len = strlen(str); diff --git a/src/settings.h b/src/settings.h index 91228a7..bfaf708 100644 --- a/src/settings.h +++ b/src/settings.h @@ -63,6 +63,7 @@ struct user_settings { char download_path[PATH_MAX]; char chatlogs_path[PATH_MAX]; char avatar_path[PATH_MAX]; + char autorun_path[PATH_MAX]; char password_eval[PASSWORD_EVAL_MAX]; int key_next_tab; diff --git a/src/toxic.c b/src/toxic.c index 2cd2493..7ba5ecf 100644 --- a/src/toxic.c +++ b/src/toxic.c @@ -76,6 +76,7 @@ ToxAV *av; #endif /* AUDIO */ #ifdef PYTHON +#include "api.h" #include "python_api.h" #endif @@ -1222,6 +1223,7 @@ int main(int argc, char **argv) #ifdef PYTHON init_python(m); + invoke_autoruns(prompt->chatwin->history, prompt); #endif /* PYTHON */ diff --git a/src/windows.c b/src/windows.c index 0475639..151e2dc 100644 --- a/src/windows.c +++ b/src/windows.c @@ -584,6 +584,12 @@ ToxWindow *get_window_ptr(int i) return toxwin; } +/* returns a pointer to the currently open ToxWindow. */ +ToxWindow *get_active_window(void) +{ + return active_window; +} + void force_refresh(WINDOW *w) { wclear(w); diff --git a/src/windows.h b/src/windows.h index 06d1abd..9371d99 100644 --- a/src/windows.h +++ b/src/windows.h @@ -259,6 +259,7 @@ void kill_all_windows(Tox *m); /* should only be called on shutdown */ void on_window_resize(void); void force_refresh(WINDOW *w); ToxWindow *get_window_ptr(int i); +ToxWindow *get_active_window(void); /* refresh inactive windows to prevent scrolling bugs. call at least once per second */