aboutsummaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorGravatar Benjamin Barenblat <bbarenblat@gmail.com>2021-12-14 12:34:06 -0500
committerGravatar Benjamin Barenblat <bbarenblat@gmail.com>2021-12-14 12:34:06 -0500
commita4635fb95235ba4bf077bd59957da0626fc5ba72 (patch)
treed029b32b6668adb1412e63d775928e0cb1ceef39 /src/ui
EC, a terminal-based RPN calculator
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/stream.cc84
-rw-r--r--src/ui/stream.h56
-rw-r--r--src/ui/terminal.cc184
-rw-r--r--src/ui/terminal.h35
-rw-r--r--src/ui/terminal/line.cc206
-rw-r--r--src/ui/terminal/line.h101
6 files changed, 666 insertions, 0 deletions
diff --git a/src/ui/stream.cc b/src/ui/stream.cc
new file mode 100644
index 0000000..74e34be
--- /dev/null
+++ b/src/ui/stream.cc
@@ -0,0 +1,84 @@
+// 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/stream.h"
+
+#include <assert.h>
+
+#include <istream>
+#include <memory>
+#include <ostream>
+#include <string>
+
+#include "src/error.h"
+#include "src/language.h"
+#include "src/parser_driver.h"
+#include "third_party/abseil/absl/strings/str_join.h"
+
+namespace ec {
+
+namespace {
+
+std::string ReadAll(std::istream& in) noexcept {
+ std::string result;
+ int bytes_read = 0;
+ while (in.good()) {
+ result.append(4096, '\0');
+ in.read(result.data() + bytes_read, 4096);
+ bytes_read += in.gcount();
+ }
+ assert(bytes_read <= result.size());
+ result.resize(bytes_read);
+ return result;
+}
+
+void ShowState(const State& state, std::ostream& out) noexcept {
+ if (state.stack.empty()) {
+ return;
+ }
+ out << absl::StrJoin(state.stack, " ", FormatStackElement) << '\n';
+}
+
+} // namespace
+
+int LineUi::Main() noexcept {
+ State state;
+ while (in_.good()) {
+ std::string line;
+ std::getline(in_, line);
+ try {
+ EvaluateAll(ParseFromString(line), state);
+ } catch (const Error& e) {
+ std::cerr << "ec: " << e.what() << '\n';
+ return 1;
+ }
+ }
+ ShowState(state, out_);
+ return 0;
+}
+
+int StreamUi::Main() noexcept {
+ State state;
+ std::string program = ReadAll(in_);
+ try {
+ EvaluateAll(ParseFromString(program), state);
+ } catch (const Error& e) {
+ std::cerr << "ec: " << e.what() << '\n';
+ return 1;
+ }
+ ShowState(state, out_);
+ return 0;
+}
+
+} // namespace ec
diff --git a/src/ui/stream.h b/src/ui/stream.h
new file mode 100644
index 0000000..106560b
--- /dev/null
+++ b/src/ui/stream.h
@@ -0,0 +1,56 @@
+// 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.
+
+// Stream-oriented interfaces to EC.
+
+#ifndef EC_SRC_UI_STREAM_H_
+#define EC_SRC_UI_STREAM_H_
+
+#include <iostream>
+#include <istream>
+#include <ostream>
+
+#include "src/ui.h"
+
+namespace ec {
+
+// A line-mode interpreter working on streams.
+class LineUi : public Ui {
+ public:
+ explicit LineUi(std::istream& in = std::cin, std::ostream& out = std::cout)
+ : in_(in), out_(out) {}
+
+ int Main() noexcept override;
+
+ private:
+ std::istream& in_;
+ std::ostream& out_;
+};
+
+// A batch-mode interpreter working on streams.
+class StreamUi : public Ui {
+ public:
+ explicit StreamUi(std::istream& in = std::cin, std::ostream& out = std::cout)
+ : in_(in), out_(out) {}
+
+ int Main() noexcept override;
+
+ private:
+ std::istream& in_;
+ std::ostream& out_;
+};
+
+} // namespace ec
+
+#endif // EC_SRC_UI_STREAM_H_
diff --git a/src/ui/terminal.cc b/src/ui/terminal.cc
new file mode 100644
index 0000000..ccb9c4d
--- /dev/null
+++ b/src/ui/terminal.cc
@@ -0,0 +1,184 @@
+// 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.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "src/builtin.h"
+#include "src/error.h"
+#include "src/language.h"
+#include "src/parser_driver.h"
+#include "src/ui/terminal/line.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/str_join.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+
+namespace ec {
+
+int TerminalUi::Main() noexcept {
+ BlockSigwinch();
+ TerminalLine tty;
+
+ State machine_state;
+ std::string input_buffer;
+ while (true) {
+ tty.SetLineImmediately(absl::StrCat(
+ absl::StrJoin(machine_state.stack, " ", FormatStackElement), " > ",
+ input_buffer));
+
+ char c = tty.GetChar();
+ if (c == tty.interrupt_char() || c == tty.quit_char()) {
+ // Somebody hit Ctrl-C or Ctrl-\.
+ return 0;
+ }
+ switch (c) {
+ case kControlD:
+ if (input_buffer.empty()) {
+ return 0;
+ }
+ tty.Beep();
+ break;
+
+ case kControlU:
+ input_buffer.clear();
+ break;
+
+ case '\n':
+ case '\r':
+ case ' ':
+ try {
+ std::vector<std::shared_ptr<const Term>> program;
+ if (input_buffer.empty()) {
+ program = {std::make_shared<ForeignProgramTerm>(BuiltinDup)};
+ } else {
+ program = ParseFromString(input_buffer);
+ }
+
+ State s = machine_state;
+ EvaluateAll(program, s);
+ machine_state = s;
+ input_buffer.clear();
+ } catch (const Error& e) {
+ tty.Beep();
+ }
+ break;
+
+ case '\x7f':
+ try {
+ if (input_buffer.empty()) {
+ State s = machine_state;
+ ForeignProgramTerm(BuiltinDrop).Evaluate(s);
+ machine_state = s;
+ } else {
+ input_buffer.pop_back();
+ }
+ } catch (const Error& e) {
+ tty.Beep();
+ }
+ break;
+
+ case '+':
+ case '-':
+ case '*':
+ case '/':
+ try {
+ std::vector<std::shared_ptr<const Term>> program =
+ ParseFromString(absl::StrCat(input_buffer, std::string(1, c)));
+ State s = machine_state;
+ EvaluateAll(program, s);
+ machine_state = s;
+ input_buffer.clear();
+ } catch (const Error& e) {
+ tty.Beep();
+ }
+ break;
+
+ case '.':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case 'A':
+ case 'B':
+ case 'C':
+ case 'D':
+ case 'E':
+ case 'F':
+ case 'G':
+ case 'H':
+ case 'I':
+ case 'J':
+ case 'K':
+ case 'L':
+ case 'M':
+ case 'N':
+ case 'O':
+ case 'P':
+ case 'Q':
+ case 'R':
+ case 'S':
+ case 'T':
+ case 'U':
+ case 'V':
+ case 'W':
+ case 'X':
+ case 'Y':
+ case 'Z':
+ case '_':
+ case 'a':
+ case 'b':
+ case 'c':
+ case 'd':
+ case 'e':
+ case 'f':
+ case 'g':
+ case 'h':
+ case 'i':
+ case 'j':
+ case 'k':
+ case 'l':
+ case 'm':
+ case 'n':
+ case 'o':
+ case 'p':
+ case 'q':
+ case 'r':
+ case 's':
+ case 't':
+ case 'u':
+ case 'v':
+ case 'w':
+ case 'x':
+ case 'y':
+ case 'z':
+ input_buffer.push_back(c);
+ break;
+
+ default:
+ tty.Beep();
+ break;
+ }
+ }
+}
+
+} // namespace ec
diff --git a/src/ui/terminal.h b/src/ui/terminal.h
new file mode 100644
index 0000000..22f5d01
--- /dev/null
+++ b/src/ui/terminal.h
@@ -0,0 +1,35 @@
+// 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.
+
+#ifndef EC_SRC_UI_TERMINAL_H_
+#define EC_SRC_UI_TERMINAL_H_
+
+#include "src/ui.h"
+
+namespace ec {
+
+// An interactive interpreter targeted at terminal sessions.
+class TerminalUi : public Ui {
+ public:
+ explicit TerminalUi() = default;
+
+ TerminalUi(const TerminalUi&) = delete;
+ TerminalUi& operator=(const TerminalUi&) = delete;
+
+ int Main() noexcept override;
+};
+
+} // namespace ec
+
+#endif // EC_SRC_UI_TERMINAL_H_
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_