Altium import, nets, canonical pins, component kinds, set/$var, scrollback, source modal.
Major additions, all wired end-to-end with doctest coverage:
- Altium netlist importer (`imports/import_altium.{hpp,cpp}`): two-pass
parser for `[ ]` parts and `( )` signals; `System::Load` no longer has
the IMPORT_ALTIUM hole.
- `duplicate <src> <dst>` deep-copies a module (signals, parts, pins,
rewired signals); connections excluded by design.
- Nets (`system/nets.{hpp,cpp}`): BFS over `Connection::pin_map` to
return the transitive (Module, Signal) closure. `verify` extended with
a second pass flagging Power↔GndShield inconsistencies in bridged
nets; new `net <module> <signal>` command for inspection.
- Canonical pin names (`system/pin_name.{hpp,cpp}`): zero-padded digit
suffix lets A1 ↔ A001 pair via `IdentityTransform` and
`CheckIdentityCompatible` without losing the imported notation.
- Component classification (`system/component_kind.{hpp,cpp}`):
`Part::kind` inferred at construction from the reference-designator
prefix (longest-match: LED/TP/SW/FB/MK/MP/MH/HS/RA/RN/RP/RV first,
then R/C/L/F/D/Q/U/J/P/Y/X/S).
- Identity wiring tolerance: `CheckIdentityCompatible` accepts the
subset case (typical when one importer drops NC pins, e.g. Altium)
and surfaces orphans as an info string. `FillIdentityNCs`
materialises orphan canonical positions as NC pins on the missing
side at connect time.
- Connector layout preparation: `pin_layout(kind)` and
`FillPartFromLayout(part, kind)` stubs in `pin_role`, called from
`set-type`. Empty today; populate alongside `vpx_3u_role`.
- TUI scrollback: PageUp/PageDown step 10 lines, Home/End jump to
ends; `Print()` snaps back to the tail.
- `set <name> <value>` declares session variables; `$name` / `${name}`
expanded inside `Finalize` between canonical-form recording and the
action call — history and script-save preserve `$var` references.
- Long `source` scripts now show a centred "Computing…" modal with a
N/M progress counter. Driven by a ticker thread that posts one
paced `Event::Special` per processed line, ack'd by the main thread,
so heavy lines don't backlog ticks and freeze the counter.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,14 +2,17 @@
|
||||
#include "tui/tui_helpers.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
void Tui::Print(const std::string &line) {
|
||||
output.push_back(line);
|
||||
scroll_offset = 0; // any new line snaps the view back to the tail
|
||||
}
|
||||
|
||||
void Tui::HistoryUp() {
|
||||
@@ -109,6 +112,8 @@ void Tui::Dispatch(const std::string &raw) {
|
||||
void Tui::Finalize(const std::string &name,
|
||||
const CommandSpec &spec,
|
||||
const std::vector<std::string> &args) {
|
||||
// Build the canonical form from the *raw* args (pre-expansion) so that
|
||||
// history and script-save preserve `$var` references.
|
||||
std::string canonical = name;
|
||||
for (const auto &a : args) {
|
||||
if (a.find_first_of(" \t\"") != std::string::npos)
|
||||
@@ -120,7 +125,12 @@ void Tui::Finalize(const std::string &name,
|
||||
history.push_back(canonical);
|
||||
AppendHistory(canonical);
|
||||
}
|
||||
spec.action(args);
|
||||
|
||||
// Expand variables only for the action call so commands see resolved values.
|
||||
std::vector<std::string> exec_args;
|
||||
exec_args.reserve(args.size());
|
||||
for (const auto &a : args) exec_args.push_back(ExpandVars(a));
|
||||
spec.action(exec_args);
|
||||
|
||||
static const std::set<std::string> no_record = {
|
||||
"clear", "help", "quit", "exit", "source", "script-save",
|
||||
@@ -128,6 +138,34 @@ void Tui::Finalize(const std::string &name,
|
||||
if (spec.scriptable && !no_record.count(name)) recorded.push_back(canonical);
|
||||
}
|
||||
|
||||
std::string Tui::ExpandVars(const std::string &s) const {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
size_t i = 0;
|
||||
while (i < s.size()) {
|
||||
if (s[i] != '$') { out.push_back(s[i++]); continue; }
|
||||
size_t j = i + 1;
|
||||
bool braces = (j < s.size() && s[j] == '{');
|
||||
if (braces) ++j;
|
||||
size_t start = j;
|
||||
while (j < s.size() && (std::isalnum((unsigned char)s[j]) || s[j] == '_')) ++j;
|
||||
std::string name = s.substr(start, j - start);
|
||||
if (braces) {
|
||||
if (j >= s.size() || s[j] != '}') {
|
||||
// Unmatched brace — emit literally and resume after the '$'.
|
||||
out.push_back('$'); ++i; continue;
|
||||
}
|
||||
++j;
|
||||
}
|
||||
if (name.empty()) { out.push_back('$'); ++i; continue; }
|
||||
auto it = vars.find(name);
|
||||
if (it != vars.end()) out += it->second;
|
||||
else out += s.substr(i, j - i); // keep unknown as-is
|
||||
i = j;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path HistoryPath() {
|
||||
@@ -168,19 +206,59 @@ void Tui::Source(const std::string &filename) {
|
||||
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;
|
||||
// Slurp the whole file so we can drive line-by-line processing from the
|
||||
// event loop (one line per posted task). This lets the screen redraw
|
||||
// between lines and surface the "Computing…" modal.
|
||||
loading_lines.clear();
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
++lineno;
|
||||
size_t start = line.find_first_not_of(" \t");
|
||||
while (std::getline(f, line)) loading_lines.push_back(line);
|
||||
|
||||
loading_filename = filename;
|
||||
loading_idx = 0;
|
||||
loading_executed = 0;
|
||||
loading_lineno = 0;
|
||||
loading_prev_in_source = in_source;
|
||||
in_source = true;
|
||||
loading = true;
|
||||
|
||||
if (!screen_ptr) {
|
||||
// Headless fallback (e.g. tests): drain synchronously.
|
||||
while (loading.load()) ProcessNextSourceLine();
|
||||
return;
|
||||
}
|
||||
|
||||
// Pacing thread: post one tick at a time and wait for the main thread
|
||||
// to ack it (by clearing tick_in_flight from ProcessNextSourceLine)
|
||||
// before sleeping & posting the next. Without this, a long-running line
|
||||
// (e.g. a Mentor parse) lets the ticker queue many ticks; FTXUI then
|
||||
// drains them in a batch without redrawing between, so the modal
|
||||
// counter freezes.
|
||||
tick_in_flight.store(false);
|
||||
std::thread([this]() {
|
||||
using namespace std::chrono_literals;
|
||||
while (loading.load()) {
|
||||
// Wait until main thread is ready for a new tick.
|
||||
while (loading.load() && tick_in_flight.load())
|
||||
std::this_thread::sleep_for(5ms);
|
||||
if (!loading.load()) break;
|
||||
std::this_thread::sleep_for(30ms);
|
||||
if (!loading.load()) break;
|
||||
tick_in_flight.store(true);
|
||||
if (screen_ptr)
|
||||
screen_ptr->PostEvent(ftxui::Event::Special("\x02tick"));
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
void Tui::ProcessNextSourceLine() {
|
||||
if (!loading.load()) return;
|
||||
while (loading_idx < loading_lines.size()) {
|
||||
const std::string &raw = loading_lines[loading_idx++];
|
||||
++loading_lineno;
|
||||
size_t start = raw.find_first_not_of(" \t");
|
||||
if (start == std::string::npos) continue;
|
||||
if (line[start] == '#') continue;
|
||||
std::string trimmed = line.substr(start);
|
||||
if (raw[start] == '#') continue;
|
||||
std::string trimmed = raw.substr(start);
|
||||
while (!trimmed.empty() && std::isspace((unsigned char)trimmed.back()))
|
||||
trimmed.pop_back();
|
||||
if (trimmed.empty()) continue;
|
||||
@@ -188,21 +266,27 @@ void Tui::Source(const std::string &filename) {
|
||||
input = trimmed;
|
||||
cursor_pos = (int)input.size();
|
||||
Submit();
|
||||
++executed;
|
||||
++loading_executed;
|
||||
|
||||
if (screen_idx != 0) {
|
||||
Print("source: line " + std::to_string(lineno)
|
||||
Print("source: line " + std::to_string(loading_lineno)
|
||||
+ " is interactive (would open a screen) — aborting.");
|
||||
screen_idx = 0;
|
||||
aborted = true;
|
||||
break;
|
||||
loading.store(false);
|
||||
tick_in_flight.store(false);
|
||||
in_source = loading_prev_in_source;
|
||||
return;
|
||||
}
|
||||
// One effective line per tick — ack so the ticker can pace the next.
|
||||
tick_in_flight.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
in_source = prev;
|
||||
|
||||
if (!aborted)
|
||||
Print("source: " + filename + " (" + std::to_string(executed) + " line(s))");
|
||||
Print("source: " + loading_filename
|
||||
+ " (" + std::to_string(loading_executed) + " line(s))");
|
||||
loading.store(false);
|
||||
tick_in_flight.store(false);
|
||||
in_source = loading_prev_in_source;
|
||||
}
|
||||
|
||||
void Tui::AppendHistory(const std::string &cmd) {
|
||||
|
||||
Reference in New Issue
Block a user