diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0412588..c2dc276 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,33 +11,37 @@ jobs: # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix runs-on: ubuntu-latest + container: rikorose/gcc-cmake:latest steps: - uses: actions/checkout@v2 + - name: Install dependencies + run: apt-get update && apt-get install -yq libboost-thread-dev libboost-system-dev + - name: Create Build Environment # Some projects don't allow in-source building, so create a separate build directory # We'll use this as our working directory for all subsequent commands - run: cmake -E make_directory ${{runner.workspace}}/build + run: cmake -E make_directory build - name: Configure CMake # Use a bash shell so we can use the same syntax for environment variable # access regardless of the host operating system shell: bash - working-directory: ${{runner.workspace}}/build + working-directory: build # Note the current convention is to use the -S and -B options here to specify source # and build directories, but this is only available with CMake 3.13 and higher. # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 - run: cmake $GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$BUILD_TYPE + run: cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE - name: Build - working-directory: ${{runner.workspace}}/build + working-directory: build shell: bash # Execute the build. You can specify a specific target with "--target " run: cmake --build . --config $BUILD_TYPE - name: Test - working-directory: ${{runner.workspace}}/build + working-directory: build shell: bash # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail diff --git a/include/buffer.hpp b/include/buffer.hpp new file mode 100644 index 0000000..111cd6b --- /dev/null +++ b/include/buffer.hpp @@ -0,0 +1,38 @@ +#ifndef __BUFFER_HPP +#define __BUFFER_HPP + +#include +#include +#include + +namespace fs = std::filesystem; + +namespace rang +{ +class buffer +{ + public: + std::vector contents; + + buffer() = default; + + virtual ~buffer() = default; + + virtual void update() = 0; +}; + +class dummy : buffer +{ +}; + +class directory_listing : public buffer +{ + public: + fs::path directory; + + directory_listing(fs::path _directory); + + void update(); +}; +} // namespace rang +#endif diff --git a/include/evloop.hpp b/include/evloop.hpp new file mode 100644 index 0000000..71b2836 --- /dev/null +++ b/include/evloop.hpp @@ -0,0 +1,40 @@ +#ifndef __EVLOOP_HPP +#define __EVLOOP_HPP + +#include +#include +#include +#include + +#undef timeout + +namespace rang +{ +class event_loop +{ + private: + boost::asio::io_context io_context; + boost::asio::signal_set signals; + boost::asio::posix::stream_descriptor input; + + public: + event_loop(); + + template + void add_regular_job(const std::function callback, + const duration &interval); // TODO implementation + + template + void add_job(const std::function callback, time_point &time); // TODO implementation + + void set_input_reaction(const std::function callback); + + void run(); + + void stop(); + + private: + void handle_input(const std::function &callback); +}; +} // namespace rang +#endif diff --git a/include/rang.h b/include/rang.h deleted file mode 100644 index 8467d8b..0000000 --- a/include/rang.h +++ /dev/null @@ -1,6 +0,0 @@ -#include - -namespace rang -{ -const std::string MESSAGE = "Hello, world\n"; -} diff --git a/include/rwindow.hpp b/include/rwindow.hpp new file mode 100644 index 0000000..e16d01b --- /dev/null +++ b/include/rwindow.hpp @@ -0,0 +1,25 @@ +#ifndef __RWINDOW_HPP +#define __RWINDOW_HPP + +#include "buffer.hpp" +#include "console_io.hpp" + +namespace rang +{ +class window +{ + private: + console_io::window win; + buffer &tied_buf; + + public: + int offset_x = 0, offset_y = 0; + window(console_io::window &&_win, buffer &_tied_buf); + + void refresh(); + + void scroll_viewport(int shift); +}; +} // namespace rang + +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 12bd8e1..83ce58e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,9 @@ -add_executable (rang rang.cpp) +add_subdirectory(console_io) +add_executable (rang evloop.cpp buffer.cpp rwindow.cpp rang.cpp) + +find_package(Boost 1.60.0 REQUIRED COMPONENTS system thread) + +target_compile_features(rang PUBLIC cxx_std_17) target_include_directories(rang PUBLIC ${CURSES_INCLUDE_DIRS}) -target_link_libraries(rang PUBLIC ${CURSES_LIBRARIES}) +target_link_libraries(rang PUBLIC ${CURSES_LIBRARIES} ${Boost_LIBRARIES} console_io) diff --git a/src/buffer.cpp b/src/buffer.cpp new file mode 100644 index 0000000..6360f70 --- /dev/null +++ b/src/buffer.cpp @@ -0,0 +1,31 @@ +#include "buffer.hpp" +#include + +using namespace rang; + +directory_listing::directory_listing(fs::path _directory) : directory(_directory) +{ +} + +void directory_listing::update() +{ + std::vector filtered; + fs::directory_iterator it(directory); + std::copy_if(fs::begin(it), fs::end(it), std::back_insert_iterator(filtered), + [](fs::directory_entry entry) -> bool { + return entry.path().filename().string().front() != '.'; + }); + std::sort(filtered.begin(), filtered.end(), + [](fs::directory_entry entry1, fs::directory_entry entry2) -> bool { + if (entry1.is_directory() != entry2.is_directory()) { + return entry1.is_directory(); + } + return entry1.path().filename().string() < + entry2.path().filename().string(); + }); + contents.resize(filtered.size()); + transform(filtered.begin(), filtered.end(), contents.begin(), + [](fs::directory_entry entry) -> std::string { + return entry.path().filename().string(); + }); +} diff --git a/src/console_io/CMakeLists.txt b/src/console_io/CMakeLists.txt new file mode 100644 index 0000000..7a2accd --- /dev/null +++ b/src/console_io/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.17) +project(console_io) + +add_library(console_io OBJECT src/window.cpp src/base.cpp) + +target_include_directories(console_io PUBLIC include) diff --git a/src/console_io/include/base.hpp b/src/console_io/include/base.hpp new file mode 100644 index 0000000..5355278 --- /dev/null +++ b/src/console_io/include/base.hpp @@ -0,0 +1,18 @@ +#ifndef __SCREEN_HPP +#define __SCREEN_HPP + +#include "window.hpp" +#include + +namespace console_io +{ +class ncurses : public window +{ + public: + ncurses(); + + ~ncurses(); +}; +} // namespace console_io + +#endif diff --git a/src/console_io/include/console_io.hpp b/src/console_io/include/console_io.hpp new file mode 100644 index 0000000..964638d --- /dev/null +++ b/src/console_io/include/console_io.hpp @@ -0,0 +1,7 @@ +#ifndef __CONSOLE_IO_HPP +#define __CONSOLE_IO_HPP +#include "base.hpp" +#include "window.hpp" + +#undef timeout +#endif diff --git a/src/console_io/include/window.hpp b/src/console_io/include/window.hpp new file mode 100644 index 0000000..8a41de0 --- /dev/null +++ b/src/console_io/include/window.hpp @@ -0,0 +1,53 @@ +#ifndef __WINDOW_HPP +#define __WINDOW_HPP + +#include +#include +#include +#include + +namespace console_io +{ +class window +{ + protected: + WINDOW *win_ptr = nullptr; + int size_x = 0; + int size_y = 0; + int offset_x = 0; + int offset_y = 0; + + public: + window *parent = nullptr; + std::vector subwindows; + + window() = default; + + window(int _size_x, int _size_y, int _offset_x, int _offset_y, window *_parent = nullptr); + + ~window(); + + bool operator==(const window &other) const; + + int get_size_x() const; + int get_size_y() const; + int get_offset_x() const; + int get_offset_y() const; + + void refresh() const; + + void move(int x, int y) const; + + void output(std::string s) const; + + void move_and_output(int x, int y, std::string s) const; + + void outputln(int y, std::string s) const; + + window subwindow(int _size_x, int _size_y, int offset_x, int offset_y); + + int set_keypad(bool value) const; +}; +} // namespace console_io + +#endif diff --git a/src/console_io/src/base.cpp b/src/console_io/src/base.cpp new file mode 100644 index 0000000..0cb34ca --- /dev/null +++ b/src/console_io/src/base.cpp @@ -0,0 +1,27 @@ +#include "base.hpp" +#include + +using namespace console_io; + +ncurses::ncurses() +{ + win_ptr = initscr(); + if (win_ptr == nullptr) { + throw 1; + } + size_x = COLS; + size_y = LINES; +} + +ncurses::~ncurses() +{ + if (win_ptr == stdscr) { // guarantee Liskov substitution + endwin(); + } else { + if (parent != nullptr) { + parent->subwindows.erase( + std::find(parent->subwindows.begin(), parent->subwindows.end(), *this)); + } + delwin(win_ptr); + } +} diff --git a/src/console_io/src/window.cpp b/src/console_io/src/window.cpp new file mode 100644 index 0000000..3598dc1 --- /dev/null +++ b/src/console_io/src/window.cpp @@ -0,0 +1,105 @@ +#include "window.hpp" +#include + +using namespace console_io; + +window::window(int _size_x, int _size_y, int offset_x, int offset_y, window *_parent) + : size_x(_size_x), size_y(_size_y), parent(_parent) +{ + win_ptr = newwin(size_y, size_x, offset_y, offset_x); + if (win_ptr == nullptr) { + throw 1; // TODO errors + } + refresh(); +} + +window::~window() +{ + if (parent != nullptr) { + // this window must be deleted from parent's subwindows in order to avoid double + // destruction + parent->subwindows.erase( + std::find(parent->subwindows.begin(), parent->subwindows.end(), *this)); + } + subwindows.clear(); + delwin(win_ptr); +} + +bool window::operator==(const window &other) const +{ + return win_ptr == other.win_ptr; +} + +int window::get_size_x() const +{ + return size_x; +} + +int window::get_size_y() const +{ + return size_y; +} + +int window::get_offset_x() const +{ + return offset_x; +} + +int window::get_offset_y() const +{ + return offset_y; +} + +void window::refresh() const +{ + if (wrefresh(win_ptr) == ERR) { + throw 1; + }; +} + +void window::move(int x, int y) const +{ + if (wmove(win_ptr, y, x) == ERR) { + throw 1; + }; +} + +void window::output(std::string s) const +{ + if (waddstr(win_ptr, s.c_str()) == ERR) { + throw 1; + } +} + +void window::move_and_output(int x, int y, std::string s) const +{ + if (mvwaddstr(win_ptr, y, x, s.c_str()) == ERR) { + throw 1; + } +} + +void window::outputln(int y, std::string s) const +{ + s.resize(size_x - 1, ' '); + if (mvwaddstr(win_ptr, y, 0, s.c_str()) == ERR) { + throw 1; + } +} + +window window::subwindow(int _size_x, int _size_y, int offset_x, int offset_y) +{ + window result; + result.win_ptr = subwin(win_ptr, _size_y, _size_x, offset_y, offset_x); + if (result.win_ptr == nullptr) { + throw 1; + } + result.size_x = _size_x; + result.size_y = _size_y; + subwindows.push_back(result); + return result; +} + +int window::set_keypad(bool value) const +{ + return keypad(win_ptr, (value ? TRUE : FALSE)); +} diff --git a/src/evloop.cpp b/src/evloop.cpp new file mode 100644 index 0000000..ab2df4a --- /dev/null +++ b/src/evloop.cpp @@ -0,0 +1,41 @@ +#include "evloop.hpp" +#include +#include + +#undef timeout + +using namespace rang; + +event_loop::event_loop() + : signals(io_context, SIGINT, SIGTERM), // handle SIGINT and SIGTERM signals + input(io_context, STDIN_FILENO) // take input from stdin stream +{ + signals.async_wait([&](auto, auto) { io_context.stop(); }); +} + +void event_loop::run() +{ + io_context.run(); +} + +void event_loop::stop() +{ + io_context.stop(); +} + +void event_loop::set_input_reaction(const std::function callback) +{ + input.cancel(); + handle_input(callback); +} + +void event_loop::handle_input(const std::function &callback) +{ + input.async_wait(boost::asio::posix::stream_descriptor::wait_read, + [this, callback](auto error) { + if (!error) { + callback(getch()); + } + handle_input(callback); + }); +} diff --git a/src/rang.cpp b/src/rang.cpp index 07f8bb5..c4ff2d3 100644 --- a/src/rang.cpp +++ b/src/rang.cpp @@ -1,12 +1,53 @@ -#include "rang.h" +#include "buffer.hpp" +#include "console_io.hpp" +#include "evloop.hpp" +#include "rwindow.hpp" +#include +#include #include int main(int argc, char *argv[]) { - initscr(); /* Start curses mode */ - printw(rang::MESSAGE.c_str()); /* Print Hello World */ - refresh(); /* Print it on to the real screen */ - getch(); /* Wait for user input */ - endwin(); /* End curses mode */ + console_io::ncurses root; + cbreak(); /* Line buffering disabled, Pass on + *everty thing to me */ + root.set_keypad(true); /* I need that nifty F1 */ + keypad(stdscr, TRUE); + noecho(); + curs_set(0); + root.refresh(); + console_io::window mywin = + root.subwindow(root.get_size_x() - 2, root.get_size_y() - 2, 1, 1); + rang::directory_listing lst("."); + lst.update(); + rang::window main(std::move(mywin), lst); + root.refresh(); + int ch; + main.refresh(); + rang::event_loop loop; + std::function reaction_func = [&](int ch) { + if (ch == KEY_F(1)) { + loop.stop(); + return; + } + try { + switch (ch) { + case KEY_DOWN: + case 'j': + main.scroll_viewport(1); + break; + case KEY_UP: + case 'k': + main.scroll_viewport(-1); + break; + default: + return; + } + main.refresh(); + } catch (error_t) { + } + }; + loop.set_input_reaction(reaction_func); + loop.run(); return 0; } diff --git a/src/rwindow.cpp b/src/rwindow.cpp new file mode 100644 index 0000000..77dcc69 --- /dev/null +++ b/src/rwindow.cpp @@ -0,0 +1,27 @@ +#include "rwindow.hpp" + +using namespace rang; + +window::window(console_io::window &&_win, buffer &_tied_buf) : win(_win), tied_buf(_tied_buf) +{ +} + +void window::refresh() +{ + for (int y = 0; y < win.get_size_y(); ++y) { + if (offset_y + y < tied_buf.contents.size()) { + win.outputln(y, tied_buf.contents[offset_y + y]); + } else { + win.outputln(y, ""); + } + } + win.refresh(); +} + +void window::scroll_viewport(int shift) +{ + if (offset_y + shift >= tied_buf.contents.size() || offset_y + shift < 0) { + throw 1; // todo errors + } + offset_y += shift; +}