#include "tui/tui.hpp" #include "tui/tui_helpers.hpp" #include #include #include #include #include #include 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>( 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 &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 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'; }