Domain - Signal carries a SignalType (Power/GndShield/Other), auto-inferred from the name in Signal::Signal via infer_signal_type. Override with the new `set-signal-type` command. - SignalType extracted to its own header so Pin can store an `expected_signal_type` without a pins↔signals include cycle. - pin_role(connector_type, pin_name) → SignalType lookup, called from set-type to populate each Pin's expected_signal_type. The VPX 3U table is currently a stub (returns Other). - New `verify` command walks typed parts and reports pins whose connected signal's type doesn't match the expectation. - ODS importer no longer drops pins with empty signal column — they stay in the part as NC, matching the rule "a pin is either NC or connected to a signal". - persist: new S tag for non-default signal type overrides. Tests - doctest v2.4.11 via FetchContent (with CMAKE_POLICY_VERSION_MINIMUM shim, doctest's CMakeLists has a too-old floor for current CMake). - Source files moved into a static library `essim_lib` so both `essim` and `essim_tests` reuse the same compilation. main.cpp is the only file kept out of the lib. - Layer 1 (pure helpers): ToLower, LongestCommonPrefix, Tokenize, NaturalLess (numeric/case/leading-zero edge cases + total-order invariants), signal_type round-trips and infer_signal_type families, VpxTransform registry + symmetry + reference-table mapping for connector P0 row 1, IdentityTransform same-name wiring. - Layer 2 (round-trip): build a synthetic 2-module system in code, save → restore → assert modules / parts / connector_types / NC pins / signal type overrides / connections + pin_map are all preserved. - Tui::Tokenize moved to a free function in tui_helpers so tests can call it without dragging ftxui into the unit-test layer. - 27 test cases, 123 assertions, ~150 ms. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
221 lines
6.2 KiB
C++
221 lines
6.2 KiB
C++
#include "tui/tui.hpp"
|
|
#include "tui/tui_helpers.hpp"
|
|
|
|
#include <cctype>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <set>
|
|
#include <system_error>
|
|
|
|
void Tui::Print(const std::string &line) {
|
|
output.push_back(line);
|
|
}
|
|
|
|
void Tui::HistoryUp() {
|
|
if (history.empty()) return;
|
|
if (history_idx == -1) history_idx = (int)history.size() - 1;
|
|
else if (history_idx > 0) history_idx--;
|
|
input = history[history_idx];
|
|
cursor_pos = (int)input.size();
|
|
}
|
|
|
|
void Tui::HistoryDown() {
|
|
if (history_idx == -1) return;
|
|
history_idx++;
|
|
if (history_idx >= (int)history.size()) {
|
|
history_idx = -1;
|
|
input.clear();
|
|
} else {
|
|
input = history[history_idx];
|
|
}
|
|
cursor_pos = (int)input.size();
|
|
}
|
|
|
|
void Tui::CancelPending() {
|
|
if (pending.empty()) return;
|
|
pending.clear();
|
|
input.clear();
|
|
cursor_pos = 0;
|
|
history_idx = -1;
|
|
Print("(cancelled)");
|
|
}
|
|
|
|
void Tui::Submit() {
|
|
if (!pending.empty()) {
|
|
if (input.empty()) { Print("(empty — Esc to cancel)"); return; }
|
|
Prompt p = std::move(pending.front());
|
|
pending.pop_front();
|
|
Print(" " + p.question + ": " + input);
|
|
std::string answer = std::move(input);
|
|
input.clear();
|
|
cursor_pos = 0;
|
|
history_idx = -1;
|
|
p.on_answer(answer);
|
|
return;
|
|
}
|
|
|
|
if (input.empty()) return;
|
|
history_idx = -1;
|
|
Print("> " + input);
|
|
std::string raw = std::move(input);
|
|
input.clear();
|
|
cursor_pos = 0;
|
|
Dispatch(raw);
|
|
}
|
|
|
|
void Tui::Dispatch(const std::string &raw) {
|
|
auto tokens = Tokenize(raw);
|
|
if (tokens.empty()) return;
|
|
|
|
auto it = commands.find(tokens[0]);
|
|
if (it == commands.end()) {
|
|
Print("unknown command: " + tokens[0]);
|
|
if (!in_source) { history.push_back(raw); AppendHistory(raw); }
|
|
return;
|
|
}
|
|
|
|
const std::string name = it->first;
|
|
const CommandSpec &spec = it->second;
|
|
|
|
if (tokens.size() - 1 > spec.params.size()) {
|
|
Print("too many arguments for '" + name + "'");
|
|
if (!in_source) { history.push_back(raw); AppendHistory(raw); }
|
|
return;
|
|
}
|
|
|
|
auto args = std::make_shared<std::vector<std::string>>(
|
|
tokens.begin() + 1, tokens.end());
|
|
|
|
if (args->size() == spec.params.size() || !spec.prompt_for_missing) {
|
|
Finalize(name, spec, *args);
|
|
return;
|
|
}
|
|
|
|
for (size_t i = args->size(); i < spec.params.size(); ++i) {
|
|
bool last = (i + 1 == spec.params.size());
|
|
const auto ¶m = spec.params[i];
|
|
pending.push_back({
|
|
param.name,
|
|
[this, name, &spec, args, last](const std::string &s) {
|
|
args->push_back(s);
|
|
if (last) Finalize(name, spec, *args);
|
|
},
|
|
param.completion,
|
|
});
|
|
}
|
|
}
|
|
|
|
void Tui::Finalize(const std::string &name,
|
|
const CommandSpec &spec,
|
|
const std::vector<std::string> &args) {
|
|
std::string canonical = name;
|
|
for (const auto &a : args) {
|
|
if (a.find_first_of(" \t\"") != std::string::npos)
|
|
canonical += " \"" + a + "\"";
|
|
else
|
|
canonical += " " + a;
|
|
}
|
|
if (!in_source) {
|
|
history.push_back(canonical);
|
|
AppendHistory(canonical);
|
|
}
|
|
spec.action(args);
|
|
|
|
static const std::set<std::string> no_record = {
|
|
"clear", "help", "quit", "exit", "source", "script-save",
|
|
};
|
|
if (spec.scriptable && !no_record.count(name)) recorded.push_back(canonical);
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::filesystem::path HistoryPath() {
|
|
namespace fs = std::filesystem;
|
|
#ifdef _WIN32
|
|
if (const char *p = std::getenv("LOCALAPPDATA"); p && *p)
|
|
return fs::path(p) / "essim" / "history";
|
|
if (const char *p = std::getenv("APPDATA"); p && *p)
|
|
return fs::path(p) / "essim" / "history";
|
|
if (const char *p = std::getenv("USERPROFILE"); p && *p)
|
|
return fs::path(p) / "AppData" / "Local" / "essim" / "history";
|
|
#else
|
|
if (const char *p = std::getenv("XDG_DATA_HOME"); p && *p)
|
|
return fs::path(p) / "essim" / "history";
|
|
if (const char *p = std::getenv("HOME"); p && *p)
|
|
return fs::path(p) / ".local" / "share" / "essim" / "history";
|
|
#endif
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void Tui::LoadHistory() {
|
|
auto p = HistoryPath();
|
|
if (p.empty()) return;
|
|
std::ifstream f(p);
|
|
std::string line;
|
|
while (std::getline(f, line))
|
|
if (!line.empty()) history.push_back(line);
|
|
}
|
|
|
|
void Tui::Source(const std::string &filename) {
|
|
std::string expanded = filename;
|
|
if (!expanded.empty() && expanded[0] == '~') {
|
|
if (const char *home = std::getenv("HOME"))
|
|
expanded = std::string(home) + expanded.substr(1);
|
|
}
|
|
std::ifstream f(expanded);
|
|
if (!f) { Print("source failed: cannot open " + filename); return; }
|
|
|
|
bool prev = in_source;
|
|
in_source = true;
|
|
|
|
int executed = 0;
|
|
int lineno = 0;
|
|
bool aborted = false;
|
|
std::string line;
|
|
while (std::getline(f, line)) {
|
|
++lineno;
|
|
size_t start = line.find_first_not_of(" \t");
|
|
if (start == std::string::npos) continue;
|
|
if (line[start] == '#') continue;
|
|
std::string trimmed = line.substr(start);
|
|
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
|
|
trimmed.pop_back();
|
|
if (trimmed.empty()) continue;
|
|
|
|
input = trimmed;
|
|
cursor_pos = (int)input.size();
|
|
Submit();
|
|
++executed;
|
|
|
|
if (screen_idx != 0) {
|
|
Print("source: line " + std::to_string(lineno)
|
|
+ " is interactive (would open a screen) — aborting.");
|
|
screen_idx = 0;
|
|
aborted = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
in_source = prev;
|
|
|
|
if (!aborted)
|
|
Print("source: " + filename + " (" + std::to_string(executed) + " line(s))");
|
|
}
|
|
|
|
void Tui::AppendHistory(const std::string &cmd) {
|
|
auto p = HistoryPath();
|
|
if (p.empty()) return;
|
|
std::string trimmed = cmd;
|
|
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
|
|
trimmed.pop_back();
|
|
if (trimmed.empty()) return;
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(p.parent_path(), ec);
|
|
if (ec) return;
|
|
std::ofstream f(p, std::ios::app);
|
|
if (f) f << trimmed << '\n';
|
|
}
|