From 80176088610e20058e0ae457eefc339f8b9ae778 Mon Sep 17 00:00:00 2001 From: CismonX Date: Fri, 2 Aug 2019 01:50:35 +0800 Subject: [PATCH] archive --- LICENSE | 21 +++++++ README.md | 72 ++++++++++++++++++++++ examples/with-timer.cpp | 71 +++++++++++++++++++++ src/ncurses-poll.hpp | 133 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/with-timer.cpp create mode 100644 src/ncurses-poll.hpp diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..83a5457 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 CismonX + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..647273e --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# ncurses-poll + +## 1. Introduction + +This repository contains a simple header, [ncurses-poll.hpp](src/ncurses-poll.hpp). With it, you can perform non-blocking I/O in your ncurses application. + +## 2. Features + +### 2.1 Multiple instances + +Each instance of `ncurses_poll` wraps an ncurses `SCREEN*`. Multiple instances can be handled simultaneously in one thread. + +Note that there's no need to call `set_term()` because this is done by the header before callbacks are invoked. + +### 2.2 Boost.Asio and coroutines + +Asynchronous I/O is achieved using Boost.Asio. Thus, other libraries(e.g. [Beast](https://github.com/boostorg/beast)) which uses Boost.Asio can be easily integrated into your ncurses applications. + +Boost.Asio's stackless coroutine is also supported for cleaner and human-friendlier code. + +## 3. Documentation + +### 3.1 Event loop + +An `ncurses_poll` instance must run within a Boost.Asio event loop (aka `io_context`). A const reference to an `io_context` instance should be passed to the constructor. + +### 3.2 Session + +The `ncurses_poll` class template requires a parameter `Session`, which is the type of session. The session may contain username, permission, or other data concerning a specific connection. + +`Session` must be a class with a constructor which accepts a pointer to this `ncurses_poll` class. + +To get pointer to current session instance of an `ncurses_poll` instance, call `session()` method. + +### 3.2 Async read/write + +A read/write operation may block, so we need to determine whether the stream is readable/writable before performing I/O. + +The `on_readable` and `on_writable` methods accept a single parameter, the const reference to a callback function. Once the stream is readable/writable, the given callback will be invoked. + +The callback function should be of the following type: + +```C++ +using callback = + std::function; +``` + +* The first argument of callback is the error code, upon success it yields zero, otherwise it is set to the corresponding errno. You should always handle `error_code` before performing I/O. +* The second argument is the pointer to the `ncurses_poll` instance. + +### 3.3 Using coroutines + +Boost.Asio's stackless coroutine requires a `boost::asio::coroutine` instance. You can simply use the `coro` property of `ncurses_poll`. For example, in a callback function: + +```C++ +if (!ec) reenter(poll->coro) for (;;) { + yield poll->on_writable(cb); + // ... +} +``` + +### 3.4 Other operations + +To get corresponding `io_context` instance, call `get_io_context()` method. + +To get `SCREEN` pointer to current ncurses instance, call `screen()` method. + +### 3.5 See also + +* [Examples](/examples) for using `ncurses_poll`. +* [Ncurses Programming Guide](http://www.cs.ukzn.ac.za/~hughm/os/notes/ncurses.html). +* Boost.Asio [documentation](https://www.boost.org/doc/libs/1_67_0/doc/html/boost_asio.html). diff --git a/examples/with-timer.cpp b/examples/with-timer.cpp new file mode 100644 index 0000000..50aa88c --- /dev/null +++ b/examples/with-timer.cpp @@ -0,0 +1,71 @@ +// +// examples/with-timer.cpp +// +// A simple example for ncurses-poll in which user input is displayed +// on the first line, while the current timestamp on the second, which +// is refreshed once per second. +// +// @author CismonX +// + +#include "ncurses-poll.hpp" + +#include + +struct my_session { + int chr = 0; + int num_chr = 0; + boost::asio::deadline_timer timer; + my_session(ncurses_poll* ncp) : timer(*ncp->get_io_context()) {} +}; + +using boost::system::error_code; +using ncp = ncurses_poll; +using callback = std::function; + +void tick(const error_code&, ncp*); + +inline void defer(ncp* poll) { + poll->session()->timer.expires_from_now(boost::posix_time::seconds(1)); + poll->session()->timer.async_wait(boost::bind(tick, boost::asio::placeholders::error, poll)); +} + +void tick(const error_code& ec, ncp* poll) { + if (ec) { + return; + } + poll->on_writable([](const error_code& ec, ncp* poll) { + if (ec) { + return; + } + mvaddstr(1, 0, std::to_string(std::time(nullptr)).c_str()); + refresh(); + defer(poll); + }); +} + +int main() { + boost::asio::io_context io_context; + ncp poll(io_context); + poll.init(); + callback cb = [&cb](const error_code& ec, ncp* poll) { + if (!ec) reenter(poll->coro) for (;;) { + poll->session()->chr = getch() | A_UNDERLINE; + yield poll->on_writable(cb); + mvaddch(0, poll->session()->num_chr++, poll->session()->chr); + refresh(); + yield poll->on_readable(cb); + } + }; + poll.on_writable([&cb](const error_code& ec, ncp* poll) { + if (!ec) { + curs_set(0); + cbreak(); + noecho(); + refresh(); + poll->on_readable(cb); + } + }); + defer(&poll); + io_context.run(); +} diff --git a/src/ncurses-poll.hpp b/src/ncurses-poll.hpp new file mode 100644 index 0000000..f738586 --- /dev/null +++ b/src/ncurses-poll.hpp @@ -0,0 +1,133 @@ +// +// ncurses-poll.hpp +// +// A header for performing asynchronous I/O with ncurses using Boost.Asio. +// +// @author CismonX +// + +#ifndef NCURSES_POLL_HPP_ +#define NCURSES_POLL_HPP_ + +#include +#include +#include + +template +class ncurses_poll { + + using stream_descriptor_t = boost::asio::posix::stream_descriptor; + + /// Provides asynchronous I/O functionalities for this ncurses instance. + stream_descriptor_t sd_; + + /// I/O context used by the stream descriptor. + boost::asio::io_context *context_; + + /// File pointer of this tty. + FILE *file_ = nullptr; + + /// Screen pointer of this ncurses instance. + SCREEN *screen_ = nullptr; + + /// User-defined session for this ncurses instance. + Session session_; + +public: + /// Provides support for stackless coroutines. + boost::asio::coroutine coro; + + /** + * Constructor. + * + * @param io_context The I/O context used for dispatching handlers. + */ + explicit ncurses_poll(boost::asio::io_context& io_context) : + sd_(io_context), context_(&io_context), session_(this) {} + + /** + * Deleted default constructor. + */ + ncurses_poll() = delete; + + /** + * Initialize ncurses instance. + * + * @param path File path. e.g. "/dev/pts/0". Empty for current tty. + */ + int init(const std::string& path = ttyname(STDIN_FILENO)) { + file_ = fopen(path.c_str(), "r+"); + if (file_ == nullptr) { + return errno; + } + screen_ = newterm(nullptr, file_, file_); + boost::system::error_code ec; + sd_.assign(fileno(file_), ec); + return ec.value(); + } + + /** + * Invoke the given callback once the terminal is readable. + * + * @param read_cb The callback to be invoked. + */ + template + void on_readable(F&& read_cb) { + sd_.async_wait(stream_descriptor_t::wait_read, boost::bind( + [this, read_cb](const boost::system::error_code& ec) { + set_term(screen_); + read_cb(ec, this); + }, boost::asio::placeholders::error)); + } + + /** + * Invoke the given callback once the terminal is writable. + * + * @param write_cb The callback to be invoked. + */ + template + void on_writable(F&& write_cb) { + sd_.async_wait(stream_descriptor_t::wait_write, boost::bind( + [this, write_cb](const boost::system::error_code& ec) { + set_term(screen_); + write_cb(ec, this); + }, boost::asio::placeholders::error)); + } + + /** + * Get current session. + */ + Session *session() { + return &session_; + } + + /** + * Get screen pointer of this ncurses instance. + */ + SCREEN *screen() const { + return screen_; + } + + /** + * Get current I/O context. + */ + boost::asio::io_context *get_io_context() { + return context_; + } + + /** + * Destructor. + */ + ~ncurses_poll() { + if (screen_) { + set_term(screen_); + endwin(); + delscreen(screen_); + } + if (file_) { + fclose(file_); + } + } +}; + +#endif