BootDispatch already runs --restore/--source synchronously before the TUI starts (Source takes its headless drain branch when no screen is attached), so the console buffer is complete by then. New --batch flag dumps that buffer (Tui::DumpOutput) to stdout and exits without launching the TUI — enabling scripted/CI runs and verify output capture (e.g. essim --batch --source s). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
290 lines
11 KiB
C++
290 lines
11 KiB
C++
#ifndef _TUI_HPP_
|
||
#define _TUI_HPP_
|
||
|
||
#include <atomic>
|
||
#include <deque>
|
||
#include <functional>
|
||
#include <iosfwd>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include <ftxui/component/component.hpp>
|
||
#include <ftxui/component/screen_interactive.hpp>
|
||
|
||
class System;
|
||
|
||
class Tui {
|
||
enum class Completion { None, Path, Command };
|
||
|
||
struct Prompt {
|
||
std::string question;
|
||
std::function<void(const std::string &)> on_answer;
|
||
Completion completion = Completion::None;
|
||
};
|
||
|
||
struct CommandSpec {
|
||
struct Param {
|
||
std::string name;
|
||
Completion completion = Completion::None;
|
||
};
|
||
std::vector<Param> params;
|
||
std::function<void(const std::vector<std::string> &)> action;
|
||
bool prompt_for_missing = true;
|
||
std::string description;
|
||
bool scriptable = true;
|
||
bool interactive = false; ///< opens a full-screen mode when called bare
|
||
bool hidden = false; ///< don't list in `help` (used by aliases)
|
||
};
|
||
|
||
// ---- Shell state ----
|
||
std::vector<std::string> history;
|
||
std::vector<std::string> recorded; // commands since the last 'new', for script-save
|
||
std::vector<std::string> output;
|
||
std::string input;
|
||
int cursor_pos;
|
||
int history_idx;
|
||
int scroll_offset; ///< Lines scrolled up from the tail; 0 = follow newest output.
|
||
bool quit;
|
||
bool in_source;
|
||
|
||
std::unique_ptr<System> sys;
|
||
std::deque<Prompt> pending;
|
||
std::map<std::string, CommandSpec> commands;
|
||
std::map<std::string, std::string> vars; ///< $var-style substitution table.
|
||
|
||
// ---- Screen orchestration ----
|
||
int screen_idx;
|
||
// Where Esc should send the user *next* if they came via an inter-screen
|
||
// jump (e.g. Enter on a part in `explore` → set-connector-type). −1 means
|
||
// "no back-link, Esc goes to the dashboard like usual". Always reset
|
||
// after consumption to avoid stale links across unrelated navigation.
|
||
int screen_back_idx = -1;
|
||
|
||
// ---- Connect screen state ----
|
||
std::vector<std::string> connect_modules;
|
||
int connect_m1_idx;
|
||
int connect_m2_idx;
|
||
std::string connect_p1_filter;
|
||
std::string connect_p2_filter;
|
||
std::vector<std::string> connect_p1_list;
|
||
std::vector<std::string> connect_p2_list;
|
||
int connect_p1_idx;
|
||
int connect_p2_idx;
|
||
int connect_focus_idx;
|
||
|
||
// ---- Explore screen state ----
|
||
std::vector<std::string> explore_modules;
|
||
int explore_module_idx;
|
||
std::vector<std::string> explore_types;
|
||
int explore_type_idx;
|
||
std::vector<std::string> explore_children;
|
||
int explore_child_idx;
|
||
std::string explore_child_filter;
|
||
std::string explore_detail_filter;
|
||
std::vector<std::string> explore_detail;
|
||
std::vector<std::string> explore_detail_sig; ///< parallel: `module\tsignal` per row (empty = no action)
|
||
int explore_detail_idx;
|
||
// Second detail sub-panel — populated only when the children pane is on
|
||
// the signals tab (top = local pins, bottom = net members).
|
||
std::vector<std::string> explore_detail2;
|
||
std::vector<std::string> explore_detail2_sig;
|
||
int explore_detail2_idx = 0;
|
||
std::string explore_header;
|
||
int explore_focus_idx;
|
||
|
||
// ---- Source-file loading (event-driven, one line per tick) ----
|
||
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
|
||
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
|
||
std::string loading_filename;
|
||
std::vector<std::string> loading_lines;
|
||
size_t loading_idx;
|
||
int loading_executed;
|
||
int loading_lineno;
|
||
bool loading_prev_in_source;
|
||
ftxui::ScreenInteractive *screen_ptr; ///< set in Run() so Source() can post events.
|
||
|
||
// ---- Dashboard scroll state (0 = top; grows as the user scrolls down) ----
|
||
int dashboard_scroll_offset = 0;
|
||
|
||
// ---- Generic error-notification dialog (modal) ----
|
||
bool error_open = false;
|
||
std::string error_message;
|
||
|
||
// ---- Generic Yes/No confirmation dialog (modal) ----
|
||
bool confirm_open = false;
|
||
std::string confirm_message;
|
||
std::function<void()> confirm_on_yes;
|
||
|
||
// ---- Generic file-picker dialog (modal) ----
|
||
// Reused for any "pick a path" interaction (export, save, restore, …).
|
||
// `persist_key` ties the dir + filename to a key persisted under the
|
||
// user-data directory so the next invocation under the same key
|
||
// re-opens on the same location. `on_confirm` runs the actual action
|
||
// when the user accepts a path.
|
||
//
|
||
// Optional `filters`: a horizontal selector (Toggle) at the top of
|
||
// the dialog with (label, extension) pairs. Picking a filter rewrites
|
||
// the filename's extension to match. The action then dispatches on
|
||
// extension (the dialog stays format-agnostic).
|
||
public:
|
||
struct FilenameFilter { std::string label; std::string extension; };
|
||
private:
|
||
struct FileDialogState {
|
||
bool open = false;
|
||
std::string title;
|
||
std::string persist_key;
|
||
std::string dir;
|
||
std::string filename;
|
||
std::vector<std::string> entries; ///< rebuilt every frame
|
||
std::vector<bool> entries_is_dir;
|
||
int entry_idx = 0;
|
||
// Focus map (variable: 0=filters [if any], else +1 each next slot):
|
||
// filters present → 0=filters 1=entries 2=filename 3=button (4 slots)
|
||
// no filters → 0=entries 1=filename 2=button (3 slots)
|
||
int focus_idx = 0;
|
||
std::string status;
|
||
std::vector<FilenameFilter> filters;
|
||
std::vector<std::string> filter_labels; ///< parallel to `filters`
|
||
int filter_idx = 0;
|
||
std::function<void(const std::string &)> on_confirm;
|
||
};
|
||
FileDialogState file_dialog;
|
||
|
||
// ---- Help screen state ----
|
||
int help_topic_idx = 0;
|
||
std::vector<std::string> help_topic_names; ///< populated by BuildHelpScreen
|
||
|
||
// ---- Analyze screen state (unified verify + analyze) ----
|
||
int analyze_focus_idx = 0; ///< 0=issues 1=groups 2=types
|
||
std::vector<std::string> analyze_issues;
|
||
std::vector<std::string> analyze_groups;
|
||
std::vector<std::string> analyze_types;
|
||
int analyze_issue_idx = 0;
|
||
int analyze_group_idx = 0;
|
||
int analyze_type_idx = 0;
|
||
|
||
// ---- Command palette (global, fuzzy-find over commands + objects) ----
|
||
bool palette_open = false;
|
||
std::string palette_query;
|
||
int palette_idx = 0;
|
||
// Rebuilt every frame from <query, sys>: label shown to the user.
|
||
std::vector<std::string> palette_labels;
|
||
// Parallel kind/payload for each label. kind: 'c'=command, 'm'=module,
|
||
// 's'=signal. payload: command name / module name / "module\tsignal".
|
||
std::vector<std::pair<char, std::string>> palette_items;
|
||
|
||
// ---- Signal-type popup (shared between net + explore screens) ----
|
||
bool sigtype_dialog_open = false;
|
||
std::string sigtype_dialog_mod;
|
||
std::string sigtype_dialog_sig;
|
||
std::vector<std::string> sigtype_dialog_entries; ///< ["power","gnd","other"]
|
||
int sigtype_dialog_choice = 0;
|
||
|
||
// ---- Set-type screen state ----
|
||
std::vector<std::string> settype_modules;
|
||
int settype_m_idx;
|
||
std::string settype_p_filter;
|
||
std::vector<std::string> settype_p_list;
|
||
int settype_p_idx;
|
||
std::string settype_type;
|
||
std::string settype_status;
|
||
int settype_focus_idx;
|
||
|
||
public:
|
||
Tui();
|
||
~Tui();
|
||
void Run();
|
||
void DumpCommandsMd(std::ostream &out) const;
|
||
// Write the accumulated console output to `out`. Used by batch mode to
|
||
// surface a script's output without starting the TUI.
|
||
void DumpOutput(std::ostream &out) const;
|
||
|
||
// Boot-time hook: dispatch a single command exactly as if the user
|
||
// typed it (e.g. `restore foo.essim` or `source bring-up.essim`).
|
||
// Call before `Run()` to seed the system before the event loop starts.
|
||
void BootDispatch(const std::string &raw);
|
||
|
||
private:
|
||
// Lifecycle (commands.cpp)
|
||
void RegisterCommands();
|
||
// Per-file command-group registrators. Each adds entries to the
|
||
// `commands` map. Called from RegisterCommands().
|
||
void RegisterExportCommands(); // commands_export.cpp
|
||
|
||
// Shell (shell.cpp)
|
||
void Print(const std::string &line);
|
||
void Submit();
|
||
void Dispatch(const std::string &raw);
|
||
void Finalize(const std::string &name,
|
||
const CommandSpec &spec,
|
||
const std::vector<std::string> &args);
|
||
void HistoryUp();
|
||
void HistoryDown();
|
||
void CancelPending();
|
||
void LoadHistory();
|
||
void AppendHistory(const std::string &cmd);
|
||
void Source(const std::string &filename);
|
||
void ProcessNextSourceLine();
|
||
std::string ExpandVars(const std::string &s) const;
|
||
|
||
// Completion (completion.cpp)
|
||
void CompleteCommand(size_t start = 0);
|
||
void CompletePath(size_t start = 0);
|
||
void CompleteInline();
|
||
|
||
// Open the signal-type popup for <mod>/<sig> (no-op if names don't resolve).
|
||
void OpenSignalTypeDialog(const std::string &mod_name,
|
||
const std::string &sig_name);
|
||
// Apply the selected type to the targeted signal, record a
|
||
// `set-signal-type` line (deduping consecutive edits of the same signal),
|
||
// and close the popup.
|
||
void ApplySignalTypeChoice();
|
||
|
||
// Filtered part list rebuild (used by connect & set-connector-type screens)
|
||
void RefreshFilteredPartList(const std::vector<std::string> &modules,
|
||
int m_idx,
|
||
const std::string &filter,
|
||
std::vector<std::string> &out,
|
||
int &sel_idx);
|
||
|
||
// Screen builders (screen_*.cpp)
|
||
ftxui::Component BuildMainScreen(ftxui::ScreenInteractive &screen);
|
||
ftxui::Component BuildConnectScreen();
|
||
ftxui::Component BuildSettypeScreen();
|
||
ftxui::Component BuildExploreScreen();
|
||
ftxui::Component BuildDashboardScreen();
|
||
ftxui::Component BuildAnalyzeScreen();
|
||
ftxui::Component BuildHelpScreen();
|
||
ftxui::Component BuildFileDialog();
|
||
ftxui::Component BuildErrorModal();
|
||
ftxui::Component BuildConfirmModal();
|
||
// Pop a centred modal with `msg` and an OK button. Esc / Enter close
|
||
// it. Use for actionable failures the user must see (write errors,
|
||
// bad inputs, etc.) — for normal feedback keep `Print()`.
|
||
void ShowError(const std::string &msg);
|
||
// Pop a centred Yes/No modal. `on_yes` runs only if the user picks
|
||
// Yes (Esc and No cancel). Use for destructive confirmations
|
||
// (overwrite a file, delete a snapshot, …).
|
||
void ShowConfirm(const std::string &msg, std::function<void()> on_yes);
|
||
// Open the picker modal. `persist_key` controls where the last-used
|
||
// dir + filename are stored (one tiny file per key under the user
|
||
// data directory). `on_confirm` runs when the user presses Enter on
|
||
// the action button — it receives the absolute path the user picked.
|
||
void OpenFileDialog(std::string title,
|
||
std::string persist_key,
|
||
std::string default_filename,
|
||
std::vector<FilenameFilter> filters,
|
||
std::function<void(const std::string &)> on_confirm);
|
||
void ConfirmFileDialog();
|
||
ftxui::Component BuildSignalTypeModal();
|
||
ftxui::Component BuildPaletteModal();
|
||
// Open palette (resets query/index, builds initial list).
|
||
void OpenPalette();
|
||
// Execute the currently-highlighted palette entry.
|
||
void ActivatePaletteEntry();
|
||
};
|
||
|
||
#endif // _TUI_HPP_
|