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