aboutsummaryrefslogtreecommitdiff
path: root/src/ui/terminal
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/terminal')
-rw-r--r--src/ui/terminal/line.cc206
-rw-r--r--src/ui/terminal/line.h101
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, &current_blocked));
+ return CheckedCall("sigismember", sigismember(&current_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, &current_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_