diff options
Diffstat (limited to 'src/ui/terminal')
-rw-r--r-- | src/ui/terminal/line.cc | 206 | ||||
-rw-r--r-- | src/ui/terminal/line.h | 101 |
2 files changed, 307 insertions, 0 deletions
diff --git a/src/ui/terminal/line.cc b/src/ui/terminal/line.cc new file mode 100644 index 0000000..8f156cb --- /dev/null +++ b/src/ui/terminal/line.cc @@ -0,0 +1,206 @@ +// Copyright 2021 Benjamin Barenblat +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +#include "src/ui/terminal/line.h" + +#include <errno.h> +#include <signal.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> + +#include <algorithm> +#include <iostream> +#include <stdexcept> +#include <string> +#include <system_error> +#include <thread> +#include <type_traits> + +#include "third_party/abseil/absl/strings/str_cat.h" +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/synchronization/mutex.h" + +namespace ec { + +namespace { + +constexpr absl::string_view kBeginningOfLine = "\r"; + +int CheckedCall(const char* what_arg, int r) { + if (r < 0) { + throw std::system_error(errno, std::generic_category(), what_arg); + } + return r; +} + +bool SigwinchBlocked() { + sigset_t current_blocked; + CheckedCall("pthread_sigmask", + pthread_sigmask(/*how=*/0, /*set=*/nullptr, ¤t_blocked)); + return CheckedCall("sigismember", sigismember(¤t_blocked, SIGWINCH)); +} + +sigset_t SigsetContaining(int signal) { + sigset_t set; + sigemptyset(&set); + CheckedCall("sigaddset", sigaddset(&set, signal)); + return set; +} + +int TerminalColumns() { + winsize size; + CheckedCall("ioctl", ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)); + return size.ws_col; +} + +absl::string_view ClearToEol() noexcept { + static const std::string kSequence = + absl::StrCat("\x1b[K", std::string(3, '\0') /* VT100 padding */); + return kSequence; +} + +} // namespace + +TerminalLine::TerminalLine() { + if (!SigwinchBlocked()) { + throw std::logic_error("TerminalLine constructed without SIGWINCH blocked"); + } + + EnterRawMode(); + + sigwinch_watcher_ = std::thread([this] { + sigset_t sigwinch = SigsetContaining(SIGWINCH); + int received; + while (true) { + sigwait(&sigwinch, &received); + ReportSigwinch(); + } + }); + ReportSigwinch(); // initialize state reset on SIGWINCH +} + +TerminalLine::~TerminalLine() noexcept { + static_assert(std::is_same_v<std::thread::native_handle_type, pthread_t>); + pthread_cancel(sigwinch_watcher_.native_handle()); + sigwinch_watcher_.join(); + + ExitRawMode(); + + // Move the cursor to the start of the next line. + std::cout << '\n'; +} + +void TerminalLine::SetLine(std::string text) { + absl::MutexLock lock(&mu_); + line_ = std::move(text); +} + +void TerminalLine::Refresh() { + absl::MutexLock lock(&mu_); + if (line_.size() < columns_) { + // We can fit the whole line and the cursor on the screen at once. + WriteRaw(absl::StrCat(kBeginningOfLine, line_, ClearToEol())); + } else { + auto to_display = std::min<int>(line_.size(), columns_ - 4); + WriteRaw( + absl::StrCat(kBeginningOfLine, "...", + absl::string_view(&*line_.end() - to_display, to_display), + ClearToEol())); + } +} + +char TerminalLine::GetChar() { + while (true) { + char c; + int r = read(STDIN_FILENO, &c, 1); + if (r > 0) { + return c; + } else if (r == 0) { // EOF + return kControlD; + } else if (errno == EINTR) { + continue; + } else { + throw std::system_error(errno, std::generic_category(), "read"); + } + } +} + +void TerminalLine::Beep() { + absl::MutexLock lock(&mu_); + WriteRaw("\a"); +} + +void TerminalLine::PrintLine(absl::string_view message) { + { + absl::MutexLock lock(&mu_); + WriteRaw(absl::StrCat("\r\n", message, "\r\n")); + } + Refresh(); +} + +void TerminalLine::EnterRawMode() { + CheckedCall("tcgetattr", tcgetattr(STDIN_FILENO, &original_termios_)); + + current_termios_ = original_termios_; + current_termios_.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + current_termios_.c_oflag &= ~(OPOST); + current_termios_.c_cflag |= CS8; + current_termios_.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + CheckedCall("tcsetattr", + tcsetattr(STDIN_FILENO, TCSAFLUSH, ¤t_termios_)); + + // tcsetattr returns successfully if _any_ of its changes were successful, so + // check again to make sure everything went through. + termios actual; + CheckedCall("tcgetattr", tcgetattr(STDIN_FILENO, &actual)); + if (actual.c_iflag & (BRKINT | ICRNL | INPCK | ISTRIP | IXON) || + actual.c_oflag & OPOST || (actual.c_cflag & CS8) != CS8 || + actual.c_lflag & (ECHO | ICANON | IEXTEN | ISIG)) { + throw std::runtime_error("tcsetattr: could not apply all settings"); + } +} + +void TerminalLine::ExitRawMode() noexcept { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios_); +} + +void TerminalLine::ReportSigwinch() { + { + absl::MutexLock lock(&mu_); + columns_ = TerminalColumns(); + } + Refresh(); +} + +void TerminalLine::WriteRaw(absl::string_view bytes) { + while (true) { + int r = write(STDOUT_FILENO, bytes.data(), bytes.size()); + if (r >= 0) { + return; + } else if (errno == EINTR) { + continue; + } else { + throw std::system_error(errno, std::generic_category(), "write"); + } + } +} + +void BlockSigwinch() { + sigset_t sigwinch = SigsetContaining(SIGWINCH); + CheckedCall("pthread_sigmask", + pthread_sigmask(SIG_BLOCK, &sigwinch, /*oldset=*/nullptr)); +} + +} // namespace ec diff --git a/src/ui/terminal/line.h b/src/ui/terminal/line.h new file mode 100644 index 0000000..a90e2b7 --- /dev/null +++ b/src/ui/terminal/line.h @@ -0,0 +1,101 @@ +// Copyright 2021 Benjamin Barenblat +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// A terminal driver for single-line UIs. + +#ifndef EC_SRC_UI_TERMINAL_LINE_H_ +#define EC_SRC_UI_TERMINAL_LINE_H_ + +#include <termios.h> + +#include <thread> + +#include "third_party/abseil/absl/base/thread_annotations.h" +#include "third_party/abseil/absl/strings/string_view.h" +#include "third_party/abseil/absl/synchronization/mutex.h" + +namespace ec { + +// The driver itself. +// +// This class is thread-safe, but there are still some sharp edges. See the +// warning in the constructor documentation about additional steps required to +// use this class in a multi-threaded program. +class TerminalLine final { + public: + // Starts driving the standard input and standard output of the process. + // + // Multi-threading warning: You must block SIGWINCH in all your program's + // threads before constructing an instance of this class. This class detects + // failure to block SIGWINCH in the calling thread, but it cannot check all + // the threads. It is your responsibility to block SIGWINCH everywhere! + // (BlockSigwinch is a convenience function to do this in the current thread.) + explicit TerminalLine(); + + TerminalLine(const TerminalLine&) = delete; + TerminalLine& operator=(const TerminalLine&) = delete; + + ~TerminalLine() noexcept; + + void SetLine(std::string) ABSL_LOCKS_EXCLUDED(mu_); + void Refresh() ABSL_LOCKS_EXCLUDED(mu_); + + void SetLineImmediately(std::string text) { + SetLine(text); + Refresh(); + } + + char GetChar(); + char interrupt_char() const noexcept { return current_termios_.c_cc[VINTR]; } + char quit_char() const noexcept { return current_termios_.c_cc[VQUIT]; } + char suspend_char() const noexcept { return current_termios_.c_cc[VSUSP]; } +#ifdef VDSUSP + char delayed_suspend_char() const noexcept { + return current_termios_.c_cc[VDSUSP]; + } +#endif + + void Beep() ABSL_LOCKS_EXCLUDED(mu_); + + // Prints the specified message on a new line, and redisplays the original + // text on the line after that. + void PrintLine(absl::string_view) ABSL_LOCKS_EXCLUDED(mu_); + + private: + void EnterRawMode(); + void ExitRawMode() noexcept; + + void ReportSigwinch() ABSL_LOCKS_EXCLUDED(mu_); + + void WriteRaw(absl::string_view bytes) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_); + + termios original_termios_, current_termios_; + + std::thread sigwinch_watcher_; + + absl::Mutex mu_; + int columns_ ABSL_GUARDED_BY(mu_); + std::string line_ ABSL_GUARDED_BY(mu_); +}; + +// Names for control characters. +constexpr char kControlD = '\x04'; +constexpr char kControlU = '\x15'; + +// A convenience function to block SIGWINCH in the calling thread. +void BlockSigwinch(); + +} // namespace ec + +#endif // EC_SRC_UI_TERMINAL_LINE_H_ |