├── .gitignore ├── config.mk ├── include ├── detail │ ├── util.hpp │ ├── resource.hpp │ ├── backlog_impl.hpp │ └── filter.hpp ├── input.hpp ├── backlog.hpp ├── tty.hpp ├── pty.hpp ├── cmd.hpp └── scroller.hpp ├── README.md ├── LICENSE ├── sts.md ├── sts.1 ├── Makefile ├── src └── main.cpp └── .ycm_extra_conf.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries 2 | bin/ 3 | 4 | # Logs 5 | .in_log 6 | .out_log 7 | 8 | # YCM 9 | *.pyc 10 | 11 | # Object files 12 | *.cpp.o 13 | 14 | # Local notes 15 | NOTES 16 | -------------------------------------------------------------------------------- /config.mk: -------------------------------------------------------------------------------- 1 | VERSION = 0.1alpha 2 | 3 | # customize below to fit your system 4 | 5 | # paths 6 | PREFIX = /usr/local 7 | MANPREFIX = ${PREFIX}/share/man 8 | 9 | # includes and libs 10 | INCS += -Iinclude 11 | LIBS += 12 | 13 | # flags 14 | CPPFLAGS += -DVERSION=\"${VERSION}\" 15 | CXXFLAGS += -std=c++14 -Wall -Wextra -pedantic -O3 ${INCS} ${CPPFLAGS} 16 | LDFLAGS += ${LIBS} 17 | 18 | # compiler and linker 19 | CXX ?= c++ 20 | -------------------------------------------------------------------------------- /include/detail/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace sts 7 | { 8 | namespace detail 9 | { 10 | template 11 | std::array, sizeof...(Ts) + 1> make_array(T &&t, Ts &&... ts) 12 | { return { { t, ts... } }; } 13 | 14 | template ::value_type> 16 | std::size_t seq_eq(std::size_t const d, It it, std::array const &arr) 17 | { return (d >= N && std::equal(it, it + N, std::begin(arr))) * N; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /include/input.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace sts 6 | { 7 | namespace input 8 | { 9 | enum class event 10 | { 11 | mouse_wheel_up = 25, 12 | mouse_wheel_down = 5, 13 | carriage_return = 13 14 | }; 15 | 16 | template 17 | bool parse(S &scroller, T const &buf, ssize_t const num_read) 18 | { 19 | bool consumed{}; 20 | for(ssize_t i{}; i < num_read; ++i) 21 | { 22 | event const ev{ static_cast(buf[i]) }; 23 | 24 | switch(ev) 25 | { 26 | case event::mouse_wheel_up: 27 | scroller.up(); 28 | consumed = true; 29 | break; 30 | case event::mouse_wheel_down: 31 | scroller.down(); 32 | consumed = true; 33 | break; 34 | case event::carriage_return: 35 | default: 36 | scroller.follow(); 37 | break; 38 | } 39 | } 40 | 41 | return consumed; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sts 2 | === 3 | 4 | sts is a pseudo terminal scroller specifically designed for [st](http://st.suckless.org). st's minimalism delegates scrolling functionality and suggests tmux or screen to provide it. Unfortunately, both are quite heavy weight for just scrolling and, if you're already using them, the session nesting becomes burdensome. 5 | 6 | To start st with sts, after installing both, use: 7 | ```bash 8 | $ st -e sts 9 | ``` 10 | 11 | ### Installation 12 | Compilation requires a modern GCC (4.9+) or clang (3.4+). Build options can be configured in `config.mk`. 13 | ```bash 14 | # assuming appropriate permissions 15 | $ make install 16 | ``` 17 | 18 | ### Configuration 19 | Any configuration features offered by sts are outlined in the help screen you can print by using `sts -h`. For more verbose information, see the accompanying man page `sts(1)`. 20 | 21 | ### Questions 22 | #### Can I still scroll within other applications? 23 | Yes, sts tries to stay out of the away and tries only to scroll your shell prompt. Applications like vim, weechat, and even tmux and screen, should still work normally. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2014 Jesse 'Jeaye' Wilkerson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sts.md: -------------------------------------------------------------------------------- 1 | STS 1 "VERSION" Linux "User Manuals" 2 | ======================================= 3 | 4 | NAME 5 | ---- 6 | sts - simple terminal scroller 7 | 8 | SYNOPSIS 9 | -------- 10 | `sts` [`-u`] [`-l` *buf_limit*] [`-s` *lines*] 11 | 12 | DESCRIPTION 13 | ----------- 14 | `sts` is a pseudo terminal scroller for the simple terminal st(1). 15 | 16 | OPTIONS 17 | ------- 18 | `-h`, `--help` 19 | Show the terse help screen and exit. 20 | 21 | `-u`, `--unlimited` 22 | Enable unlimited backlog lines (`default`). (same as `-l 0`) 23 | 24 | `-l` *buf_limit*, `--limit` *buf_limit* 25 | Specify the number of lines _beyond the screen_ `sts` should remember for scrolling. Default is 0, meaning there is no limit. (see `-u`) 26 | 27 | `-s` *lines*, `--step` *lines* 28 | Specify the number of lines each scroll step should take. Default is 5. 29 | 30 | `-v`, `--version` 31 | Show the version information and exit. 32 | 33 | BUGS 34 | ---- 35 | Bugs, issues, and feature requests should be sent to the issue tracker on Github: . 36 | 37 | AUTHOR 38 | ------ 39 | Jeaye 40 | 41 | LICENSE 42 | ------ 43 | `sts` is under the MIT license. See the _LICENSE_ file. 44 | -------------------------------------------------------------------------------- /sts.1: -------------------------------------------------------------------------------- 1 | .TH STS 1 "0.1alpha" Linux "User Manuals" 2 | .SH NAME 3 | .PP 4 | sts \- simple terminal scroller 5 | .SH SYNOPSIS 6 | .PP 7 | \fB\fCsts\fR [\fB\fC\-u\fR] [\fB\fC\-l\fR \fIbuf_limit\fP] [\fB\fC\-s\fR \fIlines\fP] 8 | .SH DESCRIPTION 9 | .PP 10 | \fB\fCsts\fR is a pseudo terminal scroller for the simple terminal 11 | .BR st (1). 12 | .SH OPTIONS 13 | .TP 14 | \fB\fC\-h\fR, \fB\fC\-\-help\fR 15 | Show the terse help screen and exit. 16 | .TP 17 | \fB\fC\-u\fR, \fB\fC\-\-unlimited\fR 18 | Enable unlimited backlog lines (\fB\fCdefault\fR). (same as \fB\fC\-l 0\fR) 19 | .TP 20 | \fB\fC\-l\fR \fIbuf_limit\fP, \fB\fC\-\-limit\fR \fIbuf_limit\fP 21 | Specify the number of lines \fIbeyond the screen\fP \fB\fCsts\fR should remember for scrolling. Default is 0, meaning there is no limit. (see \fB\fC\-u\fR) 22 | .TP 23 | \fB\fC\-s\fR \fIlines\fP, \fB\fC\-\-step\fR \fIlines\fP 24 | Specify the number of lines each scroll step should take. Default is 5. 25 | .TP 26 | \fB\fC\-v\fR, \fB\fC\-\-version\fR 27 | Show the version information and exit. 28 | .SH BUGS 29 | .PP 30 | Bugs, issues, and feature requests should be sent to the issue tracker on Github: 31 | .UR https://github.com/jeaye/sts/issues 32 | .UE \&. 33 | .SH AUTHOR 34 | .PP 35 | Jeaye 36 | .MT contact@jeaye.com 37 | .ME 38 | .SH LICENSE 39 | .PP 40 | \fB\fCsts\fR is under the MIT license. See the \fILICENSE\fP file. 41 | -------------------------------------------------------------------------------- /include/detail/resource.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace sts 7 | { 8 | namespace detail 9 | { 10 | template 11 | class resource 12 | { 13 | public: 14 | using dtor_t = std::function; 15 | 16 | resource() = delete; 17 | resource(dtor_t &&dtor) 18 | : dtor_{ std::move(dtor) } 19 | { } 20 | resource(resource const&) = delete; 21 | resource(resource &&) = default; 22 | resource(T &&t, dtor_t const &dtor) 23 | : data_(std::move(t)) 24 | , dtor_{ dtor } 25 | { } 26 | ~resource() 27 | { dtor_(data_); } 28 | 29 | resource& operator =(resource const&) = delete; 30 | resource& operator =(resource &&r) noexcept 31 | { 32 | dtor_(data_); 33 | data_ = std::move(r.data_); 34 | r.data_ = {}; 35 | return *this; 36 | } 37 | resource& operator =(T &&t) 38 | { 39 | dtor_(data_); 40 | data_ = std::move(t); 41 | return *this; 42 | } 43 | 44 | T& get() 45 | { return data_; } 46 | T const& get() const 47 | { return data_; } 48 | 49 | private: 50 | T data_{}; 51 | dtor_t dtor_; 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /include/backlog.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "detail/resource.hpp" 12 | #include "detail/backlog_impl.hpp" 13 | #include "detail/filter.hpp" 14 | 15 | namespace sts 16 | { 17 | class backlog 18 | { 19 | public: 20 | using marker_iterator = detail::backlog_impl::marker_iterator; 21 | using marker_t = detail::backlog_impl::marker_t; 22 | using limit_t = detail::backlog_impl::limit_t; 23 | 24 | backlog() = delete; 25 | backlog(tty const &tty, limit_t const limit) 26 | : tty_{ tty } 27 | , limit_{ limit } 28 | , impls_{ { tty_, limit_ } } 29 | { } 30 | 31 | template 32 | void write(It const &begin, It const &end) 33 | { 34 | if(!std::distance(begin, end)) 35 | { return; } 36 | 37 | get_impl().mark_lines(begin, end); 38 | get_impl().write(begin, end); 39 | get_impl().trim(); 40 | } 41 | 42 | private: 43 | detail::backlog_impl& get_impl() 44 | { return impls_.at(impls_.size() - 1); } 45 | 46 | friend class scroller; 47 | template 48 | friend It detail::filter(T&, It, It); 49 | 50 | tty const &tty_; 51 | limit_t const limit_{}; 52 | std::vector impls_; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # sts - simple terminal scroller 2 | 3 | include config.mk 4 | 5 | OUT = bin/ 6 | SRC = src/main.cpp 7 | OBJ = ${SRC:.cpp=.cpp.o} 8 | TARGET = ${OUT}sts 9 | 10 | all: ${TARGET} 11 | 12 | options: 13 | echo "CXXFLAGS = ${CXXFLAGS}" 14 | echo "LDFLAGS = ${LDFLAGS}" 15 | echo "CXX = ${CXX}" 16 | echo 17 | 18 | setup: 19 | mkdir -p ${OUT} 20 | 21 | %.cpp.o: %.cpp setup 22 | echo "compiling $<" 23 | ${CXX} -o $@ -c ${CXXFLAGS} $< 24 | 25 | ${TARGET}: options ${OBJ} 26 | echo "linking $@" 27 | ${CXX} -o $@ ${OBJ} ${LDFLAGS} 28 | 29 | clean: 30 | echo "cleaning" 31 | rm -f ${TARGET} ${OBJ} 32 | 33 | install: ${TARGET} 34 | echo "installing executable file to ${DESTDIR}${PREFIX}/bin" 35 | mkdir -p ${DESTDIR}${PREFIX}/bin 36 | cp -f ${TARGET} ${DESTDIR}${PREFIX}/bin 37 | chmod 755 ${DESTDIR}${PREFIX}/bin/sts 38 | echo "installing manual page to ${DESTDIR}${MANPREFIX}/man1" 39 | mkdir -p ${DESTDIR}${MANPREFIX}/man1 40 | cp -f sts.1 ${DESTDIR}${MANPREFIX}/man1/ 41 | chmod 644 ${DESTDIR}${MANPREFIX}/man1/sts.1 42 | 43 | uninstall: 44 | echo "removing executable file from ${DESTDIR}${PREFIX}/bin" 45 | rm -f ${DESTDIR}${PREFIX}/bin/sts 46 | echo "removing manual page from ${DESTDIR}${MANPREFIX}/man1" 47 | rm -f ${DESTDIR}${MANPREFIX}/man1/sts.1 48 | 49 | # for internal usage only 50 | man: 51 | rm -f sts.1 52 | md2man-roff sts.md > sts.1 53 | sed -i "s/VERSION/${VERSION}/g" sts.1 54 | 55 | # for debugging purposes only 56 | shell: 57 | st 58 | 59 | run: 60 | st -e ${TARGET} 61 | 62 | .SILENT: 63 | .PHONY: all options setup clean dist install uninstall shell run 64 | -------------------------------------------------------------------------------- /include/tty.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace sts 10 | { 11 | struct tty 12 | { 13 | struct error : std::runtime_error 14 | { using std::runtime_error::runtime_error; }; 15 | 16 | tty() 17 | { 18 | if(tcgetattr(STDIN_FILENO, &term) == -1) 19 | { throw error{ "failed to initialize termios" }; } 20 | if(ioctl(STDIN_FILENO, TIOCGWINSZ, &size) < 0) 21 | { throw error{ "failed to initialize term size" }; } 22 | } 23 | 24 | termios term; 25 | winsize size; 26 | }; 27 | 28 | struct raw_mode 29 | { 30 | raw_mode() = delete; 31 | raw_mode(tty const &t) 32 | : tty_{ t } 33 | { 34 | auto term(tty_.term); 35 | 36 | if(tcgetattr(STDIN_FILENO, &term) == -1) 37 | { throw tty::error{ "failed to initialize termios" }; } 38 | termios raw_term(term); 39 | 40 | raw_term.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO); 41 | 42 | /* Noncanonical mode, disable signals, extended 43 | input processing, and echoing */ 44 | raw_term.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR | 45 | INPCK | ISTRIP | IXON | PARMRK); 46 | 47 | /* Disable special handling of CR, NL, and BREAK. 48 | No 8th-bit stripping or parity error handling. 49 | Disable START/STOP output flow control. */ 50 | raw_term.c_oflag &= ~OPOST; /* Disable all output processing */ 51 | 52 | raw_term.c_cc[VMIN] = 1; /* Character-at-a-time input */ 53 | raw_term.c_cc[VTIME] = 0; /* with blocking */ 54 | 55 | if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_term) == -1) 56 | { throw tty::error{ "failed to enter raw mode" }; } 57 | } 58 | ~raw_mode() noexcept(false) 59 | { 60 | if(tcsetattr(STDIN_FILENO, TCSANOW, &tty_.term) == -1) 61 | { throw tty::error{ "failed to reset terminal" }; } 62 | } 63 | 64 | tty const &tty_; 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "cmd.hpp" 8 | #include "tty.hpp" 9 | #include "pty.hpp" 10 | #include "scroller.hpp" 11 | #include "input.hpp" 12 | 13 | int main(int const argc, char ** const argv) 14 | try 15 | { 16 | auto const summary(sts::cmd::parse(argc, argv)); 17 | 18 | sts::tty tty; 19 | sts::pty pt{ tty }; 20 | pt([] 21 | { 22 | char const *shell{ ::getenv("SHELL") }; 23 | if(!shell || *shell == '\0') 24 | { shell = "/bin/sh"; } 25 | 26 | ::execlp(shell, shell, nullptr); 27 | throw std::runtime_error{ "shell failed to run" }; 28 | }); 29 | 30 | /* Parent: relay data between terminal and pty master */ 31 | sts::backlog backlog{ tty, summary.limit }; 32 | sts::scroller scroller{ backlog, summary.step }; 33 | scroller.clear(); 34 | 35 | /* Place terminal in raw mode so that we can pass all terminal 36 | input to the pty master untouched */ 37 | sts::raw_mode const rm{ tty }; 38 | 39 | std::array buf{{}}; 40 | ssize_t num_read{}; 41 | fd_set in_fds{{}}; 42 | int const master_fd{ pt.get_master() }; 43 | 44 | while(true) 45 | { 46 | FD_ZERO(&in_fds); 47 | FD_SET(STDIN_FILENO, &in_fds); 48 | FD_SET(master_fd, &in_fds); 49 | 50 | if(::select(master_fd + 1, &in_fds, nullptr, nullptr, nullptr) == -1) 51 | { throw std::runtime_error{ "failed to select" }; } 52 | 53 | if(FD_ISSET(STDIN_FILENO, &in_fds)) /* stdin --> pty */ 54 | { 55 | num_read = ::read(STDIN_FILENO, buf.data(), buf.size()); 56 | if(num_read <= 0) 57 | { break; } 58 | 59 | if(sts::input::parse(scroller, buf, num_read)) 60 | { continue; } 61 | 62 | if(::write(master_fd, buf.data(), num_read) != num_read) 63 | { throw std::runtime_error{ "partial/failed write (master)" }; } 64 | } 65 | 66 | if(FD_ISSET(master_fd, &in_fds)) /* pty --> stdout + log */ 67 | { 68 | num_read = ::read(master_fd, buf.data(), buf.size()); 69 | if(num_read <= 0) 70 | { break; } 71 | scroller.write(std::begin(buf), std::begin(buf) + num_read); 72 | } 73 | } 74 | } 75 | catch(sts::cmd::help_request const &hr) 76 | { sts::cmd::show_help(hr); } 77 | catch(sts::cmd::version_request const &vr) 78 | { sts::cmd::show_version(vr); } 79 | catch(std::exception const &e) 80 | { std::cout << "exception: " << e.what() << std::endl; } 81 | catch(...) 82 | { std::cout << "unknown error occurred" << std::endl; } 83 | -------------------------------------------------------------------------------- /include/detail/backlog_impl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "tty.hpp" 8 | 9 | namespace sts 10 | { 11 | namespace detail 12 | { 13 | struct backlog_impl 14 | { 15 | using marker_iterator = std::size_t; 16 | using marker_t = std::pair; 17 | using limit_t = std::size_t; 18 | 19 | backlog_impl() = delete; 20 | backlog_impl(tty const &tty, limit_t const limit) 21 | : tty_{ tty } 22 | , limit_{ limit } 23 | { } 24 | 25 | template 26 | void write(It const &begin, It const &end) 27 | { std::copy(begin, end, std::back_inserter(buf_)); } 28 | 29 | template 30 | void mark_lines(It const &begin, It const &end) 31 | { 32 | if(line_markers_.empty()) 33 | { line_markers_.emplace_back(0, 0); } 34 | else if(last_char_ == '\n') 35 | { 36 | line_markers_.emplace_back((line_markers_.end() - 1)->second + 1, 37 | (line_markers_.end() - 1)->second + 1); 38 | } 39 | 40 | marker_t *marker{ &*(line_markers_.end() - 1) }; 41 | for(auto it(begin); it != end; ++it) 42 | { 43 | auto &i(marker->second); 44 | if(*it == '\n') 45 | { 46 | if(it + 1 != end) 47 | { 48 | line_markers_.emplace_back(i + 1, i + 1); 49 | marker = &*(line_markers_.end() - 1); 50 | } 51 | } 52 | else 53 | { ++i; } 54 | last_char_ = *it; 55 | } 56 | } 57 | 58 | void trim() 59 | { 60 | if(limit_ == 0 || line_markers_.size() <= tty_.get().size.ws_row + limit_) 61 | { return; } 62 | 63 | auto const excess(line_markers_.size() - (tty_.get().size.ws_row + limit_)); 64 | auto const offset(line_markers_[excess].first); 65 | line_markers_.erase(std::begin(line_markers_), 66 | std::begin(line_markers_) + excess); 67 | buf_.erase(0, offset); 68 | for(auto &marker : line_markers_) 69 | { 70 | marker.first -= offset; 71 | marker.second -= offset; 72 | } 73 | } 74 | 75 | std::reference_wrapper tty_; 76 | std::string buf_; 77 | std::vector line_markers_; 78 | char last_char_{}; 79 | limit_t limit_{}; 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /include/pty.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "tty.hpp" 11 | #include "detail/resource.hpp" 12 | 13 | namespace sts 14 | { 15 | class pty 16 | { 17 | public: 18 | struct error : std::runtime_error 19 | { using std::runtime_error::runtime_error; }; 20 | 21 | pty() = delete; 22 | pty(tty const &state) 23 | : tty_{ state } 24 | , master_{ posix_openpt(O_RDWR | O_NOCTTY), &close } 25 | { 26 | if(master_.get() == -1) 27 | { throw error{ "failed to open pty" }; } 28 | auto const master(master_.get()); 29 | 30 | if(grantpt(master) == -1) 31 | { throw error{ "failed to grant pty access" }; } 32 | 33 | if(unlockpt(master) == -1) 34 | { throw error{ "failed to unlock pty" }; } 35 | 36 | char const * const name{ ptsname(master) }; 37 | if(!name) 38 | { throw error{ "failed to acquire pty name" }; } 39 | name_ = name; 40 | } 41 | 42 | void operator ()(std::function const &func) 43 | { 44 | child_ = fork(); 45 | 46 | if(child_ == -1) 47 | { throw error{ "failed to fork" }; } 48 | if(child_ != 0) /* Return back to parent. */ 49 | { return; } 50 | 51 | /* Child falls through to here */ 52 | if(setsid() == -1) 53 | { throw error{ "failed to setsid" }; } 54 | master_ = 0; 55 | 56 | /* Becomes controlling tty */ 57 | slave_ = open(name_.c_str(), O_RDWR); 58 | if(slave_.get() == -1) 59 | { throw error{ "failed to open slave" }; } 60 | auto const slave(slave_.get()); 61 | 62 | if(tcsetattr(slave, TCSANOW, &tty_.term) == -1) 63 | { throw error{ "failed to set slave terminal attributes" }; } 64 | 65 | if(ioctl(slave, TIOCSWINSZ, &tty_.size) == -1) 66 | { throw error{ "failed to set slave terminal size" }; } 67 | 68 | /* Duplicate pty slave to be child's stdin, stdout, and stderr. */ 69 | if(dup2(slave, STDIN_FILENO) != STDIN_FILENO) 70 | { throw error{ "failed to duplicate stdin" }; } 71 | if(dup2(slave, STDOUT_FILENO) != STDOUT_FILENO) 72 | { throw error{ "failed to duplicate stdout" }; } 73 | if(dup2(slave, STDERR_FILENO) != STDERR_FILENO) 74 | { throw error{ "failed to duplicate stderr" }; } 75 | 76 | if(slave > STDERR_FILENO) 77 | { slave_ = 0; } 78 | 79 | func(); 80 | std::exit(0); 81 | } 82 | 83 | int get_master() const 84 | { return master_.get(); } 85 | 86 | private: 87 | tty const &tty_; 88 | detail::resource master_; 89 | detail::resource slave_{ &close }; 90 | std::string name_; 91 | pid_t child_{}; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /include/cmd.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace sts 8 | { 9 | namespace cmd 10 | { 11 | struct summary 12 | { 13 | std::string name; 14 | std::size_t limit{}; 15 | std::size_t step{ 5 }; 16 | }; 17 | 18 | struct help_request 19 | { std::string name; }; 20 | struct version_request 21 | { std::string name; }; 22 | 23 | summary parse(int argc, char **argv) 24 | { 25 | if(argc < 1) 26 | { throw std::underflow_error{ "invalid number of arguments" }; } 27 | 28 | summary sum; 29 | sum.name = argv[0]; 30 | --argc; ++argv; 31 | 32 | try 33 | { 34 | for(int i{}; i < argc; ++i) 35 | { 36 | std::string const arg{ argv[i] }; 37 | if(arg == "-h" || arg == "--help") 38 | { throw help_request{ sum.name }; } 39 | else if(arg == "-u" || arg == "--unlimited") 40 | { sum.limit = 0; } 41 | else if(arg == "-l" || arg == "--limit") 42 | { 43 | if(i + 1 == argc) 44 | { throw std::invalid_argument{ "invalid limit specifier; use " + arg + " buf_limit" }; } 45 | auto const limit(std::stoll(argv[i + 1])); 46 | if(limit < 0) 47 | { throw std::invalid_argument{ "invalid limit; must not be negative" }; } 48 | sum.limit = limit; 49 | ++i; 50 | } 51 | else if(arg == "-s" || arg == "--step") 52 | { 53 | if(i + 1 == argc) 54 | { throw std::invalid_argument{ "invalid step specifier; use " + arg + " lines" }; } 55 | auto const step(std::stoll(argv[i + 1])); 56 | if(step < 1) 57 | { throw std::invalid_argument{ "invalid step; must be > 0" }; } 58 | sum.step = step; 59 | ++i; 60 | } 61 | else if(arg == "-v" || arg == "--version") 62 | { throw version_request{ sum.name }; } 63 | else 64 | { throw help_request{ sum.name }; } 65 | } 66 | } 67 | catch(std::invalid_argument const &e) 68 | { 69 | std::cout << "error: " << e.what() << std::endl << std::endl; 70 | throw help_request{ sum.name }; 71 | } 72 | 73 | return sum; 74 | } 75 | 76 | [[noreturn]] void show_help(help_request const &hr) 77 | { 78 | std::cout << hr.name << " v" << VERSION << std::endl 79 | << "usage:\n " << hr.name << " [-u] [-l buf_limit]\n" 80 | << std::endl 81 | << "options:\n" 82 | << " -h, --help Show this help message and exit\n" 83 | << " -u, --unlimited [default] Enable unlimited backlog\n" 84 | << " -l buf_limit, --limit buf_limit [default = 0] Specify backlog limit in lines\n" 85 | << " -s lines, --step lines [default = 5] Specify scroll step in lines" 86 | << std::endl; 87 | std::exit(0); 88 | } 89 | 90 | [[noreturn]] void show_version(version_request const &vr) 91 | { 92 | std::cout << vr.name << " v" << VERSION 93 | << " compiled " << __DATE__ 94 | << std::endl; 95 | std::exit(0); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /include/detail/filter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "detail/util.hpp" 4 | 5 | #include 6 | #include 7 | 8 | namespace sts 9 | { 10 | namespace detail 11 | { 12 | template 13 | struct predicate 14 | { 15 | using func_t = std::function; 16 | 17 | predicate() = delete; 18 | predicate(std::string &&r) 19 | : regex{ std::move(r) } 20 | { } 21 | predicate(std::string &&r, func_t &&f) 22 | : regex{ std::move(r) } 23 | , func{ std::move(f) } 24 | { } 25 | 26 | std::regex const regex; 27 | func_t const func; 28 | }; 29 | 30 | template 31 | It filter(T &self, It const begin, It end) 32 | { 33 | static auto const predicates(detail::make_array 34 | ( 35 | /* smcup */ 36 | predicate 37 | { 38 | "\x1B\\[\\?(47|1049)h", 39 | [](T &self) 40 | { 41 | self.backlog_.impls_.emplace_back(self.backlog_.tty_, 42 | self.backlog_.limit_); 43 | } 44 | }, 45 | /* rmcup */ 46 | predicate 47 | { 48 | "\x1B\\[\\?(47|1049)l", 49 | [](T &self) 50 | { 51 | if(self.backlog_.impls_.size() > 1) 52 | { self.backlog_.impls_.erase(self.backlog_.impls_.end() - 1); } 53 | } 54 | }, 55 | /* clear */ 56 | predicate{ "\x1B\\[2J" }, 57 | /* move cursor/scroll */ 58 | predicate{ "\x1B\\[([[:digit:]]){0,3}(A|B|C|D|E|F|G|J|K|S|T)" }, 59 | /* move cursor */ 60 | predicate{ "\x1B\\[([[:digit:]]){0,3};?([[:digit:]]){0,3}(H|f)" }, 61 | /* report device */ 62 | predicate{ "\x1B\\[[[:digit:]]n" }, 63 | /* save/restore cursor */ 64 | predicate{ "\x1B\\[(s|u)" }, 65 | /* save/restore cursor attributes */ 66 | predicate{ "\x1B(7|8)" }, 67 | /* show/hide cursor */ 68 | predicate{ "\x1B\\[\\?12l" }, 69 | predicate{ "\x1B\\[\\?25(h|l)" }, 70 | /* scroll screen */ 71 | predicate{ "\x1B\\[([[:digit:]]){0,3};?([[:digit:]]){0,3}r" }, 72 | /* scroll down/up */ 73 | predicate{ "\x1B(D|M)" }, 74 | /* set tab */ 75 | predicate{ "\x1BH" }, 76 | /* clear tab */ 77 | predicate{ "\x1B\\[3?g" }, 78 | /* keyboard transit mode */ 79 | predicate{ "\x1B\\[\\?1l\x1B\\>" }, 80 | predicate{ "\x1B\\[\\?1h\x1B\\=" }, 81 | /* title */ 82 | predicate{ "\x1B\\]0;.*\x07" }, 83 | /* [re]set mode */ 84 | predicate{ "\x1B\\[=([[:digit:]]){0,2}(h|l)" } 85 | )); 86 | 87 | std::cmatch match; 88 | for(auto const &pred : predicates) 89 | { 90 | while(std::regex_search(begin, end, match, pred.regex)) 91 | { 92 | if(pred.func) 93 | { pred.func(self); } 94 | 95 | auto const str(match.str()); 96 | auto const it(begin + match.position()); 97 | auto const sub_end(it + match.length()); 98 | auto rit(begin); 99 | end = std::remove_if(begin, end, [&](char const) 100 | { 101 | auto const cur(rit++); 102 | return (cur >= it && cur < sub_end); 103 | }); 104 | } 105 | } 106 | 107 | return end; 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /.ycm_extra_conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ycm_core 3 | 4 | flags = [ 5 | '-Wall', 6 | '-Wextra', 7 | '-pedantic', 8 | '-std=c++1y', 9 | #'-stdlib=libc++', 10 | '-x', 11 | 'c++', 12 | '-Isrc', 13 | '-Iinclude', 14 | 15 | '-isystem', 16 | '../BoostParts', 17 | '-isystem', 18 | '/System/Library/Frameworks/Python.framework/Headers', 19 | '-isystem', 20 | '../llvm/include', 21 | '-isystem', 22 | '../llvm/tools/clang/include', 23 | '-I', 24 | '.', 25 | '-I', 26 | './ClangCompleter', 27 | '-isystem', 28 | './tests/gmock/gtest', 29 | '-isystem', 30 | './tests/gmock/gtest/include', 31 | '-isystem', 32 | './tests/gmock', 33 | '-isystem', 34 | './tests/gmock/include', 35 | '-isystem', 36 | '/usr/include', 37 | '-isystem', 38 | '/usr/local/include', 39 | '-isystem', 40 | '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../lib/c++/v1', 41 | '-isystem', 42 | '/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include', 43 | ] 44 | 45 | compilation_database_folder = '' 46 | 47 | if os.path.exists( compilation_database_folder ): 48 | database = ycm_core.CompilationDatabase( compilation_database_folder ) 49 | else: 50 | database = None 51 | 52 | SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ] 53 | 54 | def DirectoryOfThisScript(): 55 | return os.path.dirname( os.path.abspath( __file__ ) ) 56 | 57 | 58 | def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): 59 | if not working_directory: 60 | return list( flags ) 61 | new_flags = [] 62 | make_next_absolute = False 63 | path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ] 64 | for flag in flags: 65 | new_flag = flag 66 | 67 | if make_next_absolute: 68 | make_next_absolute = False 69 | if not flag.startswith( '/' ): 70 | new_flag = os.path.join( working_directory, flag ) 71 | 72 | for path_flag in path_flags: 73 | if flag == path_flag: 74 | make_next_absolute = True 75 | break 76 | 77 | if flag.startswith( path_flag ): 78 | path = flag[ len( path_flag ): ] 79 | new_flag = path_flag + os.path.join( working_directory, path ) 80 | break 81 | 82 | if new_flag: 83 | new_flags.append( new_flag ) 84 | return new_flags 85 | 86 | 87 | def IsHeaderFile( filename ): 88 | extension = os.path.splitext( filename )[ 1 ] 89 | return extension in [ '.h', '.hxx', '.hpp', '.hh' ] 90 | 91 | 92 | def GetCompilationInfoForFile( filename ): 93 | if IsHeaderFile( filename ): 94 | basename = os.path.splitext( filename )[ 0 ] 95 | for extension in SOURCE_EXTENSIONS: 96 | replacement_file = basename + extension 97 | if os.path.exists( replacement_file ): 98 | compilation_info = database.GetCompilationInfoForFile( 99 | replacement_file ) 100 | if compilation_info.compiler_flags_: 101 | return compilation_info 102 | return None 103 | return database.GetCompilationInfoForFile( filename ) 104 | 105 | 106 | def FlagsForFile( filename, **kwargs ): 107 | if database: 108 | compilation_info = GetCompilationInfoForFile( filename ) 109 | if not compilation_info: 110 | return None 111 | 112 | final_flags = MakeRelativePathsInFlagsAbsolute( 113 | compilation_info.compiler_flags_, 114 | compilation_info.compiler_working_dir_ ) 115 | 116 | else: 117 | relative_to = DirectoryOfThisScript() 118 | final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to ) 119 | 120 | return { 121 | 'flags': final_flags, 122 | 'do_cache': True 123 | } 124 | -------------------------------------------------------------------------------- /include/scroller.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "backlog.hpp" 4 | #include "detail/util.hpp" 5 | #include "detail/filter.hpp" 6 | 7 | namespace sts 8 | { 9 | class scroller 10 | { 11 | public: 12 | scroller() = delete; 13 | scroller(backlog &bl, std::size_t const step) 14 | : backlog_{ bl } 15 | , step_{ step } 16 | { } 17 | 18 | template 19 | void write(It const &begin, It end) 20 | { 21 | auto const size(std::distance(begin, end)); 22 | if(following_ && ::write(STDOUT_FILENO, &*begin, size) != size) 23 | { throw std::runtime_error{ "partial/failed write (stdout)" }; } 24 | 25 | end = detail::filter(*this, begin, end); 26 | backlog_.write(begin, end); 27 | 28 | auto &impl(backlog_.get_impl()); 29 | auto const line_markers_size(impl.line_markers_.size()); 30 | auto const rows(impl.tty_.get().size.ws_row); 31 | if(following_ && line_markers_size > rows) 32 | { scroll_pos_ = line_markers_size - rows; } 33 | } 34 | 35 | void up() 36 | { 37 | if(!scroll_pos_) 38 | { return; } 39 | 40 | following_ = false; 41 | scroll_pos_ -= std::min(step_, scroll_pos_); 42 | redraw(); 43 | } 44 | 45 | void down() 46 | { 47 | auto &impl(backlog_.get_impl()); 48 | if(scroll_pos_ + impl.tty_.get().size.ws_row >= impl.line_markers_.size()) 49 | { 50 | following_ = true; 51 | return; 52 | } 53 | ssize_t const diff 54 | { 55 | static_cast(scroll_pos_ + impl.tty_.get().size.ws_row) - 56 | static_cast(impl.line_markers_.size()) 57 | }; 58 | scroll_pos_ += std::min(step_, static_cast(std::abs(diff))); 59 | redraw(); 60 | } 61 | 62 | void follow() 63 | { 64 | auto &impl(backlog_.get_impl()); 65 | if(following_) 66 | { return; } 67 | 68 | scroll_pos_ = impl.line_markers_.size() - impl.tty_.get().size.ws_row; 69 | following_ = true; 70 | redraw(); 71 | } 72 | 73 | void clear() 74 | { 75 | static std::string const clear{ "\x1B[H\x1B[2J" }; 76 | static ssize_t const clear_size(clear.size()); 77 | if(::write(STDOUT_FILENO, clear.c_str(), clear.size()) != clear_size) 78 | { throw std::runtime_error{ "unable to clear screen" }; } 79 | } 80 | 81 | private: 82 | void redraw() 83 | { 84 | clear(); 85 | 86 | auto &impl(backlog_.get_impl()); 87 | std::size_t const rows{ impl.tty_.get().size.ws_row }; 88 | for(std::size_t i{ scroll_pos_ }; 89 | i < scroll_pos_ + std::min(impl.line_markers_.size(), rows); 90 | ++i) 91 | { 92 | ssize_t size((impl.line_markers_[i].second - 93 | impl.line_markers_[i].first) + 1); 94 | if(i == scroll_pos_ + std::min(impl.line_markers_.size() - 1, 95 | rows - 1)) 96 | { --size; } 97 | if(::write(STDOUT_FILENO, &impl.buf_[impl.line_markers_[i].first], 98 | size) != size) 99 | { throw std::runtime_error{ "partial/failed write (stdout)" }; } 100 | } 101 | } 102 | 103 | template 104 | friend It detail::filter(T&, It, It); 105 | 106 | backlog &backlog_; 107 | std::size_t scroll_pos_{}; 108 | std::size_t const step_{}; 109 | bool following_{ true }; 110 | }; 111 | } 112 | --------------------------------------------------------------------------------