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/INSTALL.md b/INSTALL.md index e6db12e..e9dab58 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -20,6 +20,7 @@ | [OpenAL](http://openal.org) | AUDIO, SOUND NOTIFICATIONS | libopenal-dev | | [OpenALUT](http://openal.org) | SOUND NOTIFICATIONS | libalut-dev | | [LibNotify](https://developer.gnome.org/libnotify) | DESKTOP NOTIFICATIONS | libnotify-dev | +| [Python 3](http://www.python.org/) | PYTHON | python3-dev | | [AsciiDoc](http://asciidoc.org/index.html) | DOCUMENTATION1 | asciidoc | 1: see [Documentation](#documentation) @@ -55,6 +56,8 @@ Run `make doc` in the build directory after editing the asciidoc files to regene * `DISABLE_AV=1` → build toxic without audio call support * `DISABLE_SOUND_NOTIFY=1` → build toxic without sound notifications support * `DISABLE_DESKTOP_NOTIFY=1` → build toxic without desktop notifications support +* Features excluded from the default build must be explicitly enabled using special variables: + * `ENABLE_PYTHON=1` → build toxic with Python scripting support #### Packaging * For packaging purpose, you can use `DESTDIR=""` to specify a directory where to store installed files 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..7662c78 --- /dev/null +++ b/apidoc/python/source/fortune.py @@ -0,0 +1,37 @@ +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!") + return + + if count < 0 or count > 20: + toxic_api.display("Argument is too large!") + return + + 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..9fa4394 --- /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 a string representing the user's current status. Can be either "online", "away", or "busy". + + :rtype: string + +.. 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) + + Executes the given command. The API exports three constants for the class parameter; GLOBAL_COMMAND, CHAT_COMMAND, and 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/audio.mk b/cfg/checks/audio.mk index eceba9e..6e2cf84 100644 --- a/cfg/checks/audio.mk +++ b/cfg/checks/audio.mk @@ -18,4 +18,4 @@ else ifneq ($(MAKECMDGOALS), clean) $(warning WARNING -- Toxic will be compiled without audio support) $(warning WARNING -- You need these libraries for audio support) $(warning WARNING -- $(MISSING_AUDIO_LIBS)) -endif \ No newline at end of file +endif diff --git a/cfg/checks/check_features.mk b/cfg/checks/check_features.mk index f739cba..aa5990f 100644 --- a/cfg/checks/check_features.mk +++ b/cfg/checks/check_features.mk @@ -40,6 +40,12 @@ ifneq ($(QR_PNG), disabled) -include $(CHECKS_DIR)/qr_png.mk endif +# Check if we want build Python scripting support +PYTHON = $(shell if [ -z "$(ENABLE_PYTHON)" ] || [ "$(ENABLE_PYTHON)" = "0" ] ; then echo disabled ; else echo enabled ; fi) +ifneq ($(PYTHON), disabled) + -include $(CHECKS_DIR)/python.mk +endif + # Check if we can build Toxic CHECK_LIBS = $(shell $(PKG_CONFIG) --exists $(LIBS) || echo -n "error") ifneq ($(CHECK_LIBS), error) diff --git a/cfg/checks/python.mk b/cfg/checks/python.mk new file mode 100644 index 0000000..50a1623 --- /dev/null +++ b/cfg/checks/python.mk @@ -0,0 +1,15 @@ +# Variables for Python scripting support +PYTHON3_LIBS = python3 +PYTHON_CFLAGS = -DPYTHON +PYTHON_OBJ = api.o python_api.o + +# Check if we can build Python scripting support +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) + $(warning WARNING -- Toxic will be compiled without Python scripting support) + $(warning WARNING -- You need python3 installed for Python scripting support) +endif diff --git a/cfg/targets/help.mk b/cfg/targets/help.mk index b99c897..7a1f136 100644 --- a/cfg/targets/help.mk +++ b/cfg/targets/help.mk @@ -15,6 +15,7 @@ help: @echo " DISABLE_SOUND_NOTIFY: Set to \"1\" to force building without sound notification support" @echo " DISABLE_DESKTOP_NOTIFY: Set to \"1\" to force building without desktop notifications support" @echo " DISABLE_QRPNG: Set to \"1\" to force building without QR exported as PNG support" + @echo " ENABLE_PYTHON: Set to \"1\" to enable building with Python scripting support" @echo " USER_CFLAGS: Add custom flags to default CFLAGS" @echo " USER_LDFLAGS: Add custom flags to default LDFLAGS" @echo " PREFIX: Specify a prefix directory for binaries, data files,... (default is \"$(abspath $(PREFIX))\")" 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 new file mode 100644 index 0000000..c7ecd36 --- /dev/null +++ b/src/api.c @@ -0,0 +1,208 @@ +/* api.c + * + * + * Copyright (C) 2017 Jakob Kreuze + * + * This file is part of Toxic. + * + * Toxic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Toxic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Toxic. If not, see . + * + */ + +#include +#include + +#include + +#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 "toxic_strings.h" +#include "windows.h" + +Tox *user_tox; +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); +} + +FriendsList api_get_friendslist(void) +{ + return Friends; +} + +char *api_get_nick(void) +{ + size_t len = tox_self_get_name_size(user_tox); + uint8_t *name = malloc(len + 1); + + if (name == NULL) + return NULL; + + tox_self_get_name(user_tox, name); + name[len] = '\0'; + return (char *) name; +} + +TOX_USER_STATUS api_get_status(void) +{ + return tox_self_get_status(user_tox); +} + +char *api_get_status_message(void) +{ + size_t len = tox_self_get_status_message_size(user_tox); + uint8_t *status = malloc(len + 1); + + if (status == NULL) + return NULL; + + tox_self_get_status_message(user_tox, status); + status[len] = '\0'; + 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]; + + if (name == NULL) + return; + + self_window = get_active_window(); + get_time_str(timefrmt, sizeof(timefrmt)); + + strncpy((char *) self_window->chatwin->line, msg, sizeof(self_window->chatwin->line)); + add_line_to_hist(self_window->chatwin); + line_info_add(self_window, timefrmt, name, NULL, OUT_MSG, 0, 0, "%s", msg); + cqueue_add(self_window->chatwin->cqueue, msg, strlen(msg), OUT_MSG, + self_window->chatwin->hst->line_end->id + 1); + free(name); +} + +void api_execute(const char *input, int mode) +{ + self_window = get_active_window(); + execute(cur_window, self_window, user_tox, input, mode); +} + +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]) +{ + FILE *fp; + const char *error_str; + + cur_window = window; + self_window = self; + + if ( argc != 1 ) { + if ( argc < 1 ) error_str = "Path must be specified!"; + else error_str = "Only one argument allowed!"; + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, error_str); + return; + } + + fp = fopen(argv[1], "r"); + + if ( fp == NULL ) { + error_str = "Path does not exist!"; + + line_info_add(self, NULL, NULL, NULL, SYS_MSG, 0, 0, error_str); + return; + } + + run_python(fp, argv[1]); + fclose(fp); +} + +void invoke_autoruns(WINDOW *window, ToxWindow *self) +{ + struct dirent *dir; + char abspath_buf[PATH_MAX + 1], err_buf[PATH_MAX + 1]; + size_t path_len; + DIR *d; + FILE *fp; + + if (user_settings->autorun_path[0] == '\0') + return; + + d = opendir(user_settings->autorun_path); + + if (d == NULL) { + snprintf(err_buf, PATH_MAX + 1, "Autorun path does not exist: %s", user_settings->autorun_path); + api_display(err_buf); + 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) { + snprintf(err_buf, PATH_MAX + 1, "Invalid path: %s", abspath_buf); + api_display(err_buf); + continue; + } + + run_python(fp, abspath_buf); + fclose(fp); + } + } + + closedir(d); +} diff --git a/src/api.h b/src/api.h new file mode 100644 index 0000000..5875d01 --- /dev/null +++ b/src/api.h @@ -0,0 +1,42 @@ +/* api.h + * + * + * Copyright (C) 2017 Jakob Kreuze + * + * This file is part of Toxic. + * + * Toxic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Toxic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Toxic. If not, see . + * + */ + +#ifndef API_H +#define API_H + +#include "friendlist.h" +#include "windows.h" + +void api_display(const char *const msg); +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/autocomplete.c b/src/autocomplete.c index 52a1934..be66a33 100644 --- a/src/autocomplete.c +++ b/src/autocomplete.c @@ -110,6 +110,10 @@ int complete_line(ToxWindow *self, const void *list, int n_items, int size) bool dir_search = !strncmp(ubuf, "/sendfile", strlen("/sendfile")) || !strncmp(ubuf, "/avatar", strlen("/avatar")); + #ifdef PYTHON + dir_search = dir_search || !strncmp(ubuf, "/run", strlen("/run")); + #endif + /* isolate substring from space behind pos to pos */ char tmp[MAX_STR_SIZE]; snprintf(tmp, sizeof(tmp), "%s", ubuf); diff --git a/src/chat.c b/src/chat.c index 1838737..73cc0bd 100644 --- a/src/chat.c +++ b/src/chat.c @@ -65,8 +65,12 @@ static void init_infobox(ToxWindow *self); static void kill_infobox(ToxWindow *self); #endif /* AUDIO */ -#ifdef AUDIO +#if defined(AUDIO) && defined(PYTHON) +#define AC_NUM_CHAT_COMMANDS 31 +#elif AUDIO #define AC_NUM_CHAT_COMMANDS 30 +#elif PYTHON +#define AC_NUM_CHAT_COMMANDS 23 #else #define AC_NUM_CHAT_COMMANDS 22 #endif /* AUDIO */ @@ -108,6 +112,12 @@ static const char chat_cmd_list[AC_NUM_CHAT_COMMANDS][MAX_CMDNAME_SIZE] = { { "/video" }, #endif /* AUDIO */ + +#ifdef PYTHON + + { "/run" }, + +#endif /* PYTHON */ }; static void set_self_typingstatus(ToxWindow *self, Tox *m, bool is_typing) @@ -931,7 +941,15 @@ static void chat_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) diff = dir_match(self, m, ctx->line, L"/sendfile"); } else if (wcsncmp(ctx->line, L"/avatar \"", wcslen(L"/avatar \"")) == 0) { diff = dir_match(self, m, ctx->line, L"/avatar"); - } else if (wcsncmp(ctx->line, L"/status ", wcslen(L"/status ")) == 0) { + } + +#ifdef PYTHON + else if (wcsncmp(ctx->line, L"/run \"", wcslen(L"/run \"")) == 0) { + diff = dir_match(self, m, ctx->line, L"/run"); + } +#endif + + else if (wcsncmp(ctx->line, L"/status ", wcslen(L"/status ")) == 0) { const char status_cmd_list[3][8] = { {"online"}, {"away"}, diff --git a/src/execute.c b/src/execute.c index b0d3d9d..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; @@ -67,6 +68,9 @@ static struct cmd_func global_commands[] = { { "/lsvdev", cmd_list_video_devices }, { "/svdev" , cmd_change_video_device }, #endif /* VIDEO */ +#ifdef PYTHON + { "/run", cmd_run }, +#endif /* PYTHON */ { NULL, NULL }, }; @@ -193,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/global_commands.h b/src/global_commands.h index 8c5c3e7..bd9a519 100644 --- a/src/global_commands.h +++ b/src/global_commands.h @@ -56,4 +56,8 @@ void cmd_list_video_devices(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv) void cmd_change_video_device(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); #endif /* VIDEO */ +#ifdef PYTHON +void cmd_run(WINDOW *, ToxWindow *, Tox *, int argc, char (*argv)[MAX_STR_SIZE]); +#endif + #endif /* #define GLOBAL_COMMANDS_H */ diff --git a/src/groupchat.c b/src/groupchat.c index 04f13e2..c9119fc 100644 --- a/src/groupchat.c +++ b/src/groupchat.c @@ -69,8 +69,12 @@ static int max_groupchat_index = 0; extern struct user_settings *user_settings; extern struct Winthread Winthread; -#ifdef AUDIO +#if defined(AUDIO) && defined(PYTHON) +#define AC_NUM_GROUP_COMMANDS 25 +#elif AUDIO #define AC_NUM_GROUP_COMMANDS 24 +#elif PYTHON +#define AC_NUM_GROUP_COMMANDS 21 #else #define AC_NUM_GROUP_COMMANDS 20 #endif /* AUDIO */ @@ -97,6 +101,12 @@ static const char group_cmd_list[AC_NUM_GROUP_COMMANDS][MAX_CMDNAME_SIZE] = { { "/requests" }, { "/status" }, { "/title" }, + +#ifdef PYTHON + + { "/run" }, + +#endif /* PYTHON */ }; int init_groupchat_win(ToxWindow *prompt, Tox *m, uint32_t groupnum, uint8_t type) @@ -543,7 +553,15 @@ static void groupchat_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) TOX_MAX_NAME_LENGTH); } else if (wcsncmp(ctx->line, L"/avatar \"", wcslen(L"/avatar \"")) == 0) { diff = dir_match(self, m, ctx->line, L"/avatar"); - } else { + } + +#ifdef PYTHON + else if (wcsncmp(ctx->line, L"/run \"", wcslen(L"/run \"")) == 0) { + diff = dir_match(self, m, ctx->line, L"/run"); + } +#endif + + else { diff = complete_line(self, group_cmd_list, AC_NUM_GROUP_COMMANDS, MAX_CMDNAME_SIZE); } diff --git a/src/help.c b/src/help.c index 13b127c..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)); @@ -185,6 +197,14 @@ static void help_draw_global(ToxWindow *self) wprintw(win, " /svdev : Set active video device\n"); #endif /* VIDEO */ +#ifdef PYTHON + wattron(win, A_BOLD); + wprintw(win, "\n Scripting:\n"); + wattroff(win, A_BOLD); + + wprintw(win, " /run : Load and run the script at path\n"); +#endif /* PYTHON */ + help_draw_bottom_menu(win); box(win, ACS_VLINE, ACS_HLINE); @@ -278,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; @@ -302,6 +342,7 @@ static void help_draw_contacts(ToxWindow *self) void help_onKey(ToxWindow *self, wint_t key) { + int height; switch (key) { case 'x': case T_KEY_ESC: @@ -320,13 +361,16 @@ void help_onKey(ToxWindow *self, wint_t key) break; case 'g': + height = 22; #ifdef VIDEO - help_init_window(self, 30, 80); + height += 8; #elif AUDIO - help_init_window(self, 26, 80); -#else - help_init_window(self, 22, 80); + height += 4; #endif +#ifdef PYTHON + height += 2; +#endif + help_init_window(self, height, 80); self->help->type = HELP_GLOBAL; break; @@ -335,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; @@ -380,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/prompt.c b/src/prompt.c index ceb5062..66dd5da 100644 --- a/src/prompt.c +++ b/src/prompt.c @@ -49,10 +49,16 @@ extern struct Winthread Winthread; extern FriendsList Friends; FriendRequests FrndRequests; -#ifdef VIDEO +#if defined(PYTHON) && defined(VIDEO) +#define AC_NUM_GLOB_COMMANDS 23 +#elif defined(PYTHON) && defined(AUDIO) +#define AC_NUM_GLOB_COMMANDS 21 +#elif VIDEO #define AC_NUM_GLOB_COMMANDS 22 #elif AUDIO #define AC_NUM_GLOB_COMMANDS 20 +#elif PYTHON +#define AC_NUM_GLOB_COMMANDS 19 #else #define AC_NUM_GLOB_COMMANDS 18 #endif @@ -92,6 +98,12 @@ static const char glob_cmd_list[AC_NUM_GLOB_COMMANDS][MAX_CMDNAME_SIZE] = { #endif /* VIDEO */ +#ifdef PYTHON + + { "/run" }, + +#endif /* PYTHON */ + }; void kill_prompt_window(ToxWindow *self) @@ -214,6 +226,12 @@ static void prompt_onKey(ToxWindow *self, Tox *m, wint_t key, bool ltr) if (wcsncmp(ctx->line, L"/avatar \"", wcslen(L"/avatar \"")) == 0) diff = dir_match(self, m, ctx->line, L"/avatar"); + +#ifdef PYTHON + else if (wcsncmp(ctx->line, L"/run \"", wcslen(L"/run \"")) == 0) + diff = dir_match(self, m, ctx->line, L"/run"); +#endif + else if (wcsncmp(ctx->line, L"/status ", wcslen(L"/status ")) == 0) { const char status_cmd_list[3][8] = { {"online"}, diff --git a/src/python_api.c b/src/python_api.c new file mode 100644 index 0000000..cc35fbe --- /dev/null +++ b/src/python_api.c @@ -0,0 +1,345 @@ +/* python_api.c + * + * + * Copyright (C) 2017 Jakob Kreuze + * + * This file is part of Toxic. + * + * Toxic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Toxic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Toxic. If not, see . + * + */ + +#include + +#include "api.h" +#include "execute.h" + +extern Tox *user_tox; + +static 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) +{ + const char *msg; + + if (!PyArg_ParseTuple(args, "s", &msg)) + return NULL; + + api_display(msg); + return Py_None; +} + +static PyObject *python_api_get_nick(PyObject *self, PyObject *args) +{ + char *name; + PyObject *ret; + + if (!PyArg_ParseTuple(args, "")) + return NULL; + + name = api_get_nick(); + + if (name == NULL) + return NULL; + + ret = Py_BuildValue("s", name); + free(name); + return ret; +} + +static PyObject *python_api_get_status(PyObject *self, PyObject *args) +{ + PyObject *ret; + + if (!PyArg_ParseTuple(args, "")) + return NULL; + + switch (api_get_status()) { + case TOX_USER_STATUS_NONE: + ret = Py_BuildValue("s", "online"); + break; + + case TOX_USER_STATUS_AWAY: + ret = Py_BuildValue("s", "away"); + break; + + case TOX_USER_STATUS_BUSY: + ret = Py_BuildValue("s", "busy"); + break; + } + + return ret; +} + +static PyObject *python_api_get_status_message(PyObject *self, PyObject *args) +{ + char *status; + PyObject *ret; + + if (!PyArg_ParseTuple(args, "")) + return NULL; + + status = api_get_status_message(); + + if (status == NULL) + return NULL; + + ret = Py_BuildValue("s", status); + free(status); + 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; + const char *command; + + if (!PyArg_ParseTuple(args, "si", &command, &mode)) + return NULL; + + api_execute(command, mode); + 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, "Calback 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 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"}, + {"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, + -1, + ToxicApiMethods +}; + +PyMODINIT_FUNC PyInit_toxic_api(void) +{ + PyObject *m = PyModule_Create(&toxic_api_module); + PyObject *global_command_const = Py_BuildValue("i", GLOBAL_COMMAND_MODE); + PyObject *chat_command_const = Py_BuildValue("i", CHAT_COMMAND_MODE); + PyObject *groupchat_command_const = Py_BuildValue("i", GROUPCHAT_COMMAND_MODE); + PyObject_SetAttrString(m, "GLOBAL_COMMAND", global_command_const); + PyObject_SetAttrString(m, "CHAT_COMMAND", chat_command_const); + PyObject_SetAttrString(m, "GROUPCHAT_COMMAND", groupchat_command_const); + Py_DECREF(global_command_const); + Py_DECREF(chat_command_const); + Py_DECREF(groupchat_command_const); + return m; +} + +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_Finalize(); +} + +void init_python(Tox *m) +{ + user_tox = m; + PyImport_AppendInittab("toxic_api", PyInit_toxic_api); + Py_Initialize(); +} + +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 new file mode 100644 index 0000000..c9ff4c7 --- /dev/null +++ b/src/python_api.h @@ -0,0 +1,37 @@ +/* python_api.h + * + * + * Copyright (C) 2017 Jakob Kreuze + * + * This file is part of Toxic. + * + * Toxic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Toxic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Toxic. If not, see . + * + */ + +#ifndef PYTHON_API_H +#define PYTHON_API_H + +#include + +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..4fe1303 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", }; @@ -193,6 +195,7 @@ static void tox_defaults(struct user_settings *settings) strcpy(settings->download_path, ""); strcpy(settings->chatlogs_path, ""); strcpy(settings->avatar_path, ""); + strcpy(settings->autorun_path, ""); strcpy(settings->password_eval, ""); } @@ -418,6 +421,20 @@ 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 52ed7ca..d5f3ec1 100644 --- a/src/toxic.c +++ b/src/toxic.c @@ -75,6 +75,11 @@ ToxAV *av; #endif /* AUDIO */ +#ifdef PYTHON +#include "api.h" +#include "python_api.h" +#endif + #ifndef PACKAGE_DATADIR #define PACKAGE_DATADIR "." #endif @@ -169,6 +174,10 @@ void exit_toxic_success(Tox *m) terminate_audio(); #endif /* AUDIO */ +#ifdef PYTHON + terminate_python(); +#endif /* PYTHON */ + free_global_data(); tox_kill(m); endwin(); @@ -1218,6 +1227,13 @@ int main(int argc, char **argv) #endif /* AUDIO */ +#ifdef PYTHON + + init_python(m); + invoke_autoruns(prompt->chatwin->history, prompt); + +#endif /* PYTHON */ + init_notify(60, 3000); /* screen/tmux auto-away timer */ 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 */