aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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 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_;