Skip to content

Commit

Permalink
Add console frontend executeable
Browse files Browse the repository at this point in the history
  • Loading branch information
oblivioncth committed Nov 21, 2024
1 parent d44a2e0 commit 81bd3cb
Show file tree
Hide file tree
Showing 15 changed files with 571 additions and 17 deletions.
11 changes: 7 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ project(CLIFp

# Get helper scripts
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchOBCMake.cmake)
fetch_ob_cmake("19d33b5bb1752b50767f78ca3e17796868354ac3")
fetch_ob_cmake("ed633b61c87f34757c5c26da32563543665940b7")

# Initialize project according to standard rules
include(OB/Project)
Expand Down Expand Up @@ -77,7 +77,7 @@ endif()

include(OB/FetchQx)
ob_fetch_qx(
REF "44a5b020784044d130765cddc0d3514bb01180c1"
REF "62f9e486d8a850123d9a4d834509282ad15e3382"
COMPONENTS
${CLIFP_QX_COMPONENTS}
)
Expand Down Expand Up @@ -111,8 +111,10 @@ string(TOLOWER "${BACKEND_ALIAS_NAME}" BACKEND_ALIAS_NAME_LC)
set(FRONTEND_FRAMEWORK_TARGET_NAME ${PROJECT_NAMESPACE_LC}_frontend_framework)
set(FRONTEND_FRAMEWORK_ALIAS_NAME FrontendFramework)
add_subdirectory(lib)
set(APP_GUI_TARGET_NAME ${PROJECT_NAMESPACE_LC}_${PROJECT_NAMESPACE_LC})
set(APP_GUI_ALIAS_NAME ${PROJECT_NAMESPACE})
set(APP_GUI_TARGET_NAME ${PROJECT_NAMESPACE_LC}_frontend_gui)
set(APP_GUI_ALIAS_NAME FrontendGui)
set(APP_CONSOLE_TARGET_NAME ${PROJECT_NAMESPACE_LC}_frontend_console)
set(APP_CONSOLE_ALIAS_NAME FrontendConsole)
add_subdirectory(app)

#--------------------Package Config-----------------------
Expand All @@ -122,6 +124,7 @@ ob_standard_project_package_config(
CONFIG STANDARD
TARGET_CONFIGS
TARGET "${PROJECT_NAMESPACE}::${APP_GUI_ALIAS_NAME}" COMPONENT "${APP_GUI_ALIAS_NAME}" DEFAULT
TARGET "${PROJECT_NAMESPACE}::${APP_CONSOLE_ALIAS_NAME}" COMPONENT "${APP_CONSOLE_ALIAS_NAME}"
)

#================= Install ==========================
Expand Down
29 changes: 29 additions & 0 deletions app/console/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#================= Common Build =========================

set(PRETTY_NAME "${PROJECT_NAME}-C")

# Add via ob standard executable
include(OB/Executable)
ob_add_standard_executable(${APP_CONSOLE_TARGET_NAME}
ALIAS "${APP_CONSOLE_ALIAS_NAME}"
OUTPUT_NAME "${PRETTY_NAME}"
SOURCE
frontend/console.h
frontend/console.cpp
frontend/input.h
frontend/input.cpp
frontend/progressprinter.h
frontend/progressprinter.cpp
main.cpp
LINKS
PRIVATE
CLIFp::FrontendFramework
magic_enum::magic_enum
CONFIG STANDARD
)

# Add exe details on Windows
if(CMAKE_SYSTEM_NAME STREQUAL Windows)
include(FrontendFramework)
set_clip_exe_details(${APP_CONSOLE_TARGET_NAME} ${PRETTY_NAME})
endif()
Binary file removed app/console/res/app/CLIFp.ico
Binary file not shown.
238 changes: 238 additions & 0 deletions app/console/src/frontend/console.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// Unit Include
#include "console.h"

// Qt Includes
#include <QGuiApplication>
#include <QFileInfo>
#include <QDir>

// Qx Includes
#include <qx/core/qx-iostream.h>
#include <qx/core/qx-string.h>

// Magic enum
#include "magic_enum.hpp"

#define ENUM_NAME(eenum) QString(magic_enum::enum_name(eenum).data())

/* NOTE: Unlike the GUI frontend, this one blocks fully when the user is prompted for input because
* the standard cin read methods block and of course don't spin the event loop internally like
* QMessageBox, QFileDialog, etc. do. Technically, this isn't a problem because whenever the driver
* thread prompts for input it also blocks, but if that ever changes then console input will have
* to be handled asynchronously using a technique like this: https://github.com/juangburgos/QConsoleListener
*/

