diff --git a/kitty/boss.py b/kitty/boss.py index 859f7fabb..1a5628463 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -24,7 +24,7 @@ from .constants import ( from .fast_data_types import ( GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA, GLFW_CURSOR, GLFW_CURSOR_HIDDEN, GLFW_CURSOR_NORMAL, GLFW_MOUSE_BUTTON_1, GLFW_PRESS, GLFW_REPEAT, - drain_read, glBlendFunc, glfw_post_empty_event, glViewport + drain_read, glBlendFunc, glfw_post_empty_event, glViewport, Timers ) from .fonts.render import set_font_family from .keys import ( @@ -33,7 +33,6 @@ from .keys import ( from .session import create_session from .shaders import Sprites from .tabs import SpecialWindow, TabManager -from .timers import Timers from .utils import handle_unix_signals, pipe2, safe_print if isosx: @@ -203,7 +202,7 @@ class Boss(Thread): self.read_dispatch_map[r]() for w in writers: self.write_dispatch_map[w]() - self.timers() + self.timers.call() for w in self.iterwindows(): if w.screen.is_dirty(): self.timers.add_if_missing( @@ -214,7 +213,7 @@ class Boss(Thread): # debounce resize events self.pending_resize = True yield - self.timers.add(0.02, self.apply_pending_resize, w, h) + self.timers.add(0.02, self.apply_pending_resize, (w, h)) def apply_pending_resize(self, w, h): if w > 100 and h > 100: diff --git a/kitty/data-types.c b/kitty/data-types.c index 8d35f8b4d..bdaab92e1 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -110,6 +110,7 @@ PyInit_fast_data_types(void) { if (!init_HistoryBuf(m)) return NULL; if (!init_Line(m)) return NULL; if (!init_Cursor(m)) return NULL; + if (!init_Timers(m)) return NULL; if (!init_ColorProfile(m)) return NULL; if (!init_SpriteMap(m)) return NULL; if (!init_ChangeTracker(m)) return NULL; diff --git a/kitty/data-types.h b/kitty/data-types.h index 89c7b8842..a3d30eefc 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -299,6 +299,21 @@ typedef struct { } Screen; PyTypeObject Screen_Type; +typedef struct { + double at; + PyObject *callback; + PyObject *args; +} TimerEvent; + +typedef struct { + PyObject_HEAD + + TimerEvent *events, *buf1, *buf2; + size_t capacity; + size_t count; +} Timers; +PyTypeObject Timers_Type; + #define left_shift_line(line, at, num) \ for(index_type __i__ = (at); __i__ < (line)->xnum - (num); __i__++) { \ COPY_CELL(line, __i__ + (num), line, __i__) \ @@ -315,6 +330,7 @@ ChangeTracker* alloc_change_tracker(unsigned int, unsigned int); int init_LineBuf(PyObject *); int init_HistoryBuf(PyObject *); int init_Cursor(PyObject *); +int init_Timers(PyObject *); int init_Line(PyObject *); int init_ColorProfile(PyObject *); int init_SpriteMap(PyObject *); diff --git a/kitty/main.py b/kitty/main.py index b851c6150..ff1fea48c 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -178,7 +178,7 @@ def dispatch_pending_calls(boss): except Exception: import traceback safe_print(traceback.format_exc()) - boss.ui_timers() + boss.ui_timers.call() def run_app(opts, args): diff --git a/kitty/timers.c b/kitty/timers.c new file mode 100644 index 000000000..47563c926 --- /dev/null +++ b/kitty/timers.c @@ -0,0 +1,177 @@ +/* + * timers.c + * Copyright (C) 2017 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "data-types.h" +#include +#include + +static PyObject * +new(PyTypeObject *type, PyObject UNUSED *args, PyObject UNUSED *kwds) { + Timers *self; + + self = (Timers *)type->tp_alloc(type, 0); + self->capacity = 1024; + self->count = 0; + self->buf1 = (TimerEvent*)PyMem_Calloc(2 * self->capacity, sizeof(TimerEvent)); + if (self->buf1 == NULL) { Py_CLEAR(self); return PyErr_NoMemory(); } + self->events = self->buf1; + self->buf2 = self->buf1 + self->capacity; + return (PyObject*) self; +} + +static void +dealloc(Timers* self) { + if (self->events) { + for (size_t i = 0; i < self->count; i++) { + Py_CLEAR(self->events[i].callback); Py_CLEAR(self->events[i].args); + } + PyMem_Free(self->events); self->events = NULL; + } + Py_TYPE(self)->tp_free((PyObject*)self); +} + + +static int +compare_events(const void *a, const void *b) { + double av = ((TimerEvent*)(a))->at, bv = ((TimerEvent*)(b))->at; + return av > bv ? 1 : (av == bv ? 0 : -1); +} + + +static inline PyObject* +_add(Timers *self, double at, PyObject *callback, PyObject *args) { + size_t i; + if (self->count >= self->capacity) { + PyErr_SetString(PyExc_ValueError, "Too many timers"); + return NULL; + } + i = self->count++; + self->events[i].at = at; self->events[i].callback = callback; self->events[i].args = args; + Py_INCREF(callback); Py_XINCREF(args); + qsort(self->events, self->count, sizeof(TimerEvent), compare_events); + Py_RETURN_NONE; +} + + +static PyObject * +add(Timers *self, PyObject *fargs) { +#define add_doc "add(delay, callback, args) -> Add callback, replacing it if it already exists" + size_t i; + bool added = false; + PyObject *callback, *args = NULL; + double delay, at; + if (!PyArg_ParseTuple(fargs, "dO|O", &delay, &callback, &args)) return NULL; + at = glfwGetTime() + delay; + + for (i = 0; i < self->count; i++) { + if (self->events[i].callback == callback) { + added = true; + self->events[i].at = at; + Py_CLEAR(self->events[i].args); + self->events[i].args = args; + Py_XINCREF(args); + break; + } + } + + if (!added) return _add(self, at, callback, args); + + Py_RETURN_NONE; +} + +static PyObject * +add_if_missing(Timers *self, PyObject *fargs) { +#define add_if_missing_doc "add_if_missing(delay, callback, args) -> Add callback, unless it already exists" + size_t i; + PyObject *callback, *args = NULL; + double delay, at; + if (!PyArg_ParseTuple(fargs, "dO|O", &delay, &callback, &args)) return NULL; + + for (i = 0; i < self->count; i++) { + if (self->events[i].callback == callback) { + Py_RETURN_NONE; + } + } + at = glfwGetTime() + delay; + + return _add(self, at, callback, args); +} + +static PyObject * +remove_event(Timers *self, PyObject *callback) { +#define remove_event_doc "remove(callback) -> Remove the event with the specified callback, if present" + TimerEvent *other = self->events == self->buf1 ? self->buf2 : self->buf1; + size_t i, j; + for (i = 0, j = 0; i < self->count; i++) { + if (self->events[i].callback != callback) { + other[j].callback = self->events[i].callback; other[j].at = self->events[i].at; other[j].args = self->events[i].args; + j++; + } else { + Py_CLEAR(self->events[i].callback); Py_CLEAR(self->events[i].args); + } + } + self->events = other; + self->count = j; + Py_RETURN_NONE; +} + +static PyObject * +timeout(Timers *self) { +#define timeout_doc "timeout() -> The time in seconds until the next event" + if (self->count < 1) { Py_RETURN_NONE; } + double ans = self->events[0].at - glfwGetTime(); + return PyFloat_FromDouble(MAX(0, ans)); +} + +static PyObject * +call(Timers *self) { +#define call_doc "call() -> Dispatch all expired events" + if (self->count < 1) { Py_RETURN_NONE; } + TimerEvent *other = self->events == self->buf1 ? self->buf2 : self->buf1; + double now = glfwGetTime(); + size_t i, j; + for (i = 0, j = 0; i < self->count; i++) { + if (self->events[i].at <= now) { // expired, call it + PyObject *ret = PyObject_CallObject(self->events[i].callback, self->events[i].args); + Py_CLEAR(self->events[i].callback); Py_CLEAR(self->events[i].args); + if (ret == NULL) PyErr_Print(); + else Py_DECREF(ret); + } else { + other[j].callback = self->events[i].callback; other[j].at = self->events[i].at; other[j].args = self->events[i].args; + j++; + } + } + self->events = other; + self->count = j; + Py_RETURN_NONE; +} + +// Boilerplate {{{ +static PyMethodDef methods[] = { + METHOD(add, METH_VARARGS) + METHOD(add_if_missing, METH_VARARGS) + METHOD(remove_event, METH_O) + METHOD(timeout, METH_NOARGS) + METHOD(call, METH_NOARGS) + {NULL} /* Sentinel */ +}; + + +PyTypeObject Timers_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "fast_data_types.Timers", + .tp_basicsize = sizeof(Timers), + .tp_dealloc = (destructor)dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = "Timers", + .tp_methods = methods, + .tp_new = new, +}; + + +INIT_TYPE(Timers) +// }}} diff --git a/kitty/timers.py b/kitty/timers.py deleted file mode 100644 index aaeb56d4a..000000000 --- a/kitty/timers.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=utf-8 -# License: GPL v3 Copyright: 2016, Kovid Goyal - -from collections import namedtuple -from operator import itemgetter -from time import monotonic - -from .utils import safe_print - -Event = namedtuple('Event', 'at callback args') -get_at = itemgetter(0) - - -class Timers: - - def __init__(self): - self.timers = [] - - def _add(self, delay, callback, args): - self.timers.append(Event(monotonic() + delay, callback, args)) - self.timers.sort(key=get_at) - - def add(self, delay, callback, *args): - self.remove(callback) - self._add(delay, callback, args) - - def add_if_missing(self, delay, callback, *args): - for ev in self.timers: - if ev.callback == callback: - return - self._add(delay, callback, args) - - def remove(self, callback): - for i, ev in enumerate(self.timers): - if ev.callback == callback: - break - else: - return - del self.timers[i] - - def timeout(self): - if self.timers: - return max(0, self.timers[0][0] - monotonic()) - - def __call__(self): - if self.timers: - now = monotonic() - expired_timers, waiting_timers = [], [] - for ev in self.timers: - (expired_timers if ev[0] <= now else waiting_timers).append(ev) - self.timers = waiting_timers - for ev in expired_timers: - try: - ev.callback(*ev.args) - except Exception: - import traceback - safe_print(traceback.format_exc())