aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Benjamin Barenblat <bbarenblat@gmail.com>2021-12-26 13:53:22 -0500
committerGravatar Benjamin Barenblat <bbarenblat@gmail.com>2021-12-26 14:55:31 -0500
commit4b49b1d0cc23f909d1be89cf8f816f820b343e0a (patch)
tree331616bf0cf1643e9abec6a49e2ead10e58ed0aa
parent520bbd892ada57a5c93680eae3c9d7eb691073af (diff)
Don’t hard-code escape sequences
Instead of hard-coding VT100-compatible escape sequences, parse the system terminfo database and read escape sequences from it. This is both more flexible (it should work well on more terminals) and more efficient (it won’t insert padding on terminals that don’t need it).
-rw-r--r--.gitignore2
-rw-r--r--README.md9
-rw-r--r--buildconf/asan.ninja2
-rw-r--r--buildconf/common.ninja97
-rw-r--r--buildconf/dbg.ninja2
-rw-r--r--buildconf/fastbuild.ninja2
-rw-r--r--goldfishterm/internal/emit.cc47
-rw-r--r--goldfishterm/internal/emit.h106
-rw-r--r--goldfishterm/internal/string_capability.h106
-rw-r--r--goldfishterm/internal/string_capability.re483
-rw-r--r--goldfishterm/internal/string_capability_test.cc372
-rw-r--r--goldfishterm/simple.cc74
-rw-r--r--goldfishterm/simple.h71
-rw-r--r--goldfishterm/terminfo.cc325
-rw-r--r--goldfishterm/terminfo.h544
-rw-r--r--goldfishterm/terminfo_system_test.cc93
-rw-r--r--goldfishterm/terminfo_test.cc228
-rwxr-xr-xscripts/run_all_tests16
-rw-r--r--src/ui/terminal/line.cc47
-rw-r--r--src/ui/terminal/line.h3
20 files changed, 2587 insertions, 42 deletions
diff --git a/.gitignore b/.gitignore
index 0d4adcb..8f16e59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,8 @@
.ninja_*
+goldfishterm/internal/string_capability.cc
+
*.interp
*.tokens
src/CalculatorBaseVisitor.*
diff --git a/README.md b/README.md
index 372a089..96043ef 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,8 @@ heavy-duty numerical analysis, EC is not yet the tool for you.
To build EC, you’ll need [our customized version of
Abseil](https://git.benjamin.barenblat.name/ec-abseil/), which is checked in as
a Git submodule. You’ll also need GCC, [Antlr&nbsp;4](https://www.antlr.org/),
-and [Ninja](https://ninja-build.org/), none of which is checked in; on a Debian
-system, you can run `apt install build-essential ninja-build antlr4
-libantlr4-runtime-dev` to get the packages you need. Fire up `ninja`, wait a
-bit, and you’ll soon have an `ec` binary in the repository root. Enjoy!
+[re2c](https://re2c.org/), and [Ninja](https://ninja-build.org/), none of which
+is checked in; on a Debian system, you can run `apt install build-essential
+ninja-build antlr4 libantlr4-runtime-dev re2c` to get the packages you need.
+Fire up `ninja`, wait a bit, and you’ll soon have an `ec` binary in the
+repository root. Enjoy!
diff --git a/buildconf/asan.ninja b/buildconf/asan.ninja
index 8f79f9e..61f1fbc 100644
--- a/buildconf/asan.ninja
+++ b/buildconf/asan.ninja
@@ -17,4 +17,6 @@ third_party_cxxflags = -fsanitize=address
ldflags = -fsanitize=address
+re2cflags = -Werror
+
subninja buildconf/common.ninja
diff --git a/buildconf/common.ninja b/buildconf/common.ninja
index 256df10..a2dab9d 100644
--- a/buildconf/common.ninja
+++ b/buildconf/common.ninja
@@ -27,6 +27,8 @@ cxxflags = $common_cxxflags -Wall -Wextra -Wno-logical-op-parentheses $
-Wno-sign-compare $cxxflags
third_party_cxxflags = $common_cxxflags $third_party_cxxflags
+re2cflags = --empty-class error --no-generation-date -W $re2cflags
+
rule antlr
command = antlr4 $antlrflags $in
description = Generating parser from $in
@@ -45,6 +47,61 @@ rule link
command = g++ $ldflags -o $out $in $libs -pthread
description = Linking $out
+rule re2c
+ command = re2c $re2cflags $in -o $out
+ description = Generating DFAs in $out
+
+build goldfishterm/internal/emit.o: cxx goldfishterm/internal/emit.cc
+build goldfishterm/internal/string_capability.cc: re2c $
+ goldfishterm/internal/string_capability.re
+ re2cflags = $re2cflags
+build goldfishterm/internal/string_capability.o: cxx $
+ goldfishterm/internal/string_capability.cc
+build goldfishterm/simple.o: cxx goldfishterm/simple.cc
+build goldfishterm/terminfo.o: cxx goldfishterm/terminfo.cc
+
+build goldfishterm/internal/string_capability_test.o: cxx $
+ goldfishterm/internal/string_capability_test.cc
+build goldfishterm/internal/string_capability_test: link $
+ goldfishterm/internal/emit.o goldfishterm/internal/string_capability.o $
+ goldfishterm/internal/string_capability_test.o goldfishterm/terminfo.o $
+ $absl/strings/cord.a $absl/strings/cordz_info.a $
+ $absl/strings/cord_internal.a $absl/strings/cordz_functions.a $
+ $absl/strings/cordz_handle.a $absl/hash/hash.a $absl/hash/city.a $
+ $absl/types/bad_variant_access.a $absl/hash/low_level_hash.a $
+ $absl/container/raw_hash_set.a $absl/container/hashtablez_sampler.a $
+ $absl/profiling/exponential_biased.a $
+ $absl/synchronization/synchronization.a $
+ $absl/synchronization/graphcycles_internal.a $absl/debugging/stacktrace.a $
+ $absl/debugging/symbolize.a $absl/debugging/debugging_internal.a $
+ $absl/debugging/demangle_internal.a $absl/base/malloc_internal.a $
+ $absl/strings/str_format_internal.a $absl/time/time.a $
+ $absl/time/civil_time.a $absl/strings/strings.a $absl/strings/internal.a $
+ $absl/base/base.a $absl/base/spinlock_wait.a $absl/numeric/int128.a $
+ $absl/time/time_zone.a $absl/types/bad_optional_access.a $
+ $absl/base/throw_delegate.a $
+ $absl/base/raw_logging_internal.a $absl/base/log_severity.a
+ libs = -lgmock_main -lgmock -lgtest -lm -lrt
+
+build goldfishterm/terminfo_system_test.o: cxx $
+ goldfishterm/terminfo_system_test.cc
+build goldfishterm/terminfo_system_test: link goldfishterm/terminfo.o $
+ goldfishterm/terminfo_system_test.o $absl/types/bad_optional_access.a $
+ $absl/strings/str_format_internal.a $absl/strings/strings.a $
+ $absl/strings/internal.a $absl/base/base.a $absl/base/spinlock_wait.a $
+ $absl/numeric/int128.a $absl/base/throw_delegate.a $
+ $absl/base/raw_logging_internal.a $absl/base/log_severity.a
+ libs = -lrt
+
+build goldfishterm/terminfo_test.o: cxx goldfishterm/terminfo_test.cc
+build goldfishterm/terminfo_test: link goldfishterm/terminfo.o $
+ goldfishterm/terminfo_test.o $absl/strings/str_format_internal.a $
+ $absl/strings/strings.a $absl/strings/internal.a $absl/base/base.a $
+ $absl/base/spinlock_wait.a $absl/numeric/int128.a $
+ $absl/base/throw_delegate.a $absl/types/bad_optional_access.a $
+ $absl/base/raw_logging_internal.a $absl/base/log_severity.a
+ libs = -lgmock_main -lgmock -lgtest -lrt
+
build | src/CalculatorBaseVisitor.cpp src/CalculatorBaseVisitor.h $
src/Calculator.interp src/CalculatorLexer.cpp src/CalculatorLexer.h $
src/CalculatorLexer.interp src/CalculatorLexer.tokens $
@@ -128,21 +185,24 @@ build src/parser_driver_test: link src/builtin.o src/CalculatorBaseVisitor.o $
build ec: link src/CalculatorBaseVisitor.o src/CalculatorLexer.o $
src/CalculatorParser.o src/CalculatorVisitor.o src/builtin.o $
src/language.o src/main.o src/parser_driver.o src/util.o src/ui/stream.o $
- src/ui/terminal.o src/ui/terminal/line.o $absl/strings/cord.a $
- $absl/strings/cordz_info.a $absl/strings/cord_internal.a $
- $absl/strings/cordz_functions.a $absl/strings/cordz_handle.a $
- $absl/hash/hash.a $absl/hash/city.a $absl/types/bad_variant_access.a $
+ src/ui/terminal.o src/ui/terminal/line.o goldfishterm/internal/emit.o $
+ goldfishterm/internal/string_capability.o goldfishterm/simple.o $
+ goldfishterm/terminfo.o $absl/strings/cord.a $absl/strings/cordz_info.a $
+ $absl/strings/cord_internal.a $absl/strings/cordz_functions.a $
+ $absl/strings/cordz_handle.a $absl/hash/hash.a $absl/hash/city.a $
$absl/hash/low_level_hash.a $absl/container/raw_hash_set.a $
- $absl/types/bad_optional_access.a $absl/container/hashtablez_sampler.a $
- $absl/profiling/exponential_biased.a $
+ $absl/container/hashtablez_sampler.a $absl/profiling/exponential_biased.a $
+ $absl/strings/str_format_internal.a $
$absl/synchronization/synchronization.a $
$absl/synchronization/graphcycles_internal.a $absl/debugging/stacktrace.a $
$absl/debugging/symbolize.a $absl/debugging/debugging_internal.a $
$absl/debugging/demangle_internal.a $absl/base/malloc_internal.a $
$absl/time/time.a $absl/time/civil_time.a $absl/strings/strings.a $
$absl/strings/internal.a $absl/base/base.a $absl/base/spinlock_wait.a $
- $absl/numeric/int128.a $absl/time/time_zone.a $absl/base/throw_delegate.a $
- $absl/base/raw_logging_internal.a $absl/base/log_severity.a
+ $absl/numeric/int128.a $absl/time/time_zone.a $
+ $absl/types/bad_optional_access.a $absl/base/throw_delegate.a $
+ $absl/types/bad_variant_access.a $absl/base/raw_logging_internal.a $
+ $absl/base/log_severity.a
libs = -lantlr4-runtime -lm -lrt
# //absl/base
@@ -372,6 +432,27 @@ build $absl/strings/internal/ostringstream.o: cxx $
build $absl/strings/internal/utf8.o: cxx $absl/strings/internal/utf8.cc
cxxflags = $third_party_cxxflags
+# //absl/strings:str_format_internal
+build $absl/strings/str_format_internal.a: ar $
+ $absl/strings/internal/str_format/arg.o $
+ $absl/strings/internal/str_format/bind.o $
+ $absl/strings/internal/str_format/extension.o $
+ $absl/strings/internal/str_format/float_conversion.o $
+ $absl/strings/internal/str_format/output.o $
+ $absl/strings/internal/str_format/parser.o
+build $absl/strings/internal/str_format/arg.o: cxx $absl/strings/internal/str_format/arg.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/str_format/bind.o: cxx $absl/strings/internal/str_format/bind.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/str_format/extension.o: cxx $absl/strings/internal/str_format/extension.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/str_format/float_conversion.o: cxx $absl/strings/internal/str_format/float_conversion.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/str_format/output.o: cxx $absl/strings/internal/str_format/output.cc
+ cxxflags = $third_party_cxxflags
+build $absl/strings/internal/str_format/parser.o: cxx $absl/strings/internal/str_format/parser.cc
+ cxxflags = $third_party_cxxflags
+
# //absl/synchronization
build $absl/synchronization/synchronization.a: ar $
$absl/synchronization/barrier.o $absl/synchronization/blocking_counter.o $
diff --git a/buildconf/dbg.ninja b/buildconf/dbg.ninja
index cce13eb..4486e80 100644
--- a/buildconf/dbg.ninja
+++ b/buildconf/dbg.ninja
@@ -15,4 +15,6 @@
cxxflags = -Og -g3 -Werror
third_party_cxxflags = -Og -g3
+re2cflags = -Werror
+
subninja buildconf/common.ninja
diff --git a/buildconf/fastbuild.ninja b/buildconf/fastbuild.ninja
index c321e30..8cd042a 100644
--- a/buildconf/fastbuild.ninja
+++ b/buildconf/fastbuild.ninja
@@ -14,4 +14,6 @@
cxxflags = -Werror
+re2cflags = -Werror
+
subninja buildconf/common.ninja
diff --git a/goldfishterm/internal/emit.cc b/goldfishterm/internal/emit.cc
new file mode 100644
index 0000000..2cc7609
--- /dev/null
+++ b/goldfishterm/internal/emit.cc
@@ -0,0 +1,47 @@
+// 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 "goldfishterm/internal/emit.h"
+
+#include <errno.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include <memory>
+#include <system_error>
+#include <vector>
+
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/time/clock.h"
+#include "third_party/abseil/absl/types/span.h"
+
+namespace goldfishterm_internal {
+
+void EmitBytes::Emit(std::ostream& out) const {
+ out.write(bytes_.data(), bytes_.size());
+}
+
+void EmitDelay::Emit(std::ostream& out) const noexcept {
+ out.flush();
+ absl::SleepFor(delay_);
+}
+
+void Emit(absl::Span<const std::shared_ptr<const EmitTerm>> terms,
+ std::ostream& out) {
+ for (const auto& term : terms) {
+ term->Emit(out);
+ }
+}
+
+} // namespace goldfishterm_internal
diff --git a/goldfishterm/internal/emit.h b/goldfishterm/internal/emit.h
new file mode 100644
index 0000000..371f6b0
--- /dev/null
+++ b/goldfishterm/internal/emit.h
@@ -0,0 +1,106 @@
+// 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.
+
+// An extremely simple EDSL for describing output to the terminal. It supports
+// exactly two instructions: "send some bytes to the terminal" and "sleep". The
+// string capability interpreter produces terms in this EDSL, which makes it
+// easier to test.
+
+#ifndef EC_GOLDFISHTERM_INTERNAL_EMIT_H_
+#define EC_GOLDFISHTERM_INTERNAL_EMIT_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <utility>
+
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/time/time.h"
+#include "third_party/abseil/absl/types/span.h"
+
+namespace goldfishterm_internal {
+
+class EmitTerm {
+ public:
+ virtual void Emit(std::ostream&) const = 0;
+
+ virtual ~EmitTerm() noexcept = default;
+};
+
+class EmitBytes final : public EmitTerm {
+ public:
+ explicit EmitBytes() noexcept = default;
+
+ explicit EmitBytes(absl::string_view bytes) noexcept : bytes_(bytes) {}
+
+ EmitBytes(const EmitBytes&) = default;
+ EmitBytes& operator=(const EmitBytes&) = default;
+ EmitBytes(EmitBytes&&) noexcept = default;
+ EmitBytes& operator=(EmitBytes&&) noexcept = default;
+
+ bool empty() const noexcept { return bytes_.empty(); }
+
+ void Append(absl::string_view more) noexcept {
+ absl::StrAppend(&bytes_, more);
+ }
+
+ void Emit(std::ostream&) const override;
+
+ bool operator==(const EmitBytes& other) const noexcept {
+ return bytes_ == other.bytes_;
+ }
+
+ friend std::ostream& operator<<(std::ostream& out,
+ const EmitBytes& emit) noexcept {
+ return out << "EmitBytes(" << emit.bytes_ << ')';
+ }
+
+ private:
+ std::string bytes_;
+};
+
+class EmitDelay final : public EmitTerm {
+ public:
+ explicit EmitDelay() noexcept = default;
+
+ explicit EmitDelay(absl::Duration delay) : delay_(std::move(delay)) {}
+
+ EmitDelay(const EmitDelay&) = default;
+ EmitDelay& operator=(const EmitDelay&) = default;
+ EmitDelay(EmitDelay&&) noexcept = default;
+ EmitDelay& operator=(EmitDelay&&) noexcept = default;
+
+ bool zero() const noexcept { return delay_ == absl::ZeroDuration(); }
+
+ void Increase(absl::Duration more) noexcept { delay_ += more; }
+
+ void Emit(std::ostream&) const noexcept override;
+
+ bool operator==(const EmitDelay& other) const noexcept {
+ return delay_ == other.delay_;
+ }
+
+ private:
+ absl::Duration delay_;
+};
+
+// Runs an entire program in the emitter EDSL.
+void Emit(absl::Span<const std::shared_ptr<const EmitTerm>>, std::ostream&);
+
+} // namespace goldfishterm_internal
+
+#endif // EC_GOLDFISHTERM_INTERNAL_EMIT_H_
diff --git a/goldfishterm/internal/string_capability.h b/goldfishterm/internal/string_capability.h
new file mode 100644
index 0000000..ddae964
--- /dev/null
+++ b/goldfishterm/internal/string_capability.h
@@ -0,0 +1,106 @@
+// 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.
+
+// Parses and interprets terminfo string capabilities. These are written in a
+// simple stack-based programming language whose semantics are documented in
+// terminfo(5).
+
+#ifndef EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_
+#define EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "goldfishterm/internal/emit.h"
+#include "goldfishterm/terminfo.h"
+#include "third_party/abseil/absl/types/optional.h"
+#include "third_party/abseil/absl/types/variant.h"
+
+namespace goldfishterm_internal {
+
+using StringCapabilityParameter = absl::variant<int, std::string>;
+
+struct InterpretStringCapabilityInput {
+ std::string capability;
+ std::vector<StringCapabilityParameter> parameters;
+
+ // The number of lines affected by this operation.
+ int lines_affected;
+
+ // The minimum baud rate at which padding should be expanded.
+ int padding_baud_rate;
+
+ // The padding character, or absl::nullopt if the terminal doesn't support
+ // padding.
+ absl::optional<char> pad_char;
+
+ // True iff the terminal supports XON/XOFF handshaking.
+ bool has_xon_xoff;
+
+ // The baud rate of the terminal.
+ int baud;
+
+ // The number of extra bits sent per character. For a terminal emulator, this
+ // is generally 0; for an RS-232 line, it's generally 2 or 3 (one start bit,
+ // one stop bit, and possibly one parity bit).
+ int extra_bits_per_character;
+
+ // Constructs an InterpretStringCapabilityInput with fail-safe defaults.
+ explicit InterpretStringCapabilityInput() noexcept
+ : lines_affected(1),
+ padding_baud_rate(0),
+ pad_char('\0'),
+ has_xon_xoff(false),
+ baud(9600),
+ extra_bits_per_character(0) {}
+
+ // Constructs an InterpretStringCapabilityInput from a terminfo entry.
+ explicit InterpretStringCapabilityInput(
+ const goldfishterm::TerminfoEntry& terminfo, std::string capability_,
+ std::vector<StringCapabilityParameter> parameters_, int baud_,
+ int lines_affected_) noexcept
+ : capability(std::move(capability_)),
+ parameters(std::move(parameters_)),
+ lines_affected(lines_affected_),
+ padding_baud_rate(
+ terminfo.get(goldfishterm::NumericCapability::kPaddingBaudRate)
+ .value_or(0)),
+ pad_char(terminfo.get(goldfishterm::StringCapability::kCharPadding)
+ .value_or(std::string(1, '\0'))[0]),
+ has_xon_xoff(terminfo.get(goldfishterm::BooleanCapability::kXonXoff)),
+ baud(baud_),
+ extra_bits_per_character(0) {}
+};
+
+struct InterpretStringCapabilityResult {
+ // Terms in the emitter EDSL (see emit.h).
+ std::vector<std::shared_ptr<const EmitTerm>> terms;
+
+ // The expected cost of emitting these terms. This is a unitless, arbitrarily
+ // scaled real number; it will be at least 0, but you should not rely on any
+ // absolute semantics.
+ float cost;
+};
+
+// Interprets a terminfo string capability, expanding parameters and inserting
+// padding and delays as appropriate. Throws std::runtime_error if parsing or
+// interpretation fails.
+InterpretStringCapabilityResult InterpretStringCapability(
+ const InterpretStringCapabilityInput&);
+
+} // namespace goldfishterm_internal
+
+#endif // EC_GOLDFISHTERM_INTERNAL_STRING_CAPABILITY_H_
diff --git a/goldfishterm/internal/string_capability.re b/goldfishterm/internal/string_capability.re
new file mode 100644
index 0000000..6920066
--- /dev/null
+++ b/goldfishterm/internal/string_capability.re
@@ -0,0 +1,483 @@
+// 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 <math.h>
+
+#include <functional>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "goldfishterm/internal/string_capability.h"
+#include "third_party/abseil/absl/container/flat_hash_map.h"
+#include "third_party/abseil/absl/strings/match.h"
+#include "third_party/abseil/absl/strings/numbers.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/str_format.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/time/time.h"
+#include "third_party/abseil/absl/types/variant.h"
+
+namespace goldfishterm_internal {
+
+namespace {
+
+absl::string_view Slice(const char* start, const char* end) noexcept {
+ return absl::string_view(start, end - start);
+}
+
+// The string capability interpreter, implemented as an re2c-generated DFA.
+class Interpreter final {
+ public:
+ explicit Interpreter(InterpretStringCapabilityInput input) noexcept
+ : input_(std::move(input)),
+ chars_per_ms_(input_.baud / 1000.0 /
+ (8 * sizeof(char) + input_.extra_bits_per_character)) {
+ result_.cost = 0;
+ }
+
+ void Run() {
+ enum ConditionalEvaluationState {
+ kNormal,
+ kEvaluatingThen,
+ kSkippingThen,
+ kSkippingElse,
+ } if_state = kNormal;
+
+ const char* YYCURSOR = input_.capability.c_str();
+ const char* YYMARKER;
+ const char* a;
+ const char* b;
+ /*!stags:re2c format = "const char* @@;"; */
+
+ loop:
+ /*!re2c
+ re2c:define:YYCTYPE = char;
+ re2c:flags:tags = 1;
+ re2c:yyfill:enable = 0;
+
+ integer = [0-9]+;
+ decimal = integer ("." integer)?;
+
+ [\x00] { goto done; }
+
+ "$<" @a ( "0"* [1-9] "0"* ("." "0"*)? | "0"* "." "0"* [1-9] "0"* ) @b "*"?
+ "/"? ">" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse ||
+ input_.baud < input_.padding_baud_rate) {
+ goto loop;
+ }
+
+ float delay_ms;
+ if (!absl::SimpleAtof(Slice(a, b), &delay_ms)) {
+ throw std::logic_error(
+ "goldfishterm: parser produced an invalid float");
+ }
+ bool per_line_pad = *b == '*';
+ bool padding_mandatory = *(YYCURSOR - 2) == '/';
+ if (per_line_pad) {
+ delay_ms *= input_.lines_affected;
+ }
+ if (input_.has_xon_xoff && !padding_mandatory) {
+ // Record that we expect a delay, but don't actually insert a delay.
+ ExpectDelay(absl::Milliseconds(delay_ms));
+ } else if (input_.pad_char.has_value()) {
+ Bytes(std::string(ceilf(delay_ms * chars_per_ms_), *input_.pad_char));
+ } else {
+ Delay(absl::Milliseconds(delay_ms));
+ }
+ goto loop;
+ }
+
+ "%%" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ Bytes("%");
+ goto loop;
+ }
+
+ "%" @a ((":"? [-+# ]+)? decimal? [doxX] | ":-"? integer? "c") {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ EmitPopped<int>(Slice(a, YYCURSOR));
+ goto loop;
+ }
+
+ "%" @a ":-"? decimal? "s" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ EmitPopped<std::string>(Slice(a, YYCURSOR));
+ goto loop;
+ }
+
+ "%p" [1-9] {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ int index = *(YYCURSOR - 1) - '0';
+ try {
+ stack_.push_back(input_.parameters.at(index - 1));
+ } catch (const std::out_of_range& e) {
+ throw std::runtime_error(
+ absl::StrCat(absl::StrCat("invalid parameter ", index)));
+ }
+ goto loop;
+ }
+
+ "%P" [A-Za-z] {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ RequireNonempty();
+ environment_[*(YYCURSOR - 1)] = std::move(stack_.back());
+ stack_.pop_back();
+ goto loop;
+ }
+
+ "%g" [A-Za-z] {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ char var = *(YYCURSOR - 1);
+ auto it = environment_.find(var);
+ if (it == environment_.end()) {
+ throw std::runtime_error(absl::StrCat("undefined variable ",
+ Slice(YYCURSOR - 1, YYCURSOR)));
+ }
+ stack_.push_back(it->second);
+ goto loop;
+ }
+
+ "%'" [^\x00] "'" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ stack_.push_back(*(YYCURSOR - 2));
+ goto loop;
+ }
+
+ "%{" @a integer "}" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ int value;
+ if (!absl::SimpleAtoi(Slice(a, YYCURSOR - 1), &value)) {
+ throw std::logic_error(
+ "goldfishterm: parser produced an invalid int");
+ }
+ stack_.push_back(value);
+ goto loop;
+ }
+
+ "%l" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ Unop<std::string, int>(
+ [](const std::string& s) { return static_cast<int>(s.size()); });
+ goto loop;
+ }
+
+ "%+" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::plus<int>());
+ goto loop;
+ }
+
+ "%-" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::minus<int>());
+ goto loop;
+ }
+
+ "%*" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::multiplies<int>());
+ goto loop;
+ }
+
+ "%/" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::divides<int>());
+ goto loop;
+ }
+
+ "%m" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::modulus<int>());
+ goto loop;
+ }
+
+ "%&" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::bit_and<int>());
+ goto loop;
+ }
+
+ "%|" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::bit_or<int>());
+ goto loop;
+ }
+
+ "%^" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::bit_xor<int>());
+ goto loop;
+ }
+
+ "%=" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::equal_to<int>());
+ goto loop;
+ }
+
+ "%>" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::greater<int>());
+ goto loop;
+ }
+
+ "%<" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::less<int>());
+ goto loop;
+ }
+
+ "%A" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::logical_and<int>());
+ goto loop;
+ }
+
+ "%O" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ BinopN(std::logical_or<int>());
+ goto loop;
+ }
+
+ "%!" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ UnopN(std::logical_not<int>());
+ goto loop;
+ }
+
+ "%~" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ UnopN(std::bit_not<int>());
+ goto loop;
+ }
+
+ "%i" {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ if (input_.parameters.size() < 2) {
+ throw std::runtime_error("%i requires at least two parameters");
+ }
+ auto* one = absl::get_if<int>(&input_.parameters[0]);
+ auto* two = absl::get_if<int>(&input_.parameters[1]);
+ RequireType<int>(one);
+ RequireType<int>(two);
+ ++*one;
+ ++*two;
+ goto loop;
+ }
+
+ "%?" {
+ if (if_state != kNormal) {
+ throw std::logic_error("goldfishterm: unexpected %?");
+ }
+ goto loop;
+ }
+ "%t" {
+ if (if_state == kNormal) {
+ RequireNonempty();
+ int* condition_int = absl::get_if<int>(&stack_.back());
+ // Strings are true, as are nonzero integers.
+ bool condition = condition_int == nullptr || *condition_int;
+ stack_.pop_back();
+ if_state = condition ? kEvaluatingThen : kSkippingThen;
+ } else if (if_state != kSkippingElse) {
+ throw std::logic_error("goldfishterm: unexpected %t");
+ }
+ goto loop;
+ }
+ "%e" {
+ switch (if_state) {
+ case kNormal:
+ throw std::logic_error("goldfishterm: unexpected %e");
+ break;
+ case kEvaluatingThen:
+ if_state = kSkippingElse;
+ break;
+ case kSkippingThen:
+ if_state = kNormal;
+ break;
+ case kSkippingElse:
+ break;
+ }
+ goto loop;
+ }
+ "%;" {
+ if_state = kNormal;
+ goto loop;
+ }
+
+ * {
+ if (if_state == kSkippingThen || if_state == kSkippingElse) goto loop;
+
+ Bytes(Slice(YYCURSOR - 1, YYCURSOR));
+ goto loop;
+ }
+ */
+
+ done:
+ Flush();
+ }
+
+ const InterpretStringCapabilityResult& result() noexcept { return result_; }
+
+ private:
+ void RequireNonempty() {
+ if (stack_.empty()) {
+ throw std::runtime_error("stack underflow");
+ }
+ }
+
+ // A companion to absl::get_if: Requires that the passed pointer is nonnull
+ // and throws an appropriate error if it's not.
+ template <typename T>
+ void RequireType(const void* v) {
+ if (v == nullptr) {
+ if constexpr (std::is_same_v<T, const int>) {
+ throw std::runtime_error("type error: expected integer");
+ } else if constexpr (std::is_same_v<T, const std::string>) {
+ throw std::runtime_error("type error: expected string");
+ } else {
+ throw std::runtime_error("type error");
+ }
+ }
+ }
+
+ // Executes push(f(pop())).
+ template <typename T, typename R>
+ void Unop(std::function<R(T)> f) {
+ RequireNonempty();
+ T* v = absl::get_if<T>(&stack_.back());
+ RequireType<int>(v);
+ stack_.back() = f(*v);
+ }
+
+ // Executes x = pop(); push(f(pop(), x)).
+ template <typename T1, typename T2, typename R>
+ void Binop(std::function<R(T1, T2)> f) {
+ if (stack_.size() < 2) {
+ throw std::runtime_error("stack underflow during binary operation");
+ }
+ int target = stack_.size() - 2;
+ T1* one = absl::get_if<T1>(&stack_[target]);
+ T2* two = absl::get_if<T2>(&stack_.back());
+ RequireType<T1>(one);
+ RequireType<T2>(two);
+ stack_[target] = f(*one, *two);
+ stack_.pop_back();
+ }
+
+ // Convenience specializations for Unop and Binop over integers.
+ void UnopN(std::function<int(int)> f) { Unop<int, int>(f); }
+ void BinopN(std::function<int(int, int)> f) { Binop<int, int, int>(f); }
+
+ template <typename T>
+ void EmitPopped(absl::string_view format) {
+ if (format[0] == ':') {
+ // The colon is used by to disambiguate parsing. It isn't interesting to
+ // absl::FormatUntyped.
+ format = format.substr(1);
+ }
+
+ std::string out;
+ auto* param = absl::get_if<T>(&stack_.back());
+ RequireType<T>(param);
+ if (!absl::FormatUntyped(&out,
+ absl::UntypedFormatSpec(absl::StrCat("%", format)),
+ {absl::FormatArg(*param)})) {
+ throw std::logic_error(
+ absl::StrCat("absl::FormatUntyped failed with format ", format));
+ }
+ Bytes(out);
+ stack_.pop_back();
+ }
+
+ void ExpectDelay(absl::Duration delay) noexcept {
+ result_.cost += absl::ToDoubleMilliseconds(delay) * chars_per_ms_;
+ }
+
+ void Delay(absl::Duration delay) noexcept {
+ if (!pending_bytes_.empty()) {
+ Flush();
+ }
+ pending_delay_.Increase(delay);
+ ExpectDelay(delay);
+ }
+
+ void Bytes(absl::string_view bytes) noexcept {
+ pending_bytes_.Append(bytes);
+ result_.cost += bytes.size();
+ }
+
+ void Flush() noexcept {
+ if (!pending_delay_.zero()) {
+ result_.terms.push_back(
+ std::make_shared<EmitDelay>(std::move(pending_delay_)));
+ };
+ if (!pending_bytes_.empty()) {
+ result_.terms.push_back(
+ std::make_shared<EmitBytes>(std::move(pending_bytes_)));
+ };
+ }
+
+ InterpretStringCapabilityInput input_;
+ double chars_per_ms_;
+
+ std::vector<StringCapabilityParameter> stack_;
+ absl::flat_hash_map<char, StringCapabilityParameter> environment_;
+
+ EmitDelay pending_delay_;
+ EmitBytes pending_bytes_;
+
+ InterpretStringCapabilityResult result_;
+};
+
+} // namespace
+
+InterpretStringCapabilityResult InterpretStringCapability(
+ const InterpretStringCapabilityInput& input) {
+ Interpreter interp(input);
+ interp.Run();
+ return interp.result();
+}
+
+} // namespace goldfishterm_internal
diff --git a/goldfishterm/internal/string_capability_test.cc b/goldfishterm/internal/string_capability_test.cc
new file mode 100644
index 0000000..a821551
--- /dev/null
+++ b/goldfishterm/internal/string_capability_test.cc
@@ -0,0 +1,372 @@
+// 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 "goldfishterm/internal/string_capability.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <string.h>
+
+#include <ostream>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "goldfishterm/internal/emit.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/time/time.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace goldfishterm_internal {
+namespace {
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+using ::testing::Pointee;
+using ::testing::Property;
+using ::testing::StrEq;
+using ::testing::WhenDynamicCastTo;
+
+MATCHER_P(PointsToEmitBytes, s,
+ absl::StrCat("points to EmitBytes('", s, "')")) {
+ const char* start;
+ int length;
+ if constexpr (std::is_same_v<decltype(s), const char* const>) {
+ start = s;
+ length = strlen(s);
+ } else {
+ start = s.data();
+ length = s.size();
+ }
+ return ExplainMatchResult(Pointee(WhenDynamicCastTo<const EmitBytes&>(
+ EmitBytes(absl::string_view(start, length)))),
+ arg, result_listener);
+}
+
+MATCHER_P(PointsToEmitDelay, t,
+ absl::StrCat("points to EmitDelay('", absl::FormatDuration(t),
+ "')")) {
+ return ExplainMatchResult(
+ Pointee(WhenDynamicCastTo<const EmitDelay&>(EmitDelay(t))), arg,
+ result_listener);
+}
+
+InterpretStringCapabilityResult Interpret(
+ std::string text, std::vector<StringCapabilityParameter> parameters = {}) {
+ InterpretStringCapabilityInput input;
+ input.capability = std::move(text);
+ input.parameters = std::move(parameters);
+ return InterpretStringCapability(input);
+}
+
+TEST(InterpretStringCapabilityTest, PadInteger) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.baud = 8'000; // 1 character every millisecond
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, PadFractional) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<.9>";
+ input.baud = 80'000; // 10 characters every millisecond
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(9, '\0'))));
+ EXPECT_EQ(result.cost, 9);
+}
+
+TEST(InterpretStringCapabilityTest, PadIgnoredIfMultipleSignificantFigures) {
+ EXPECT_THAT(Interpret("$<1.1>").terms,
+ ElementsAre(PointsToEmitBytes("$<1.1>")));
+}
+
+TEST(InterpretStringCapabilityTest, PerLinePad) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<4*>";
+ input.lines_affected = 10;
+ input.baud = 8'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, PadIgnoredIfTerminalTooSlow) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.padding_baud_rate = 115200;
+ input.baud = 9600;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms, IsEmpty());
+ EXPECT_EQ(result.cost, 0);
+}
+
+TEST(InterpretStringCapabilityTest, PadWithWeirdCharacter) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<.1>";
+ input.pad_char = 'a';
+ input.baud = 9600;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("a")));
+ EXPECT_EQ(result.cost, 1);
+}
+
+TEST(InterpretStringCapabilityTest, SleepIfPadUnsupported) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.pad_char = absl::nullopt;
+ input.baud = 16'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitDelay(absl::Milliseconds(40))));
+ EXPECT_EQ(result.cost, 80); // the equivalent of 80 characters
+}
+
+TEST(InterpretStringCapabilityTest, PadWith8N1) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.baud = 10'000; // 1 character every millisecond using
+ input.extra_bits_per_character = 2; // one start and one stop bit
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, PadIgnoredIfXonXoffAvailable) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40>";
+ input.has_xon_xoff = true;
+ input.baud = 8'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms, IsEmpty());
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, MandatoryPadSentEvenIfXonXoffAvailable) {
+ InterpretStringCapabilityInput input;
+ input.capability = "$<40/>";
+ input.has_xon_xoff = true;
+ input.baud = 8'000;
+ InterpretStringCapabilityResult result = InterpretStringCapability(input);
+ EXPECT_THAT(result.terms,
+ ElementsAre(PointsToEmitBytes(std::string(40, '\0'))));
+ EXPECT_EQ(result.cost, 40);
+}
+
+TEST(InterpretStringCapabilityTest, Empty) {
+ InterpretStringCapabilityResult result = Interpret("");
+ EXPECT_THAT(result.terms, IsEmpty());
+ EXPECT_EQ(result.cost, 0);
+}
+
+TEST(InterpretStringCapabilityTest, EmitLiteralPercent) {
+ InterpretStringCapabilityResult result = Interpret("%%");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("%")));
+ EXPECT_EQ(result.cost, 1);
+}
+
+TEST(InterpretStringCapabilityTest, EmitChar) {
+ InterpretStringCapabilityResult result = Interpret("a");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("a")));
+ EXPECT_EQ(result.cost, 1);
+}
+
+TEST(InterpretStringCapabilityTest, EmitText) {
+ InterpretStringCapabilityResult result = Interpret("hi");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("hi")));
+ EXPECT_EQ(result.cost, 2);
+}
+
+TEST(InterpretStringCapabilityTest, EmitNonprinting) {
+ InterpretStringCapabilityResult result = Interpret("\r\n");
+ EXPECT_THAT(result.terms, ElementsAre(PointsToEmitBytes("\r\n")));
+ EXPECT_EQ(result.cost, 2);
+}
+
+TEST(InterpretStringCapabilityTest, PushParameter) {
+ EXPECT_THAT(Interpret("%p2%d", {42, 96}).terms,
+ ElementsAre(PointsToEmitBytes("96")));
+}
+
+TEST(InterpretStringCapabilityTest, PopNumber) {
+ EXPECT_THAT(Interpret("%p1%:-10.5o", {42}).terms,
+ ElementsAre(PointsToEmitBytes("00052 ")));
+}
+
+TEST(InterpretStringCapabilityTest, PopString) {
+ EXPECT_THAT(Interpret("%p1%:-10s", {"foo"}).terms,
+ ElementsAre(PointsToEmitBytes("foo ")));
+}
+
+TEST(InterpretStringCapabilityTest, PopChar) {
+ EXPECT_THAT(Interpret("%p1%:-10c", {'x'}).terms,
+ ElementsAre(PointsToEmitBytes("x ")));
+}
+
+TEST(InterpretStringCapabilityTest, StoreRecall) {
+ EXPECT_THAT(Interpret("%p1%Pa%p2%Pb%gb%s,%ga%d", {1, "foo"}).terms,
+ ElementsAre(PointsToEmitBytes("foo,1")));
+}
+
+TEST(InterpretStringCapabilityTest, PushChar) {
+ EXPECT_THAT(Interpret("%'x'%'y'%c%c").terms,
+ ElementsAre(PointsToEmitBytes("yx")));
+}
+
+TEST(InterpretStringCapabilityTest, PushNumber) {
+ EXPECT_THAT(Interpret("%{123}%d").terms,
+ ElementsAre(PointsToEmitBytes("123")));
+}
+
+TEST(InterpretStringCapabilityTest, Strlen) {
+ EXPECT_THAT(Interpret("%p1%l%d", {"foobar"}).terms,
+ ElementsAre(PointsToEmitBytes("6")));
+}
+
+TEST(InterpretStringCapabilityTest, Add) {
+ EXPECT_THAT(Interpret("%{5}%{2}%+%d").terms,
+ ElementsAre(PointsToEmitBytes("7")));
+}
+
+TEST(InterpretStringCapabilityTest, Subtract) {
+ EXPECT_THAT(Interpret("%{5}%{2}%-%d").terms,
+ ElementsAre(PointsToEmitBytes("3")));
+}
+
+TEST(InterpretStringCapabilityTest, Multiply) {
+ EXPECT_THAT(Interpret("%{5}%{2}%*%d").terms,
+ ElementsAre(PointsToEmitBytes("10")));
+}
+
+TEST(InterpretStringCapabilityTest, Divide) {
+ EXPECT_THAT(Interpret("%{5}%{2}%/%d").terms,
+ ElementsAre(PointsToEmitBytes("2")));
+}
+
+TEST(InterpretStringCapabilityTest, Modulo) {
+ EXPECT_THAT(Interpret("%{5}%{2}%m%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseAnd) {
+ EXPECT_THAT(Interpret("%{3}%{15}%&%d").terms,
+ ElementsAre(PointsToEmitBytes("3")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseOr) {
+ EXPECT_THAT(Interpret("%{2}%{1}%|%d").terms,
+ ElementsAre(PointsToEmitBytes("3")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseXor) {
+ EXPECT_THAT(Interpret("%{15}%{10}%^%d").terms,
+ ElementsAre(PointsToEmitBytes("5")));
+}
+
+TEST(InterpretStringCapabilityTest, Equals) {
+ EXPECT_THAT(Interpret("%{15}%{15}%=%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{15}%{14}%=%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, LessThan) {
+ EXPECT_THAT(Interpret("%{2}%{3}%<%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{2}%{2}%<%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, GreaterThan) {
+ EXPECT_THAT(Interpret("%{3}%{2}%>%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{2}%{2}%>%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, LogicalAnd) {
+ EXPECT_THAT(Interpret("%{0}%{0}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{0}%{1}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{1}%{0}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{1}%{1}%A%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+}
+
+TEST(InterpretStringCapabilityTest, LogicalOr) {
+ EXPECT_THAT(Interpret("%{0}%{0}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("0")));
+ EXPECT_THAT(Interpret("%{0}%{1}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{1}%{0}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{1}%{1}%O%d").terms,
+ ElementsAre(PointsToEmitBytes("1")));
+}
+
+TEST(InterpretStringCapabilityTest, LogicalNot) {
+ EXPECT_THAT(Interpret("%{0}%!%d").terms, ElementsAre(PointsToEmitBytes("1")));
+ EXPECT_THAT(Interpret("%{5}%!%d").terms, ElementsAre(PointsToEmitBytes("0")));
+}
+
+TEST(InterpretStringCapabilityTest, BitwiseNot) {
+ EXPECT_THAT(Interpret("%{8}%~%d").terms,
+ ElementsAre(PointsToEmitBytes("-9")));
+}
+
+TEST(InterpretStringCapabilityTest, IncrementParameters) {
+ EXPECT_THAT(Interpret("%i%p1%d,%p2%d", {6, 8}).terms,
+ ElementsAre(PointsToEmitBytes("7,9")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThen) {
+ EXPECT_THAT(Interpret("%?%{0}%ttrue%;").terms, IsEmpty());
+ EXPECT_THAT(Interpret("%?%{1}%ttrue%;").terms,
+ ElementsAre(PointsToEmitBytes("true")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElse) {
+ EXPECT_THAT(Interpret("%?%{0}%ttrue%efalse%;").terms,
+ ElementsAre(PointsToEmitBytes("false")));
+ EXPECT_THAT(Interpret("%?%{1}%ttrue%efalse%;").terms,
+ ElementsAre(PointsToEmitBytes("true")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElseThen) {
+ EXPECT_THAT(Interpret("%?%{0}%t!%e%{1}%tgood%;").terms,
+ ElementsAre(PointsToEmitBytes("good")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElseThenElse) {
+ EXPECT_THAT(Interpret("%?%{0}%t!%e%{0}%t!%egood%;").terms,
+ ElementsAre(PointsToEmitBytes("good")));
+}
+
+TEST(InterpretStringCapabilityTest, IfThenElseThenElseShortCircuit) {
+ EXPECT_THAT(Interpret("%?%{1}%tgood%e%{0}%t!%e!%;").terms,
+ ElementsAre(PointsToEmitBytes("good")));
+}
+
+} // namespace
+} // namespace goldfishterm_internal
diff --git a/goldfishterm/simple.cc b/goldfishterm/simple.cc
new file mode 100644
index 0000000..9c4c9a9
--- /dev/null
+++ b/goldfishterm/simple.cc
@@ -0,0 +1,74 @@
+// 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 "goldfishterm/simple.h"
+
+#include <stdlib.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <stdexcept>
+#include <vector>
+
+#include "goldfishterm/internal/emit.h"
+#include "goldfishterm/terminfo.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+
+namespace goldfishterm {
+
+namespace {
+
+using ::goldfishterm_internal::InterpretStringCapability;
+using ::goldfishterm_internal::InterpretStringCapabilityInput;
+
+termios StdoutTermios() {
+ termios tty;
+ if (tcgetattr(STDOUT_FILENO, &tty) < 0) {
+ throw std::system_error(errno, std::system_category(), "tcgetattr");
+ }
+ return tty;
+}
+
+absl::string_view TerminalNameFromEnvironment() {
+ const char* name = getenv("TERM");
+ if (name == nullptr) {
+ throw std::runtime_error("TERM unset");
+ }
+ return name;
+}
+
+} // namespace
+
+SimpleTerminalOutput::SimpleTerminalOutput()
+ : SimpleTerminalOutput(StdoutTermios()) {}
+
+SimpleTerminalOutput::SimpleTerminalOutput(const termios& tty)
+ : terminfo_(
+ TerminfoEntry::FromSystemDatabase(TerminalNameFromEnvironment())) {
+ baud_ = cfgetospeed(&tty);
+}
+
+void SimpleTerminalOutput::Emit(
+ StringCapability cap,
+ std::vector<goldfishterm_internal::StringCapabilityParameter> parameters) {
+ goldfishterm_internal::Emit(
+ InterpretStringCapability(
+ InterpretStringCapabilityInput(terminfo_, terminfo_.get(cap).value(),
+ std::move(parameters), /*baud=*/baud_,
+ /*lines_affected=*/1))
+ .terms,
+ std::cout);
+}
+
+} // namespace goldfishterm
diff --git a/goldfishterm/simple.h b/goldfishterm/simple.h
new file mode 100644
index 0000000..3f5cc9f
--- /dev/null
+++ b/goldfishterm/simple.h
@@ -0,0 +1,71 @@
+// 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_GOLDFISHTERM_SIMPLE_H_
+#define EC_GOLDFISHTERM_SIMPLE_H_
+
+#include <termios.h>
+
+#include <iostream>
+#include <vector>
+
+#include "goldfishterm/internal/string_capability.h"
+#include "goldfishterm/terminfo.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+
+namespace goldfishterm {
+
+// Looks up escape sequences for the terminal described in the TERM environment
+// variable, and allows sending those escape sequences to standard output.
+//
+// This class is thread-safe, provided you don't mutate the TERM environment
+// variable while its constructor is running.
+class SimpleTerminalOutput final {
+ public:
+ explicit SimpleTerminalOutput();
+ explicit SimpleTerminalOutput(const termios&);
+
+ SimpleTerminalOutput(const SimpleTerminalOutput&) noexcept = default;
+ SimpleTerminalOutput& operator=(const SimpleTerminalOutput&) noexcept =
+ default;
+ SimpleTerminalOutput(SimpleTerminalOutput&&) noexcept = default;
+ SimpleTerminalOutput& operator=(SimpleTerminalOutput&&) noexcept = default;
+
+ void Write(absl::string_view s) { std::cout.write(s.data(), s.size()); }
+
+ void Flush() { std::cout.flush(); }
+
+ // Rings the bell. On some terminals, this may flash the screen instead.
+ void Beep() { Emit(StringCapability::kBell); }
+
+ // Moves the cursor to the beginning of the current line.
+ void BeginningOfLine() { Emit(StringCapability::kCarriageReturn); }
+
+ // Clears from the cursor to the end of the current line.
+ void ClearToEndOfLine() { Emit(StringCapability::kClrEol); }
+
+ // Moves the cursor down one line.
+ void CursorDown() { Emit(StringCapability::kCursorDown); }
+
+ private:
+ void Emit(StringCapability,
+ std::vector<goldfishterm_internal::StringCapabilityParameter> = {});
+
+ TerminfoEntry terminfo_;
+ int baud_;
+};
+
+} // namespace goldfishterm
+
+#endif // EC_GOLDFISHTERM_SIMPLE_H_
diff --git a/goldfishterm/terminfo.cc b/goldfishterm/terminfo.cc
new file mode 100644
index 0000000..b58fb5b
--- /dev/null
+++ b/goldfishterm/terminfo.cc
@@ -0,0 +1,325 @@
+// 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 "goldfishterm/terminfo.h"
+
+#include <byteswap.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <fstream>
+#include <istream>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include "third_party/abseil/absl/base/config.h"
+#include "third_party/abseil/absl/meta/type_traits.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/strings/str_format.h"
+#include "third_party/abseil/absl/strings/str_split.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace goldfishterm {
+
+namespace {
+
+constexpr int16_t kTerminfoMagic = 0432;
+constexpr int16_t kTerminfoExtendedNumberFormatMagic = 01036;
+constexpr char kSystemTerminfoDirectory[] = "/etc/terminfo";
+
+template <typename T>
+constexpr absl::underlying_type_t<T> FromEnum(T x) {
+ return static_cast<absl::underlying_type_t<T>>(x);
+}
+
+std::string GetEnv(const char* var) noexcept {
+ const char* val = getenv(var);
+ if (val == nullptr) {
+ return "";
+ }
+ return val;
+}
+
+// Reads a potentially unaligned little-endian int16_t.
+int16_t ReadShort(std::istream& in) {
+ // TODO(bbarenblat@gmail.com): Do we need to support anything else here (maybe
+ // PDP-11-endian)?
+#if !defined(ABSL_IS_LITTLE_ENDIAN) && !defined(ABSL_IS_BIG_ENDIAN)
+#error Neither ABSL_IS_LITTLE_ENDIAN nor ABSL_IS_BIG_ENDIAN is defined
+#endif
+
+ int16_t n;
+ in.read(reinterpret_cast<char*>(&n), sizeof(int16_t));
+#ifdef ABSL_IS_BIG_ENDIAN
+ n = bswap_16(n);
+#endif
+ return n;
+}
+
+// Reads a potentially unaligned little-endian int32_t.
+int32_t ReadInt(std::istream& in) {
+ // TODO(bbarenblat@gmail.com): Do we need to support anything else here (maybe
+ // PDP-11-endian)?
+#if !defined(ABSL_IS_LITTLE_ENDIAN) && !defined(ABSL_IS_BIG_ENDIAN)
+#error Neither ABSL_IS_LITTLE_ENDIAN nor ABSL_IS_BIG_ENDIAN is defined
+#endif
+
+ int32_t n;
+ in.read(reinterpret_cast<char*>(&n), sizeof(int32_t));
+#ifdef ABSL_IS_BIG_ENDIAN
+ n = bswap_32(n);
+#endif
+ return n;
+}
+
+int16_t ReadNonnegativeShort(const char* error_message, std::istream& in) {
+ int16_t n = ReadShort(in);
+ if (n < 0) {
+ throw std::runtime_error(error_message);
+ }
+ return n;
+}
+
+void RequireValidNumber(const char* error_message, int n) {
+ // -1 and -2 are reserved to mean "not present" and "canceled", respectively.
+ if (n < -2) {
+ throw std::runtime_error(absl::StrCat(error_message, " ", n));
+ }
+}
+
+struct Header final {
+ bool extended_number_format;
+ int names_bytes;
+ int booleans_bytes;
+ int numbers_count;
+ int strings_offsets_count;
+ int strings_bytes;
+
+ Header(std::istream& description) {
+ switch (ReadShort(description)) {
+ case kTerminfoMagic:
+ extended_number_format = false;
+ break;
+ case kTerminfoExtendedNumberFormatMagic:
+ extended_number_format = true;
+ break;
+ default:
+ throw std::runtime_error("bad magic");
+ }
+ names_bytes = ReadNonnegativeShort(
+ "terminal names section cannot have negative length", description);
+ booleans_bytes = ReadNonnegativeShort(
+ "boolean flags section cannot have negative length", description);
+ numbers_count = ReadNonnegativeShort(
+ "numbers section cannot have negative length", description);
+ strings_offsets_count = ReadNonnegativeShort(
+ "strings section cannot have negative numbers of entries", description);
+ strings_bytes = ReadNonnegativeShort(
+ "strings table cannot have negative length", description);
+ }
+};
+
+std::vector<std::string> ReadTerminalNames(int bytes, std::istream& in) {
+ assert(bytes > 0);
+ std::string names(bytes - 1, '\0');
+ in.read(names.data(), names.size());
+ // Discard the null at the end of the names list.
+ if (in.get() != '\0') {
+ throw std::runtime_error("terminal names were not null-terminated");
+ }
+ return absl::StrSplit(names, '|');
+}
+
+std::vector<int8_t> ReadBooleanFlags(int bytes, std::istream& in) {
+ std::vector<int8_t> booleans(bytes);
+ in.read(reinterpret_cast<char*>(booleans.data()), booleans.size());
+ for (int8_t b : booleans) {
+ RequireValidNumber("invalid boolean capability", b);
+ }
+ return booleans;
+}
+
+std::vector<int32_t> ReadNumbers(bool extended_number_format, int count,
+ std::istream& in) {
+ std::vector<int32_t> numbers;
+ numbers.reserve(count);
+ for (int i = 0; i < count; ++i) {
+ int32_t n;
+ if (extended_number_format) {
+ n = ReadInt(in);
+ } else {
+ n = ReadShort(in);
+ }
+ RequireValidNumber("invalid numeric capability", n);
+ numbers.push_back(n);
+ }
+ return numbers;
+}
+
+std::vector<absl::optional<std::string>> ReadStrings(int offsets_count,
+ int bytes,
+ std::istream& in) {
+ std::vector<int16_t> offsets;
+ offsets.reserve(offsets_count);
+ for (int i = 0; i < offsets_count; ++i) {
+ int16_t n = ReadShort(in);
+ RequireValidNumber("invalid string offset", n);
+ offsets.push_back(n);
+ }
+
+ // Read the string table, including the trailing null (so we can easily find
+ // the end of each capability).
+ std::string data(bytes, '\0');
+ in.read(data.data(), data.size());
+
+ std::vector<absl::optional<std::string>> result;
+ for (int16_t offset : offsets) {
+ if (offset < 0) {
+ result.push_back(absl::nullopt);
+ } else {
+ result.push_back(std::string(data.data() + offset));
+ }
+ }
+ return result;
+}
+
+std::vector<std::string> DatabasePaths() noexcept {
+ // If TERMINFO is set, only look there.
+ if (std::string terminfo = GetEnv("TERMINFO"); !terminfo.empty()) {
+ return {terminfo};
+ }
+
+ std::vector<std::string> paths;
+
+ // Start by looking in HOME.
+ if (std::string home = GetEnv("HOME"); !home.empty()) {
+ paths.push_back(absl::StrCat(home, "/.terminfo"));
+ }
+
+ // Next, look in TERMINFO_DIRS.
+ if (std::string terminfo_dirs = GetEnv("TERMINFO_DIRS");
+ !terminfo_dirs.empty()) {
+ for (absl::string_view dir : absl::StrSplit(terminfo_dirs, ':')) {
+ if (dir.empty()) {
+ paths.push_back(kSystemTerminfoDirectory);
+ } else {
+ paths.push_back(std::string(dir));
+ }
+ }
+ }
+
+ // Finally, look in system locations.
+ paths.push_back(kSystemTerminfoDirectory);
+ paths.push_back("/lib/terminfo");
+ paths.push_back("/usr/share/terminfo");
+
+ return paths;
+}
+
+std::ifstream OpenDescriptionForTerminal(absl::string_view db_path,
+ absl::string_view name) {
+ // The path to the terminal entry inside the database takes the general form
+ // `c/name`, where c is the first character of the name (on file systems that
+ // are case-sensitive) and the hex encoding of the name (on file systems that
+ // are not).
+
+ if (name.empty()) {
+ throw std::invalid_argument("empty terminal name");
+ }
+
+ std::ifstream entry(absl::StrFormat("%s/%02x/%s", db_path, name[0], name));
+ if (!entry.fail()) {
+ return entry;
+ }
+
+ entry = std::ifstream(absl::StrFormat("%s/%c/%s", db_path, name[0], name));
+ if (!entry.fail()) {
+ return entry;
+ }
+
+ throw std::runtime_error("terminfo entry not found");
+}
+
+} // namespace
+
+TerminfoEntry TerminfoEntry::FromSystemDatabase(
+ absl::string_view terminal_name) {
+ for (const auto& path : DatabasePaths()) {
+ try {
+ std::ifstream binary = OpenDescriptionForTerminal(path, terminal_name);
+ return TerminfoEntry(binary);
+ } catch (const std::runtime_error&) {
+ continue;
+ }
+ }
+ throw std::runtime_error("no valid system entry by that name");
+}
+
+TerminfoEntry::TerminfoEntry(std::istream& description) {
+ description.exceptions(std::istream::badbit | std::istream::failbit);
+
+ Header header(description);
+ if (header.names_bytes > 0) {
+ names_ = ReadTerminalNames(header.names_bytes, description);
+ }
+ booleans_ = ReadBooleanFlags(header.booleans_bytes, description);
+
+ // Discard the padding byte between the boolean flags section and the number
+ // section, if it exists.
+ if ((header.names_bytes + header.booleans_bytes) % 2 == 1 &&
+ (header.numbers_count != 0 || header.strings_offsets_count != 0 ||
+ header.strings_bytes != 0) &&
+ description.get() != '\0') {
+ throw std::runtime_error("padding byte was nonnull");
+ }
+
+ numbers_ = ReadNumbers(header.extended_number_format, header.numbers_count,
+ description);
+ strings_ = ReadStrings(header.strings_offsets_count, header.strings_bytes,
+ description);
+}
+
+bool TerminfoEntry::get(BooleanCapability cap) const noexcept {
+ try {
+ return booleans_.at(FromEnum(cap)) > 0;
+ } catch (const std::out_of_range&) {
+ return false;
+ }
+}
+
+absl::optional<int> TerminfoEntry::get(NumericCapability cap) const noexcept {
+ try {
+ int r = numbers_.at(FromEnum(cap));
+ if (r >= 0) {
+ return r;
+ } else {
+ return absl::nullopt;
+ }
+ } catch (const std::out_of_range&) {
+ return absl::nullopt;
+ }
+}
+
+const absl::optional<std::string>& TerminfoEntry::get(
+ StringCapability cap) const noexcept {
+ try {
+ return strings_.at(FromEnum(cap));
+ } catch (const std::out_of_range&) {
+ static const absl::optional<std::string> kNone = absl::nullopt;
+ return kNone;
+ }
+}
+
+} // namespace goldfishterm
diff --git a/goldfishterm/terminfo.h b/goldfishterm/terminfo.h
new file mode 100644
index 0000000..9452468
--- /dev/null
+++ b/goldfishterm/terminfo.h
@@ -0,0 +1,544 @@
+// 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.
+
+// Terminfo entries and the terminfo database.
+
+#ifndef EC_GOLDFISHTERM_TERMINFO_H_
+#define EC_GOLDFISHTERM_TERMINFO_H_
+
+#include <stdint.h>
+
+#include <istream>
+#include <string>
+#include <vector>
+
+#include "third_party/abseil/absl/strings/string_view.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace goldfishterm {
+
+// Boolean terminal capabilities. See terminfo(5) for full documentation.
+enum class BooleanCapability {
+ kAutoLeftMargin = 0,
+ kAutoRightMargin = 1,
+ kNoEscCtlc = 2,
+ kCeolStandoutGlitch = 3,
+ kEatNewlineGlitch = 4,
+ kEraseOverstrike = 5,
+ kGenericType = 6,
+ kHardCopy = 7,
+ kHasMetaKey = 8,
+ kHasStatusLine = 9,
+ kInsertNullGlitch = 10,
+ kMemoryAbove = 11,
+ kMemoryBelow = 12,
+ kMoveInsertMode = 13,
+ kMoveStandoutMode = 14,
+ kOverStrike = 15,
+ kStatusLineEscOk = 16,
+ kDestTabsMagicSmso = 17,
+ kTildeGlitch = 18,
+ kTransparentUnderline = 19,
+ kXonXoff = 20,
+ kNeedsXonXoff = 21,
+ kPrtrSilent = 22,
+ kHardCursor = 23,
+ kNonRevRmcup = 24,
+ kNoPadChar = 25,
+ kNonDestScrollRegion = 26,
+ kCanChange = 27,
+ kBackColorErase = 28,
+ kHueLightnessSaturation = 29,
+ kColAddrGlitch = 30,
+ kCrCancelsMicroMode = 31,
+ kHasPrintWheel = 32,
+ kRowAddrGlitch = 33,
+ kSemiAutoRightMargin = 34,
+ kCpiChangesRes = 35,
+ kLpiChangesRes = 36,
+};
+
+// Numeric terminal capabilities. See terminfo(5) for full documentation.
+enum class NumericCapability {
+ kColumns = 0,
+ kInitTabs = 1,
+ kLines = 2,
+ kLinesOfMemory = 3,
+ kMagicCookieGlitch = 4,
+ kPaddingBaudRate = 5,
+ kVirtualTerminal = 6,
+ kWidthStatusLine = 7,
+ kNumLabels = 8,
+ kLabelHeight = 9,
+ kLabelWidth = 10,
+ kMaxAttributes = 11,
+ kMaximumWindows = 12,
+ kMaxColors = 13,
+ kMaxPairs = 14,
+ kNoColorVideo = 15,
+ kBufferCapacity = 16,
+ kDotVertSpacing = 17,
+ kDotHorzSpacing = 18,
+ kMaxMicroAddress = 19,
+ kMaxMicroJump = 20,
+ kMicroColSize = 21,
+ kMicroLineSize = 22,
+ kNumberOfPins = 23,
+ kOutputResChar = 24,
+ kOutputResLine = 25,
+ kOutputResHorzInch = 26,
+ kOutputResVertInch = 27,
+ kPrintRate = 28,
+ kWideCharSize = 29,
+ kButtons = 30,
+ kBitImageEntwining = 31,
+ kBitImageType = 32,
+};
+
+// String terminal capabilities. See terminfo(5) for full documentation.
+enum class StringCapability {
+ kBackTab = 0,
+ kBell = 1,
+ kCarriageReturn = 2,
+ kChangeScrollRegion = 3,
+ kClearAllTabs = 4,
+ kClearScreen = 5,
+ kClrEol = 6,
+ kClrEos = 7,
+ kColumnAddress = 8,
+ kCommandCharacter = 9,
+ kCursorAddress = 10,
+ kCursorDown = 11,
+ kCursorHome = 12,
+ kCursorInvisible = 13,
+ kCursorLeft = 14,
+ kCursorMemAddress = 15,
+ kCursorNormal = 16,
+ kCursorRight = 17,
+ kCursorToLl = 18,
+ kCursorUp = 19,
+ kCursorVisible = 20,
+ kDeleteCharacter = 21,
+ kDeleteLine = 22,
+ kDisStatusLine = 23,
+ kDownHalfLine = 24,
+ kEnterAltCharsetMode = 25,
+ kEnterBlinkMode = 26,
+ kEnterBoldMode = 27,
+ kEnterCaMode = 28,
+ kEnterDeleteMode = 29,
+ kEnterDimMode = 30,
+ kEnterInsertMode = 31,
+ kEnterSecureMode = 32,
+ kEnterProtectedMode = 33,
+ kEnterReverseMode = 34,
+ kEnterStandoutMode = 35,
+ kEnterUnderlineMode = 36,
+ kEraseChars = 37,
+ kExitAltCharsetMode = 38,
+ kExitAttributeMode = 39,
+ kExitCaMode = 40,
+ kExitDeleteMode = 41,
+ kExitInsertMode = 42,
+ kExitStandoutMode = 43,
+ kExitUnderlineMode = 44,
+ kFlashScreen = 45,
+ kFormFeed = 46,
+ kFromStatusLine = 47,
+ kInit1string = 48,
+ kInit2string = 49,
+ kInit3string = 50,
+ kInitFile = 51,
+ kInsertCharacter = 52,
+ kInsertLine = 53,
+ kInsertPadding = 54,
+ kKeyBackspace = 55,
+ kKeyCatab = 56,
+ kKeyClear = 57,
+ kKeyCtab = 58,
+ kKeyDc = 59,
+ kKeyDl = 60,
+ kKeyDown = 61,
+ kKeyEic = 62,
+ kKeyEol = 63,
+ kKeyEos = 64,
+ kKeyF0 = 65,
+ kKeyF1 = 66,
+ kKeyF10 = 67,
+ kKeyF2 = 68,
+ kKeyF3 = 69,
+ kKeyF4 = 70,
+ kKeyF5 = 71,
+ kKeyF6 = 72,
+ kKeyF7 = 73,
+ kKeyF8 = 74,
+ kKeyF9 = 75,
+ kKeyHome = 76,
+ kKeyIc = 77,
+ kKeyIl = 78,
+ kKeyLeft = 79,
+ kKeyLl = 80,
+ kKeyNpage = 81,
+ kKeyPpage = 82,
+ kKeyRight = 83,
+ kKeySf = 84,
+ kKeySr = 85,
+ kKeyStab = 86,
+ kKeyUp = 87,
+ kKeypadLocal = 88,
+ kKeypadXmit = 89,
+ kLabF0 = 90,
+ kLabF1 = 91,
+ kLabF10 = 92,
+ kLabF2 = 93,
+ kLabF3 = 94,
+ kLabF4 = 95,
+ kLabF5 = 96,
+ kLabF6 = 97,
+ kLabF7 = 98,
+ kLabF8 = 99,
+ kLabF9 = 100,
+ kMetaOff = 101,
+ kMetaOn = 102,
+ kNewline = 103,
+ kPadChar = 104,
+ kParmDch = 105,
+ kParmDeleteLine = 106,
+ kParmDownCursor = 107,
+ kParmIch = 108,
+ kParmIndex = 109,
+ kParmInsertLine = 110,
+ kParmLeftCursor = 111,
+ kParmRightCursor = 112,
+ kParmRindex = 113,
+ kParmUpCursor = 114,
+ kPkeyKey = 115,
+ kPkeyLocal = 116,
+ kPkeyXmit = 117,
+ kPrintScreen = 118,
+ kPrtrOff = 119,
+ kPrtrOn = 120,
+ kRepeatChar = 121,
+ kReset1string = 122,
+ kReset2string = 123,
+ kReset3string = 124,
+ kResetFile = 125,
+ kRestoreCursor = 126,
+ kRowAddress = 127,
+ kSaveCursor = 128,
+ kScrollForward = 129,
+ kScrollReverse = 130,
+ kSetAttributes = 131,
+ kSetTab = 132,
+ kSetWindow = 133,
+ kTab = 134,
+ kToStatusLine = 135,
+ kUnderlineChar = 136,
+ kUpHalfLine = 137,
+ kInitProg = 138,
+ kKeyA1 = 139,
+ kKeyA3 = 140,
+ kKeyB2 = 141,
+ kKeyC1 = 142,
+ kKeyC3 = 143,
+ kPrtrNon = 144,
+ kCharPadding = 145,
+ kAcsChars = 146,
+ kPlabNorm = 147,
+ kKeyBtab = 148,
+ kEnterXonMode = 149,
+ kExitXonMode = 150,
+ kEnterAmMode = 151,
+ kExitAmMode = 152,
+ kXonCharacter = 153,
+ kXoffCharacter = 154,
+ kEnaAcs = 155,
+ kLabelOn = 156,
+ kLabelOff = 157,
+ kKeyBeg = 158,
+ kKeyCancel = 159,
+ kKeyClose = 160,
+ kKeyCommand = 161,
+ kKeyCopy = 162,
+ kKeyCreate = 163,
+ kKeyEnd = 164,
+ kKeyEnter = 165,
+ kKeyExit = 166,
+ kKeyFind = 167,
+ kKeyHelp = 168,
+ kKeyMark = 169,
+ kKeyMessage = 170,
+ kKeyMove = 171,
+ kKeyNext = 172,
+ kKeyOpen = 173,
+ kKeyOptions = 174,
+ kKeyPrevious = 175,
+ kKeyPrint = 176,
+ kKeyRedo = 177,
+ kKeyReference = 178,
+ kKeyRefresh = 179,
+ kKeyReplace = 180,
+ kKeyRestart = 181,
+ kKeyResume = 182,
+ kKeySave = 183,
+ kKeySuspend = 184,
+ kKeyUndo = 185,
+ kKeySbeg = 186,
+ kKeyScancel = 187,
+ kKeyScommand = 188,
+ kKeyScopy = 189,
+ kKeyScreate = 190,
+ kKeySdc = 191,
+ kKeySdl = 192,
+ kKeySelect = 193,
+ kKeySend = 194,
+ kKeySeol = 195,
+ kKeySexit = 196,
+ kKeySfind = 197,
+ kKeyShelp = 198,
+ kKeyShome = 199,
+ kKeySic = 200,
+ kKeySleft = 201,
+ kKeySmessage = 202,
+ kKeySmove = 203,
+ kKeySnext = 204,
+ kKeySoptions = 205,
+ kKeySprevious = 206,
+ kKeySprint = 207,
+ kKeySredo = 208,
+ kKeySreplace = 209,
+ kKeySright = 210,
+ kKeySrsume = 211,
+ kKeySsave = 212,
+ kKeySsuspend = 213,
+ kKeySundo = 214,
+ kReqForInput = 215,
+ kKeyF11 = 216,
+ kKeyF12 = 217,
+ kKeyF13 = 218,
+ kKeyF14 = 219,
+ kKeyF15 = 220,
+ kKeyF16 = 221,
+ kKeyF17 = 222,
+ kKeyF18 = 223,
+ kKeyF19 = 224,
+ kKeyF20 = 225,
+ kKeyF21 = 226,
+ kKeyF22 = 227,
+ kKeyF23 = 228,
+ kKeyF24 = 229,
+ kKeyF25 = 230,
+ kKeyF26 = 231,
+ kKeyF27 = 232,
+ kKeyF28 = 233,
+ kKeyF29 = 234,
+ kKeyF30 = 235,
+ kKeyF31 = 236,
+ kKeyF32 = 237,
+ kKeyF33 = 238,
+ kKeyF34 = 239,
+ kKeyF35 = 240,
+ kKeyF36 = 241,
+ kKeyF37 = 242,
+ kKeyF38 = 243,
+ kKeyF39 = 244,
+ kKeyF40 = 245,
+ kKeyF41 = 246,
+ kKeyF42 = 247,
+ kKeyF43 = 248,
+ kKeyF44 = 249,
+ kKeyF45 = 250,
+ kKeyF46 = 251,
+ kKeyF47 = 252,
+ kKeyF48 = 253,
+ kKeyF49 = 254,
+ kKeyF50 = 255,
+ kKeyF51 = 256,
+ kKeyF52 = 257,
+ kKeyF53 = 258,
+ kKeyF54 = 259,
+ kKeyF55 = 260,
+ kKeyF56 = 261,
+ kKeyF57 = 262,
+ kKeyF58 = 263,
+ kKeyF59 = 264,
+ kKeyF60 = 265,
+ kKeyF61 = 266,
+ kKeyF62 = 267,
+ kKeyF63 = 268,
+ kClrBol = 269,
+ kClearMargins = 270,
+ kSetLeftMargin = 271,
+ kSetRightMargin = 272,
+ kLabelFormat = 273,
+ kSetClock = 274,
+ kDisplayClock = 275,
+ kRemoveClock = 276,
+ kCreateWindow = 277,
+ kGotoWindow = 278,
+ kHangup = 279,
+ kDialPhone = 280,
+ kQuickDial = 281,
+ kTone = 282,
+ kPulse = 283,
+ kFlashHook = 284,
+ kFixedPause = 285,
+ kWaitTone = 286,
+ kUser0 = 287,
+ kUser1 = 288,
+ kUser2 = 289,
+ kUser3 = 290,
+ kUser4 = 291,
+ kUser5 = 292,
+ kUser6 = 293,
+ kUser7 = 294,
+ kUser8 = 295,
+ kUser9 = 296,
+ kOrigPair = 297,
+ kOrigColors = 298,
+ kInitializeColor = 299,
+ kInitializePair = 300,
+ kSetColorPair = 301,
+ kSetForeground = 302,
+ kSetBackground = 303,
+ kChangeCharPitch = 304,
+ kChangeLinePitch = 305,
+ kChangeResHorz = 306,
+ kChangeResVert = 307,
+ kDefineChar = 308,
+ kEnterDoublewideMode = 309,
+ kEnterDraftQuality = 310,
+ kEnterItalicsMode = 311,
+ kEnterLeftwardMode = 312,
+ kEnterMicroMode = 313,
+ kEnterNearLetterQuality = 314,
+ kEnterNormalQuality = 315,
+ kEnterShadowMode = 316,
+ kEnterSubscriptMode = 317,
+ kEnterSuperscriptMode = 318,
+ kEnterUpwardMode = 319,
+ kExitDoublewideMode = 320,
+ kExitItalicsMode = 321,
+ kExitLeftwardMode = 322,
+ kExitMicroMode = 323,
+ kExitShadowMode = 324,
+ kExitSubscriptMode = 325,
+ kExitSuperscriptMode = 326,
+ kExitUpwardMode = 327,
+ kMicroColumnAddress = 328,
+ kMicroDown = 329,
+ kMicroLeft = 330,
+ kMicroRight = 331,
+ kMicroRowAddress = 332,
+ kMicroUp = 333,
+ kOrderOfPins = 334,
+ kParmDownMicro = 335,
+ kParmLeftMicro = 336,
+ kParmRightMicro = 337,
+ kParmUpMicro = 338,
+ kSelectCharSet = 339,
+ kSetBottomMargin = 340,
+ kSetBottomMarginParm = 341,
+ kSetLeftMarginParm = 342,
+ kSetRightMarginParm = 343,
+ kSetTopMargin = 344,
+ kSetTopMarginParm = 345,
+ kStartBitImage = 346,
+ kStartCharSetDef = 347,
+ kStopBitImage = 348,
+ kStopCharSetDef = 349,
+ kSubscriptCharacters = 350,
+ kSuperscriptCharacters = 351,
+ kTheseCauseCr = 352,
+ kZeroMotion = 353,
+ kCharSetNames = 354,
+ kKeyMouse = 355,
+ kMouseInfo = 356,
+ kReqMousePos = 357,
+ kGetMouse = 358,
+ kSetAForeground = 359,
+ kSetABackground = 360,
+ kPkeyPlab = 361,
+ kDeviceType = 362,
+ kCodeSetInit = 363,
+ kSet0DesSeq = 364,
+ kSet1DesSeq = 365,
+ kSet2DesSeq = 366,
+ kSet3DesSeq = 367,
+ kSetLrMargin = 368,
+ kSetTbMargin = 369,
+ kBitImageRepeat = 370,
+ kBitImageNewline = 371,
+ kBitImageCarriageReturn = 372,
+ kColorNames = 373,
+ kDefineBitImageRegion = 374,
+ kEndBitImageRegion = 375,
+ kSetColorBand = 376,
+ kSetPageLength = 377,
+ kDisplayPcChar = 378,
+ kEnterPcCharsetMode = 379,
+ kExitPcCharsetMode = 380,
+ kEnterScancodeMode = 381,
+ kExitScancodeMode = 382,
+ kPcTermOptions = 383,
+ kScancodeEscape = 384,
+ kAltScancodeEsc = 385,
+ kEnterHorizontalHlMode = 386,
+ kEnterLeftHlMode = 387,
+ kEnterLowHlMode = 388,
+ kEnterRightHlMode = 389,
+ kEnterTopHlMode = 390,
+ kEnterVerticalHlMode = 391,
+ kSetAAttributes = 392,
+ kSetPglenInch = 393,
+};
+
+// An entry from the terminfo database.
+//
+// This class is thread-safe.
+class TerminfoEntry final {
+ public:
+ // Looks up a terminfo entry in the system terminfo database. Throws
+ // std::runtime_error if no valid system entry exists for the specified
+ // terminal.
+ //
+ // This function is thread-hostile (it inspects the environment).
+ static TerminfoEntry FromSystemDatabase(absl::string_view terminal_name);
+
+ // Parses an entry from binary data. Throws std::runtime_error if the data are
+ // invalid.
+ explicit TerminfoEntry(std::istream&);
+
+ TerminfoEntry(const TerminfoEntry&) noexcept = default;
+ TerminfoEntry& operator=(const TerminfoEntry&) noexcept = default;
+ TerminfoEntry(TerminfoEntry&&) noexcept = default;
+ TerminfoEntry& operator=(TerminfoEntry&&) noexcept = default;
+
+ const std::string& name() const noexcept { return names_[0]; }
+ const std::vector<std::string>& names() const noexcept { return names_; }
+
+ bool get(BooleanCapability) const noexcept;
+ absl::optional<int> get(NumericCapability) const noexcept;
+ const absl::optional<std::string>& get(StringCapability) const noexcept;
+
+ private:
+ std::vector<std::string> names_;
+ std::vector<int8_t> booleans_;
+ std::vector<int32_t> numbers_;
+ std::vector<absl::optional<std::string>> strings_;
+};
+
+} // namespace goldfishterm
+
+#endif // EC_GOLDFISHTERM_TERMINFO_H_
diff --git a/goldfishterm/terminfo_system_test.cc b/goldfishterm/terminfo_system_test.cc
new file mode 100644
index 0000000..fe9ea9a
--- /dev/null
+++ b/goldfishterm/terminfo_system_test.cc
@@ -0,0 +1,93 @@
+// 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 <getopt.h>
+#include <locale.h>
+
+#include <iostream>
+#include <stdexcept>
+#include <string>
+
+#include "goldfishterm/terminfo.h"
+#include "third_party/abseil/absl/strings/string_view.h"
+
+constexpr absl::string_view kShortUsage =
+ "Usage: find /usr/share/terminfo -type f -exec basename '{}' ';' | sort | "
+ "uniq |\n terminfo_system_test\n";
+
+constexpr absl::string_view kHelp = R"(
+Tests goldfishterm's terminfo parser against real-world binary terminfo files.
+Reads terminal names, one per line, from standard input and attempts to look up
+and parse the terminfo file associated with each one. Prints the terminal names
+that failed lookup or parsing.
+
+A Reverse Polish scientific calculator. Invoked directly, ec launches an
+interactive interpreter that displays the stack and accepts input. Invoked on a
+file, ec processes the entire file; if the result stack is nonempty, ec prints
+the stack in space-separated form.
+
+Options:
+ --help display this help and exit
+)";
+
+constexpr absl::string_view kAskForHelp =
+ "Try 'terminfo_system_test --help' for more information.\n";
+
+enum {
+ kHelpLongOption = 128,
+};
+
+int main(int argc, char* argv[]) {
+ setlocale(LC_ALL, "");
+
+ static option long_options[] = {
+ {"help", no_argument, nullptr, kHelpLongOption},
+ {nullptr, 0, nullptr, 0},
+ };
+ while (true) {
+ int c = getopt_long(argc, argv, "", long_options, /*longindex=*/nullptr);
+ if (c == -1) {
+ break;
+ }
+ switch (c) {
+ case kHelpLongOption:
+ std::cout << kShortUsage << kHelp;
+ return 0;
+ case '?':
+ std::cerr << kAskForHelp;
+ return 1;
+ default:
+ std::cerr << "terminfo_system_test: internal error: unhandled getopt "
+ << "switch\nThis is a bug! Please report it.\n";
+ return 1;
+ }
+ }
+
+ if (optind != argc) {
+ std::cerr << kShortUsage << kAskForHelp;
+ return 1;
+ }
+
+ std::string line;
+ int r = 0;
+ while (std::getline(std::cin, line)) {
+ try {
+ goldfishterm::TerminfoEntry::FromSystemDatabase(line);
+ } catch (const std::runtime_error&) {
+ std::cout << line << '\n';
+ r = 1;
+ }
+ }
+ return r;
+}
diff --git a/goldfishterm/terminfo_test.cc b/goldfishterm/terminfo_test.cc
new file mode 100644
index 0000000..c50cb90
--- /dev/null
+++ b/goldfishterm/terminfo_test.cc
@@ -0,0 +1,228 @@
+// 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 "goldfishterm/terminfo.h"
+
+#include <errno.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <unistd.h>
+
+#include <filesystem>
+#include <fstream>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <system_error>
+
+#include "third_party/abseil/absl/cleanup/cleanup.h"
+#include "third_party/abseil/absl/strings/str_cat.h"
+#include "third_party/abseil/absl/types/optional.h"
+
+namespace goldfishterm {
+namespace {
+
+using namespace std::literals::string_literals;
+
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::IsEmpty;
+using ::testing::Optional;
+
+void SetEnv(const char* var, const char* val) {
+ if (setenv(var, val, /*overwrite=*/1)) {
+ throw std::system_error(errno, std::generic_category(), "setenv");
+ }
+}
+
+// Creates a temporary directory for this test.
+std::string MakeTemporaryDirectory() noexcept {
+ std::string dir =
+ absl::StrCat(testing::TempDir(), "/terminfo_test.", getpid());
+ std::filesystem::create_directories(dir);
+ return dir;
+}
+
+TEST(EntryParseTest, EmptyFails) {
+ std::istringstream s("");
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, BadMagicFails) {
+ std::istringstream s("foobar");
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, TooShortHeaderFails) {
+ std::istringstream s("\x1a\x01");
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, NegativeNamesLengthFails) {
+ std::istringstream s("\x1a\x01\xff\xff");
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, NegativeBooleansLengthFails) {
+ std::istringstream s("\x1a\x01\0\0\xff\xff"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, NegativeNumbersCountFails) {
+ std::istringstream s("\x1a\x01\0\0\0\0\xff\xff"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, NegativeStringsOffsetsCountFails) {
+ std::istringstream s("\x1a\x01\0\0\0\0\0\0\xff\xff"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, NegativeStringsLengthFails) {
+ std::istringstream s("\x1a\x01\0\0\0\0\0\0\0\0\xff\xff"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, MinimalTerminfo) {
+ std::istringstream s("\x1a\x01\0\0\0\0\0\0\0\0\0\0"s);
+ TerminfoEntry t(s);
+}
+
+TEST(EntryParseTest, NamesNotNullTerminated) {
+ std::istringstream s("\x1a\x01\x04\0\0\0\0\0\0\0\0\0term"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, Names) {
+ std::istringstream s("\x1a\x01\x0f\0\0\0\0\0\0\0\0\0name|othername\0"s);
+ EXPECT_THAT(TerminfoEntry(s).names(), ElementsAre("name", "othername"));
+}
+
+TEST(EntryParseTest, FirstName) {
+ std::istringstream s("\x1a\x01\x0f\0\0\0\0\0\0\0\0\0name|othername\0"s);
+ EXPECT_EQ(TerminfoEntry(s).name(), "name");
+}
+
+TEST(EntryParseTest, Booleans) {
+ std::istringstream s("\x1a\x01\x01\0\x04\0\0\0\0\0\0\0\0\x01\x00\x01\x00"s);
+ TerminfoEntry t(s);
+ EXPECT_TRUE(t.get(BooleanCapability::kAutoLeftMargin));
+ EXPECT_FALSE(t.get(BooleanCapability::kAutoRightMargin));
+ EXPECT_TRUE(t.get(BooleanCapability::kNoEscCtlc));
+ EXPECT_FALSE(t.get(BooleanCapability::kCeolStandoutGlitch));
+}
+
+TEST(EntryParseTest, BooleanNegativeOneIsFalse) {
+ std::istringstream s("\x1a\x01\x01\0\x01\0\0\0\0\0\0\0\0\xff"s);
+ EXPECT_FALSE(TerminfoEntry(s).get(BooleanCapability::kAutoLeftMargin));
+}
+
+TEST(EntryParseTest, BooleanNegativeTwoIsFalse) {
+ std::istringstream s("\x1a\x01\x01\0\x01\0\0\0\0\0\0\0\0\xfe"s);
+ EXPECT_FALSE(TerminfoEntry(s).get(BooleanCapability::kAutoLeftMargin));
+}
+
+TEST(EntryParseTest, Numbers) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\x02\0\0\0\0\0\0\0\x01\x02\x03\x04"s);
+ TerminfoEntry t(s);
+ EXPECT_EQ(t.get(NumericCapability::kColumns), 0x0201);
+ EXPECT_EQ(t.get(NumericCapability::kInitTabs), 0x0403);
+}
+
+TEST(EntryParseTest, PaddingBeforeNumbersForAlignment) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\x02\0\0\0\0\0\0\x01\x02\x03\x04"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, NumberNegativeOneOkay) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\x01\0\0\0\0\0\0\0\xff\xff"s);
+ TerminfoEntry t(s);
+}
+
+TEST(EntryParseTest, NumberNegativeTwoOkay) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\x01\0\0\0\0\0\0\0\xfe\xff"s);
+ TerminfoEntry t(s);
+}
+
+TEST(EntryParseTest, NumberNegativeThreeFails) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\x01\0\0\0\0\0\0\0\xfd\xff"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, ExtendedNumberFormat) {
+ std::istringstream s("\x1e\x02\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\x02\x03\x04"s);
+ EXPECT_EQ(TerminfoEntry(s).get(NumericCapability::kColumns), 0x04030201);
+}
+
+TEST(EntryParseTest, StringsWithoutOffsets) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\0\0\0\0\x08\0\0\0foo\0bar\0"s);
+ TerminfoEntry t(s);
+ EXPECT_EQ(t.get(StringCapability::kBackTab), absl::nullopt);
+ EXPECT_EQ(t.get(StringCapability::kBell), absl::nullopt);
+}
+
+TEST(EntryParseTest, ZeroOffsets) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x02\0\0\0\0\0\0\0\0\0"s);
+ TerminfoEntry t(s);
+ EXPECT_THAT(t.get(StringCapability::kBackTab), Optional(IsEmpty()));
+ EXPECT_THAT(t.get(StringCapability::kBell), Optional(IsEmpty()));
+}
+
+TEST(EntryParseTest, NegativeOneOffsetOkay) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x01\0\0\0\0\0\xff\xff"s);
+ EXPECT_EQ(TerminfoEntry(s).get(StringCapability::kBackTab), absl::nullopt);
+}
+
+TEST(EntryParseTest, NegativeTwoOffsetOkay) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x01\0\0\0\0\0\xfe\xff"s);
+ EXPECT_EQ(TerminfoEntry(s).get(StringCapability::kBackTab), absl::nullopt);
+}
+
+TEST(EntryParseTest, NegativeThreeOffsetFails) {
+ std::istringstream s("\x1a\x01\x01\0\0\0\0\0\x01\0\0\0\0\0\xfd\xff"s);
+ EXPECT_THROW(TerminfoEntry t(s), std::runtime_error);
+}
+
+TEST(EntryParseTest, Strings) {
+ std::istringstream s(
+ "\x1a\x01\x01\0\0\0\0\0\x02\0\x08\0\0\0\x04\0\0\0foo\0bar\0"s);
+ TerminfoEntry t(s);
+ EXPECT_THAT(t.get(StringCapability::kBackTab), Optional(Eq("bar")));
+ EXPECT_THAT(t.get(StringCapability::kBell), Optional(Eq("foo")));
+}
+
+TEST(SystemTerminfoTest, NoSuchTerminal) {
+ std::string tmp = MakeTemporaryDirectory();
+ absl::Cleanup remove_tmp = [&] { std::filesystem::remove_all(tmp); };
+ SetEnv("TERMINFO", tmp.c_str());
+ EXPECT_THROW(TerminfoEntry::FromSystemDatabase("vt100"), std::runtime_error);
+}
+
+TEST(SystemTerminfoTest, LooksUpAdm3a) {
+ std::string tmp = MakeTemporaryDirectory();
+ absl::Cleanup remove_tmp = [&] { std::filesystem::remove_all(tmp); };
+ SetEnv("TERMINFO", tmp.c_str());
+
+ std::filesystem::create_directories(absl::StrCat(tmp, "/n"));
+ std::string binary_terminfo =
+ "\x1a\x01\x11\0\0\0\0\0\0\0\0\0notarealterminal\0"s;
+ std::ofstream(std::string(absl::StrCat(tmp, "/n/notarealterminal")))
+ .write(binary_terminfo.data(), binary_terminfo.size());
+
+ EXPECT_EQ(TerminfoEntry::FromSystemDatabase("notarealterminal").name(),
+ "notarealterminal");
+}
+
+} // namespace
+} // namespace goldfishterm
diff --git a/scripts/run_all_tests b/scripts/run_all_tests
index a73ca23..3282475 100755
--- a/scripts/run_all_tests
+++ b/scripts/run_all_tests
@@ -16,10 +16,22 @@
set -eu -o pipefail
-readonly TESTS=(src/builtin_test src/language_test src/parser_driver_test)
+readonly TESTS=(
+ goldfishterm/internal/string_capability_test
+ goldfishterm/terminfo_test
+ src/builtin_test
+ src/language_test
+ src/parser_driver_test
+)
+readonly MANUAL_TESTS=(goldfishterm/terminfo_system_test)
cd "$(dirname "$(realpath "$0")")/.."
-ninja "${TESTS[@]}"
+ninja "${TESTS[@]}" "${MANUAL_TESTS[@]}"
for test in "${TESTS[@]}"; do
"./$test"
done
+
+echo >&2 'Running terminfo system test; failed terminals are:'
+if find /usr/share/terminfo -type f -exec basename '{}' ';' | goldfishterm/terminfo_system_test; then
+ echo >&2 'No failed terminals!'
+fi
diff --git a/src/ui/terminal/line.cc b/src/ui/terminal/line.cc
index 8f156cb..0ae2d30 100644
--- a/src/ui/terminal/line.cc
+++ b/src/ui/terminal/line.cc
@@ -22,12 +22,15 @@
#include <algorithm>
#include <iostream>
+#include <memory>
#include <stdexcept>
#include <string>
#include <system_error>
#include <thread>
#include <type_traits>
+#include <utility>
+#include "goldfishterm/simple.h"
#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"
@@ -65,12 +68,6 @@ int TerminalColumns() {
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() {
@@ -79,6 +76,7 @@ TerminalLine::TerminalLine() {
}
EnterRawMode();
+ tty_ = std::make_unique<goldfishterm::SimpleTerminalOutput>(current_termios_);
sigwinch_watcher_ = std::thread([this] {
sigset_t sigwinch = SigsetContaining(SIGWINCH);
@@ -109,16 +107,17 @@ void TerminalLine::SetLine(std::string text) {
void TerminalLine::Refresh() {
absl::MutexLock lock(&mu_);
+ tty_->BeginningOfLine();
if (line_.size() < columns_) {
// We can fit the whole line and the cursor on the screen at once.
- WriteRaw(absl::StrCat(kBeginningOfLine, line_, ClearToEol()));
+ tty_->Write(line_);
} 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()));
+ tty_->Write("...");
+ tty_->Write(absl::string_view(&*line_.end() - to_display, to_display));
}
+ tty_->ClearToEndOfLine();
+ tty_->Flush();
}
char TerminalLine::GetChar() {
@@ -139,21 +138,26 @@ char TerminalLine::GetChar() {
void TerminalLine::Beep() {
absl::MutexLock lock(&mu_);
- WriteRaw("\a");
+ tty_->Beep();
+ tty_->Flush();
}
void TerminalLine::PrintLine(absl::string_view message) {
{
absl::MutexLock lock(&mu_);
- WriteRaw(absl::StrCat("\r\n", message, "\r\n"));
+ tty_->BeginningOfLine();
+ tty_->CursorDown();
+ tty_->Write(message);
+ tty_->BeginningOfLine();
+ tty_->CursorDown();
}
- Refresh();
+ Refresh(); // includes a flush
}
void TerminalLine::EnterRawMode() {
CheckedCall("tcgetattr", tcgetattr(STDIN_FILENO, &original_termios_));
- current_termios_ = original_termios_;
+ termios current_termios_ = original_termios_;
current_termios_.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
current_termios_.c_oflag &= ~(OPOST);
current_termios_.c_cflag |= CS8;
@@ -184,19 +188,6 @@ void TerminalLine::ReportSigwinch() {
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",
diff --git a/src/ui/terminal/line.h b/src/ui/terminal/line.h
index a90e2b7..dfe86cb 100644
--- a/src/ui/terminal/line.h
+++ b/src/ui/terminal/line.h
@@ -19,8 +19,10 @@
#include <termios.h>
+#include <memory>
#include <thread>
+#include "goldfishterm/simple.h"
#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"
@@ -81,6 +83,7 @@ class TerminalLine final {
void WriteRaw(absl::string_view bytes) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mu_);
termios original_termios_, current_termios_;
+ std::unique_ptr<goldfishterm::SimpleTerminalOutput> tty_;
std::thread sigwinch_watcher_;