# Urwid main loop code # Copyright (C) 2004-2012 Ian Ward # Copyright (C) 2008 Walter Mundt # Copyright (C) 2009 Andrew Psaltis # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Select based urwid EventLoop implementation.""" from __future__ import annotations import heapq import select import time import typing from collections.abc import Callable, Iterator from itertools import count from .abstract_loop import EventLoop, ExitMainLoop if typing.TYPE_CHECKING: from typing_extensions import Literal __all__ = ("SelectEventLoop",) class SelectEventLoop(EventLoop): """ Event loop based on :func:`select.select` """ def __init__(self) -> None: self._alarms: list[tuple[float, int, Callable[[], typing.Any]]] = [] self._watch_files: dict[int, Callable[[], typing.Any]] = {} self._idle_handle: int = 0 self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {} self._tie_break: Iterator[int] = count() self._did_something: bool = False def alarm( self, seconds: float | int, callback: Callable[[], typing.Any], ) -> tuple[float, int, Callable[[], typing.Any]]: """ Call callback() a given time from now. No parameters are passed to callback. Returns a handle that may be passed to remove_alarm() seconds -- floating point time to wait before calling callback callback -- function to call from event loop """ tm = time.time() + seconds handle = (tm, next(self._tie_break), callback) heapq.heappush(self._alarms, handle) return handle def remove_alarm(self, handle: tuple[float, int, Callable[[], typing.Any]]) -> bool: """ Remove an alarm. Returns True if the alarm exists, False otherwise """ try: self._alarms.remove(handle) heapq.heapify(self._alarms) return True except ValueError: return False def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int: """ Call callback() when fd has some data to read. No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file() fd -- file descriptor to watch for input callback -- function to call when input is available """ self._watch_files[fd] = callback return fd def remove_watch_file(self, handle: int) -> bool: """ Remove an input file. Returns True if the input file exists, False otherwise """ if handle in self._watch_files: del self._watch_files[handle] return True return False def enter_idle(self, callback: Callable[[], typing.Any]) -> int: """ Add a callback for entering idle. Returns a handle that may be passed to remove_idle() """ self._idle_handle += 1 self._idle_callbacks[self._idle_handle] = callback return self._idle_handle def remove_enter_idle(self, handle: int) -> bool: """ Remove an idle callback. Returns True if the handle was removed. """ try: del self._idle_callbacks[handle] except KeyError: return False return True def _entering_idle(self) -> None: """ Call all the registered idle callbacks. """ for callback in self._idle_callbacks.values(): callback() def run(self) -> None: """ Start the event loop. Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly. """ try: self._did_something = True while True: try: self._loop() except InterruptedError: pass except ExitMainLoop: pass def _loop(self) -> None: """ A single iteration of the event loop """ fds = list(self._watch_files) if self._alarms or self._did_something: timeout = 0. tm: float | Literal['idle'] | None = None if self._alarms: timeout_ = self._alarms[0][0] tm = timeout_ timeout = max(timeout, timeout_ - time.time()) if self._did_something and (not self._alarms or (self._alarms and timeout > 0)): timeout = 0. tm = 'idle' ready, w, err = select.select(fds, [], fds, timeout) else: tm = None ready, w, err = select.select(fds, [], fds) if not ready: if tm == 'idle': self._entering_idle() self._did_something = False elif tm is not None: # must have been a timeout tm, tie_break, alarm_callback = heapq.heappop(self._alarms) alarm_callback() self._did_something = True for fd in ready: self._watch_files[fd]() self._did_something = True