1//! @file console.cpp
2//! @author ryftchen
3//! @brief The definitions (console) in the application module.
4//! @version 0.1.0
5//! @copyright Copyright (c) 2022-2026 ryftchen. All rights reserved.
6
7#include "console.hpp"
8#include "build.hpp"
9
10#ifndef _PRECOMPILED_HEADER
11#include <algorithm>
12#include <fstream>
13#include <iomanip>
14#include <iostream>
15#include <ranges>
16#else
17#include "application/pch/precompiled_header.hpp"
18#endif
19
20#include "utility/include/macro.hpp"
21
22namespace application::console
23{
24//! @brief Anonymous namespace.
25inline namespace
26{
27//! @brief Current console instance.
28thread_local constinit Console* currentSession = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
29} // namespace
30
31//! @brief Local options.
32namespace local
33{
34//! @brief The "usage" option.
35constexpr const char* const usage = MACRO_STRINGIFY(usage);
36//! @brief The "quit" option.
37constexpr const char* const quit = MACRO_STRINGIFY(quit);
38//! @brief The "trace" option.
39constexpr const char* const trace = MACRO_STRINGIFY(trace);
40//! @brief The "clean" option.
41constexpr const char* const clean = MACRO_STRINGIFY(clean);
42//! @brief The "batch" option.
43constexpr const char* const batch = MACRO_STRINGIFY(batch);
44} // namespace local
45
46Console::Console(const std::string_view greeting) : terminal{std::make_unique<Terminal>(args: greeting)}
47{
48 ::rl_attempted_completion_function = customCompleter;
49 setDefaultOptions();
50}
51
52Console::~Console()
53{
54 ::rl_attempted_completion_function = nullptr;
55 ::rl_free(emptyHistory);
56 ::rl_clear_history();
57 ::rl_set_prompt(nullptr);
58 ::rl_restore_prompt();
59
60 currentSession = nullptr;
61}
62
63void Console::registerOption(const std::string_view name, const std::string_view description, Callback callback)
64{
65 terminal->regTable.emplace(args: name, args: std::make_pair(x: description, y: std::move(callback)));
66 terminal->orderList.emplace_back(args: name);
67}
68
69void Console::setGreeting(const std::string_view greeting)
70{
71 terminal->greeting = greeting;
72}
73
74Console::RetCode Console::optionExecutor(const std::string& option) const
75{
76 std::vector<std::string> inputs{};
77 std::istringstream transfer(option);
78 std::copy(
79 first: std::istream_iterator<std::string>{transfer}, last: std::istream_iterator<std::string>{}, result: std::back_inserter(x&: inputs));
80 if (inputs.empty())
81 {
82 return RetCode::success;
83 }
84
85 const auto regIter = terminal->regTable.find(x: inputs.front());
86 if (regIter == terminal->regTable.cend())
87 {
88 throw std::runtime_error{
89 "The console option (" + inputs.front() + ") could not be found. Enter the \"" + local::usage
90 + "\" for help."};
91 }
92 const auto& mappedCallback = std::get<Callback>(p&: regIter->second);
93 return mappedCallback ? mappedCallback(inputs) : dummyCallback(inputs);
94}
95
96Console::RetCode Console::fileExecutor(const std::string& filename) const
97{
98 std::ifstream batches(filename);
99 if (!batches)
100 {
101 throw std::runtime_error{"Could not find the " + filename + " file to run."};
102 }
103
104 std::string input{};
105 std::uint32_t counter = 0;
106 std::ostringstream out{};
107 while (std::getline(is&: batches, str&: input))
108 {
109 if (input.empty() || (input.front() == '#'))
110 {
111 continue;
112 }
113
114 ++counter;
115 out << '#' << counter << ' ' << input << '\n';
116 std::cout << out.str() << std::flush;
117 out.str(s: "");
118 out.clear();
119 if (const auto result = optionExecutor(option: input); result != RetCode::success)
120 {
121 return result;
122 }
123 std::cout << std::endl;
124 }
125 return RetCode::success;
126}
127
128Console::RetCode Console::readLine()
129{
130 reserveConsole();
131 char* const buffer = ::readline(terminal->greeting.c_str());
132 if (!buffer)
133 {
134 std::cout << std::endl;
135 return RetCode::quit;
136 }
137
138 if (buffer[0] != '\0')
139 {
140 ::add_history(buffer);
141 }
142 const auto input = std::string{buffer};
143 ::rl_free(buffer);
144 return optionExecutor(option: input);
145}
146
147auto Console::getOptionHelpPairs() const
148{
149 const auto transformed =
150 terminal->orderList
151 | std::views::transform(
152 [this](const auto& option)
153 { return std::make_pair(option, std::get<std::string>(terminal->regTable.at(option))); });
154 return std::vector<std::ranges::range_value_t<decltype(transformed)>>{transformed.begin(), transformed.end()};
155}
156
157void Console::setDefaultOptions()
158{
159 auto& regTable = terminal->regTable;
160 auto& orderList = terminal->orderList;
161
162 regTable[local::usage] = std::make_pair(
163 x: "show help message",
164 y: [this](const Args& /*inputs*/)
165 {
166 const auto pairs = getOptionHelpPairs();
167 const auto align =
168 std::ranges::max(pairs, std::less<std::size_t>{}, [](const auto& pair) { return pair.first.length(); })
169 .first.length();
170 std::ostringstream out{};
171 out << std::setiosflags(std::ios_base::left);
172 for (const auto& [option, help] : pairs)
173 {
174 out << "- " << std::setw(static_cast<int>(align)) << option << " " << help << '\n';
175 }
176 out << std::resetiosflags(mask: std::ios_base::left);
177 std::cout << out.str() << std::flush;
178 return RetCode::success;
179 });
180 orderList.emplace_back(args: local::usage);
181
182 regTable[local::quit] = std::make_pair(
183 x: "exit the console",
184 y: [](const Args& /*inputs*/)
185 {
186 std::cout << "exit" << std::endl;
187 return RetCode::quit;
188 });
189 orderList.emplace_back(args: local::quit);
190
191 regTable[local::trace] = std::make_pair(
192 x: "get history of applied options",
193 y: [](const Args& /*inputs*/)
194 {
195 if (const auto* const* const historyList = ::history_list())
196 {
197 const auto align = std::to_string(val: ::history_length).length() + 1;
198 std::ostringstream out{};
199 out << std::setiosflags(std::ios_base::right);
200 for (std::uint32_t index = 0;
201 const auto& history : std::span{historyList, static_cast<std::size_t>(::history_length)})
202 {
203 out << std::setw(static_cast<int>(align)) << ((index++) + ::history_base) << " " << history->line
204 << '\n';
205 }
206 out << std::resetiosflags(mask: std::ios_base::right);
207 std::cout << out.str() << std::flush;
208 }
209 else
210 {
211 std::cout << "no history" << std::endl;
212 }
213 return RetCode::success;
214 });
215 orderList.emplace_back(args: local::trace);
216
217 regTable[local::clean] = std::make_pair(
218 x: "clear full screen",
219 y: [](const Args& /*inputs*/)
220 {
221 std::cout << "\033[2J\033[1;1H" << std::flush;
222 std::cout << build::banner() << std::endl;
223 return RetCode::success;
224 });
225 orderList.emplace_back(args: local::clean);
226
227 regTable[local::batch] = std::make_pair(
228 x: "run lines from the file [inputs: FILE]",
229 y: [this](const Args& inputs)
230 {
231 if (inputs.size() < 2)
232 {
233 throw std::runtime_error{
234 "Please enter the \"" + std::string{local::batch} + "\" and append with FILE (full path)."};
235 }
236 return fileExecutor(filename: inputs.at(n: 1));
237 });
238 orderList.emplace_back(args: local::batch);
239}
240
241void Console::saveState()
242{
243 ::rl_free(terminal->history);
244 terminal->history = ::history_get_history_state();
245}
246
247void Console::reserveConsole()
248{
249 if (currentSession == this)
250 {
251 return;
252 }
253
254 if (currentSession)
255 {
256 currentSession->saveState();
257 }
258 ::history_set_history_state(terminal->history ? terminal->history : emptyHistory);
259 currentSession = this;
260}
261
262char** Console::customCompleter(const char* text, int start, int /*end*/)
263{
264 return (start == 0) ? ::rl_completion_matches(text, customCompentry) : nullptr;
265}
266
267char* Console::customCompentry(const char* text, int state)
268{
269 static thread_local Terminal::RegisteredOption::iterator optionIterator{};
270 if (!currentSession)
271 {
272 return nullptr;
273 }
274
275 auto& regTable = currentSession->terminal->regTable;
276 if (state == 0)
277 {
278 optionIterator = regTable.begin();
279 }
280
281 while (optionIterator != regTable.cend())
282 {
283 const auto& option = optionIterator->first;
284 ++optionIterator;
285 if (option.starts_with(x: text))
286 {
287 return ::strdup(s: option.c_str());
288 }
289 }
290 return nullptr;
291}
292} // namespace application::console
293