#include "tui/tui.hpp" #include "system/modules.hpp" #include "system/parts.hpp" #include "system/signals.hpp" #include "system/system.hpp" #include #include #include #include #include #include #include #include #include #include using namespace ftxui; Tui::Tui() : cursor_pos(0), history_idx(-1), quit(false), screen_idx(0), search_types{"parts", "signals"}, search_module_idx(0), search_type_idx(0), search_focus_idx(0) { LoadHistory(); RegisterCommands(); Print("essim — type 'help' for commands, 'quit' to exit."); } Tui::~Tui() = default; 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)"); } static std::string ToLower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); return s; } std::vector Tui::Tokenize(const std::string &s) { std::vector out; std::string cur; bool in_q = false; for (char c : s) { if (c == '"') { in_q = !in_q; continue; } if (!in_q && std::isspace((unsigned char)c)) { if (!cur.empty()) { out.push_back(std::move(cur)); cur.clear(); } } else { cur.push_back(c); } } if (!cur.empty()) out.push_back(std::move(cur)); return out; } void Tui::RegisterCommands() { commands["help"] = { {}, [this](auto &) { Print("Commands (give params inline or fill them in via prompt):"); Print(" new create a new (empty) system"); Print(" load load a module into the system"); Print(" search interactive search (parts/signals, live filter)"); Print(" clear clear the visualization area"); Print(" help show this message"); Print(" quit / exit leave essim"); Print(" Esc cancel a multi-step prompt"); Print(" Tab complete command name / file path"); }}; commands["clear"] = { {}, [this](auto &) { output.clear(); }}; commands["quit"] = { {}, [this](auto &) { quit = true; }}; commands["exit"] = { {}, [this](auto &) { quit = true; }}; commands["new"] = { {}, [this](auto &) { sys = std::make_unique(); Print("system created."); }}; commands["load"] = { {{"module name", false}, {"filename", true}, {"import type [mentor|altium|ods]", false}}, [this](const std::vector &args) { if (!sys) { Print("no system: run 'new' first."); return; } std::string ls = args[2]; std::transform(ls.begin(), ls.end(), ls.begin(), [](unsigned char c) { return std::tolower(c); }); ImportType t; if (ls == "mentor") t = ImportType::IMPORT_MENTOR; else if (ls == "altium") t = ImportType::IMPORT_ALTIUM; else if (ls == "ods") t = ImportType::IMPORT_ODS; else { Print("unknown import type: " + args[2]); return; } try { sys->Load(args[0], args[1], t); Module *mod = sys->modules()->get(args[0]); Print("loaded '" + args[0] + "' from " + args[1]); Print(" parts: " + std::to_string(mod->size())); Print(" signals: " + std::to_string(mod->signals->size())); } catch (const std::exception &e) { Print(std::string("load failed: ") + e.what()); } } }; commands["search"] = { {}, [this](auto &) { if (!sys) { Print("no system: run 'new' first."); return; } search_modules.clear(); for (auto &m : *sys->modules()) search_modules.push_back(m.first); if (search_modules.empty()) { Print("no modules loaded."); return; } search_module_idx = 0; search_type_idx = 0; search_query.clear(); search_focus_idx = 0; // start with the query input focused screen_idx = 1; }}; } 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]); 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 + "'"); history.push_back(raw); AppendHistory(raw); return; } auto args = std::make_shared>( tokens.begin() + 1, tokens.end()); if (args->size() == spec.params.size()) { 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.path_completion, }); } } void Tui::Finalize(const std::string &name, const CommandSpec &spec, const std::vector &args) { std::string canonical = name; for (const auto &a : args) { if (a.find_first_of(" \t\"") != std::string::npos) canonical += " \"" + a + "\""; else canonical += " " + a; } history.push_back(canonical); AppendHistory(canonical); spec.action(args); } static 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 {}; } 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::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'; } static std::string LongestCommonPrefix(const std::vector &v) { if (v.empty()) return ""; std::string lcp = v[0]; for (size_t i = 1; i < v.size(); ++i) { size_t k = 0; while (k < lcp.size() && k < v[i].size() && lcp[k] == v[i][k]) ++k; lcp.resize(k); } return lcp; } void Tui::CompleteCommand() { std::vector matches; for (const auto &kv : commands) if (kv.first.rfind(input, 0) == 0) matches.push_back(kv.first); if (matches.empty()) return; if (matches.size() == 1) { input = matches[0]; cursor_pos = (int)input.size(); return; } std::string lcp = LongestCommonPrefix(matches); if (lcp.size() > input.size()) { input = lcp; cursor_pos = (int)input.size(); return; } std::string line = " "; for (const auto &m : matches) line += " " + m; Print(line); } void Tui::CompletePath() { namespace fs = std::filesystem; auto pos = input.rfind('/'); std::string disp, prefix; if (pos == std::string::npos) { disp = ""; prefix = input; } else { disp = input.substr(0, pos + 1); prefix = input.substr(pos + 1); } std::string resolved = disp.empty() ? "." : disp; if (!resolved.empty() && resolved[0] == '~') { if (const char *home = std::getenv("HOME")) resolved = std::string(home) + resolved.substr(1); } std::vector names; std::vector is_dir; try { for (const auto &e : fs::directory_iterator(resolved)) { std::string n = e.path().filename().string(); if (n.rfind(prefix, 0) == 0) { names.push_back(n); is_dir.push_back(e.is_directory()); } } } catch (const std::exception &) { return; } if (names.empty()) return; if (names.size() == 1) { input = disp + names[0] + (is_dir[0] ? "/" : ""); cursor_pos = (int)input.size(); return; } std::string lcp = LongestCommonPrefix(names); if (lcp.size() > prefix.size()) { input = disp + lcp; cursor_pos = (int)input.size(); return; } std::string line = " "; for (size_t i = 0; i < names.size(); ++i) line += " " + names[i] + (is_dir[i] ? "/" : ""); Print(line); } void Tui::Run() { auto screen = ScreenInteractive::Fullscreen(); // ---- Main TUI ---- InputOption opt; opt.multiline = false; opt.cursor_position = &cursor_pos; opt.on_enter = [this] { Submit(); }; opt.transform = [](InputState s) { auto el = s.element; if (s.is_placeholder) el |= dim; return el; }; auto input_component = Input(&input, "type a command…", opt); auto main_renderer = Renderer(input_component, [this, &screen, input_component] { if (quit) screen.Exit(); Elements lines; for (const auto &l : output) lines.push_back(text(l)); auto view = vbox(std::move(lines)) | focusPositionRelative(0, 1) | yframe | flex; std::string label = pending.empty() ? "> " : pending.front().question + "? "; return vbox({ view, separator(), hbox({text(label), input_component->Render()}), }) | border; }); // ---- Search screen ---- InputOption query_opt; query_opt.multiline = false; query_opt.transform = opt.transform; auto query_input = Input(&search_query, "filter…", query_opt); auto module_menu = Menu(&search_modules, &search_module_idx); auto type_menu = Menu(&search_types, &search_type_idx); auto search_components = Container::Vertical( {query_input, module_menu, type_menu}, &search_focus_idx); auto search_renderer = Renderer(search_components, [this, query_input, module_menu, type_menu] { // Compute filtered list. Elements result_lines; int total = 0; if (!search_modules.empty() && sys) { const std::string &mname = search_modules[search_module_idx]; try { Module *mod = sys->modules()->get(mname); std::string needle = ToLower(search_query); if (search_type_idx == 0) { // parts for (auto &pkv : *mod) { if (needle.empty() || ToLower(pkv.first).find(needle) != std::string::npos) { result_lines.push_back( text(" " + pkv.first + " (" + std::to_string(pkv.second->size()) + " pins)")); ++total; } } } else { // signals for (auto &skv : *mod->signals) { if (needle.empty() || ToLower(skv.first).find(needle) != std::string::npos) { result_lines.push_back( text(" " + skv.first + " (" + std::to_string(skv.second->size()) + " pins)")); ++total; } } } } catch (const std::exception &) {} } auto left = vbox({ text("module") | bold, module_menu->Render() | yframe | flex, separator(), text("type") | bold, type_menu->Render(), }) | size(WIDTH, EQUAL, 28); auto right = vbox({ hbox({text(" search: "), query_input->Render() | flex}) | border, text(std::to_string(total) + " match(es)") | dim, vbox(std::move(result_lines)) | yframe | flex, }) | flex; return vbox({ hbox({left, separator(), right}) | flex, text(" Tab: cycle focus | Esc: leave search ") | dim, }) | border; }); // ---- Screen tab + global key handling ---- auto tab = Container::Tab({main_renderer, search_renderer}, &screen_idx); auto root = CatchEvent(tab, [this](Event e) { if (screen_idx == 1) { // Search mode if (e == Event::Escape) { screen_idx = 0; return true; } // Cycle focus query → modules → type → query (Menu eats Tab otherwise). if (e == Event::Tab) { search_focus_idx = (search_focus_idx + 1) % 3; return true; } if (e == Event::TabReverse) { search_focus_idx = (search_focus_idx + 2) % 3; return true; } return false; // let menus / input handle the rest } // Main mode if (e == Event::Escape && !pending.empty()) { CancelPending(); return true; } if (e == Event::ArrowUp || e == Event::ArrowDown) { if (pending.empty()) { if (e == Event::ArrowUp) HistoryUp(); else HistoryDown(); } return true; } if (e == Event::Tab) { if (pending.empty()) { if (input.find(' ') == std::string::npos) CompleteCommand(); } else if (pending.front().path_completion) { CompletePath(); } return true; } return false; }); screen.Loop(root); }