//===============================================================================================================
// FrontendConsole
//===============================================================================================================

//-Constructor-------------------------------------------------------------------------------------------------------
//Public:
FrontendConsole::FrontendConsole(QGuiApplication* app) :
FrontendFramework(app)
{
// We don't make windows in this frontend, but just to be safe
app->setQuitOnLastWindowClosed(false);
}

//-Class Functions--------------------------------------------------------------------------------------------------------
//Private:

//-Instance Functions------------------------------------------------------------------------------------------------------
//Private:
void FrontendConsole::handleDirective(const DMessage& d)
{
/* TODO: Probably should add a heading like "Notice)" or something.
*
* TODO: Look into escape codes (console support per platform varies)
* to replicate bold/italic/underlined.
*
* Also, might want to replace the html tags with some kind of more
* complex message object or just some other way to better communicate
* the message in a frontend agnostic matter, so we could even have something
* like <u>Underlined Text</u> for GUI and:
*
* Underlined Text
* ---------------
*
* for console.
*/

// Replace possible HTML tags/entities with something more sensible for console
QString txt = Qx::String::mapArg(d.text, {
{u"<u>"_s, u""_s},
{u"</u>"_s, u""_s},
{u"<b>"_s, u"*"_s},
{u"</b>"_s, u"*"_s},
{u"<i>"_s, u"`"_s},
{u"</i>"_s, u"`"_s},
{u"<br>"_s, u"\n"_s},
{u"&lt;"_s, u"<"_s},
{u"&gt;"_s, u">"_s},
{u"&nbsp;"_s, u" "_s},
});
print(txt);
}

void FrontendConsole::handleDirective(const DError& d) { print(d.error);}

void FrontendConsole::handleDirective(const DProcedureStart& d)
{
mProgressPrinter.start(d.label);
}

void FrontendConsole::handleDirective(const DProcedureStop& d)
{
Q_UNUSED(d);
/* Always reset the dialog regardless of whether it is visible or not as it may not be currently visible,
* but queued to be visible on the next event loop iteration and therefore still needs to be hidden
* immediately after.
*/
mProgressPrinter.reset();
}

void FrontendConsole::handleDirective(const DProcedureProgress& d) { mProgressPrinter.setValue(d.current); }
void FrontendConsole::handleDirective(const DProcedureScale& d) { mProgressPrinter.setMaximum(d.max); }
void FrontendConsole::handleDirective(const DStatusUpdate& d) { print(d.heading + u"] "_s + d.message);}

// Sync directive handlers
void FrontendConsole::handleDirective(const DBlockingMessage& d)
{
// TODO: Probably should add a heading like "Notice)" or something
print(d.text);
}

// Request directive handlers
void FrontendConsole::handleDirective(const DBlockingError& d, DBlockingError::Choice* response)
{
Q_ASSERT(d.choices != DBlockingError::Choice::NoChoice);
bool validDef = d.choices.testFlag(d.defaultChoice);

// Print Error
print(d.error);
print();

// Print Prompt
print(validDef ? INSTR_CHOICE_SEL : INSTR_CHOICE_SEL_NO_DEF);

// Print Choices
QList<DBlockingError::Choice> choices;
for(auto c : magic_enum::enum_values<DBlockingError::Choice>())
{
if(!d.choices.testFlag(c))
continue;

choices.append(c);
QString cStr = TEMPL_OPTION.arg(choices.count()).arg(ENUM_NAME(c));
if(validDef && c == d.defaultChoice)
cStr += '*';
print(cStr);
}

// Get selection
auto sel = prompt<int>([c = choices.count()](int i){ return i > 0 && i <= c; });
*response = sel ? choices[*sel - 1] : d.defaultChoice;
}

void FrontendConsole::handleDirective(const DSaveFilename& d, QString* response)
{
// Print Prompt
QString cap = d.caption + (!d.extFilterDesc.isEmpty() ? u" ("_s + d.extFilterDesc + u")"_s : QString());
QString inst = INSTR_FILE_ENTER.arg(Qx::String::join(d.extFilter, u" "_s, u"*."_s));
print(cap);
print(inst);

// Get selection
auto path = prompt<QString>([ef = d.extFilter](const QString& i){
QFileInfo fi(i);
return ef.contains(fi.suffix());
});
*response = path ? *path : QString();
}

void FrontendConsole::handleDirective(const DExistingDir& d, QString* response)
{
// Print Prompt
print(d.caption);
print(INSTR_DIR_ENTER_EXIST);

// Get selection
auto path = prompt<QString>([](const QString& i){
QDir dir(i);
return dir.exists();
});
*response = path ? *path: QString();
}

void FrontendConsole::handleDirective(const DItemSelection& d, QString* response)
{
Q_ASSERT(!d.items.isEmpty());

// Print Prompt
print(d.caption);
print(d.label);
print(INSTR_ITEM_SEL);

// Print Choices
for(auto i = 0; const QString& itm : d.items)
print(TEMPL_OPTION.arg(++i).arg(itm));

// Get selection
auto sel = prompt<int>([c = d.items.count()](int i){ return i > 0 && i <= c; });
*response = sel ? d.items[*sel - 1] : d.items.first();
}

void FrontendConsole::handleDirective(const DYesOrNo& d, bool* response)
{
// Print Prompt
print(d.question);
print(INSTR_YES_OR_NO);

// Get selection
static QSet<QString> yes{u"yes"_s, u"y"_s};
static QSet<QString> no{u"no"_s, u"n"_s};
auto answer = prompt<QString>([](const QString& i){
QString lc = i.toLower();
return yes.contains(lc) || no.contains(lc);
});
*response = answer && yes.contains(answer->toLower());
}

template<typename P>
requires Qx::defines_left_shift_for<QTextStream, P>
void FrontendConsole::print(const P& p)
{
if(mProgressPrinter.checkAndResetEndlCtrl())
Qx::cout << "\n";

Qx::cout << p << Qt::endl;
}

template<Input::Type R, typename V>
requires Input::Validator<R, V>
std::optional<R> FrontendConsole::prompt(V v)
{
forever
{
QString line;
forever
{
QString str = Qx::cin.read(1);
QChar ch = str.isEmpty() ? QChar() : str[0];
if(ch.isNull() || ch == u'\x1B') // EOL or Esc
return std::nullopt;
else if(ch == u'\n')
break;
line += ch;
}

if(line.isEmpty())
return std::nullopt;

R convIn;
if(Input::to(line, convIn) && v(convIn))
return convIn;

print(INSTR_TRY_AGAIN);
}
}
69 changes: 69 additions & 0 deletions app/console/src/frontend/console.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#ifndef CONSOLE_H
#define CONSOLE_H

// Qx Includes
#include <qx/utility/qx-concepts.h>

// Project Includes
#include "frontend/framework.h"
#include "frontend/progressprinter.h"
#include "input.h"

class FrontendConsole final : public FrontendFramework
{
//-Class Variables--------------------------------------------------------------------------------------------------------
private:
// Instructions
static inline const QString INSTR_TRY_AGAIN = u"Invalid response, try again..."_s;
static inline const QString INSTR_CHOICE_SEL = u"Select Action (or ENTER for *):"_s;
static inline const QString INSTR_CHOICE_SEL_NO_DEF = u"Select Action:"_s;
static inline const QString INSTR_FILE_ENTER = u"File Path:"_s;
static inline const QString INSTR_DIR_ENTER_EXIST = u"Existing Directory:"_s;
static inline const QString INSTR_YES_OR_NO = u"Yes or No/ENTER:"_s;
static inline const QString INSTR_ITEM_SEL = u"Select Item (or ENTER for 1):"_s;

// Templates
static inline const QString TEMPL_OPTION = u"%1) %2"_s;

//-Instance Variables----------------------------------------------------------------------------------------------
private:
ProgressPrinter mProgressPrinter;

//-Constructor-------------------------------------------------------------------------------------------------------
public:
explicit FrontendConsole(QGuiApplication* app);



//-Instance Functions------------------------------------------------------------------------------------------------------
private:
// Async directive handlers
void handleDirective(const DMessage& d) override;
void handleDirective(const DError& d) override;
void handleDirective(const DProcedureStart& d) override;
void handleDirective(const DProcedureStop& d) override;
void handleDirective(const DProcedureProgress& d) override;
void handleDirective(const DProcedureScale& d) override;
void handleDirective(const DStatusUpdate& d) override;

// Sync directive handlers
void handleDirective(const DBlockingMessage& d) override;

// Request directive handlers
void handleDirective(const DBlockingError& d, DBlockingError::Choice* response) override;
void handleDirective(const DSaveFilename& d, QString* response) override;
void handleDirective(const DExistingDir& d, QString* response) override;
void handleDirective(const DItemSelection& d, QString* response) override;
void handleDirective(const DYesOrNo& d, bool* response) override;

// Derived
template<typename P = QString>
requires Qx::defines_left_shift_for<QTextStream, P>
void print(const P& p = QString());

template<Input::Type R, typename V>
requires Input::Validator<R, V>
std::optional<R> prompt(V v);
};

#endif // CONSOLE_H
Loading

0 comments on commit 81bd3cb

Please sign in to comment.