tom removed tabulate/table.hpp from headers 🙌

This commit can't be divided because everything is related.

The tabulate/table.hpp #include is too expensive to compile and it was
included everywhere and it also was exposed to the client code.

Whole code was refactored to move this header into the .cpp files.

The InteractsWithIO was also hidden in similar way, forward declared
in the TomApplication, used std::unique_ptr<>, and #include moved
to the .cpp file.

The InteractsWithIO is now private because it's fully initialized after
the TomApplication::run() method call and it would need more work.

The next change is in the InteractsWithIO::table() method, the values
passed to it aren't tabulate::Table::Row_t anymore (for header and body
rows), but are are own types. The reason for this was to get rid of
the Tabulate version number checks and based on these version checks
define the correct Tabulate row/cell types. Now, when this type will
change in the future it will not cause any problems.
These our new table row/cell values/types are internally converted
to the tabulate::Table::Row_t type.

The last change was to extract the formatting code for the Tabulate
table into the StatusCommand. These formatting code/rules were directly
defined in the InteractsWithIO::table() method and that was a bug.
I have added the FormatTableCallback that can be passed to this InteractsWithIO::table() method that allows to format Tabulate table.

 - updated all InteractsWithIO method calls through the io() getter
 - moved the Application::~Application() to the .cpp file (non-inline)
 - exposed the isAnsiW/Output() methods (made public)
 - removed the InteractsWithIO() constructor and
   initialize(const QCommandLineParser &) methods
 - moved the Tabulate table formatting logic to the StatusCommand
 - removed Tabulate version checks thx to our own types
This commit is contained in:
silverqx
2024-09-16 19:54:43 +02:00
parent 1d044bd8c6
commit 662d734335
7 changed files with 217 additions and 179 deletions
+9 -5
View File
@@ -15,7 +15,6 @@ TINY_SYSTEM_HEADER
#include "tom/config.hpp" // IWYU pragma: keep
#include "tom/concerns/guesscommandname.hpp"
#include "tom/concerns/interactswithio.hpp"
#include "tom/tomtypes.hpp"
#include "tom/types/commandlineoption.hpp"
@@ -39,6 +38,7 @@ namespace Commands
namespace Concerns
{
class CallsCommands;
class InteractsWithIO;
class PrintsOptions;
} // namespace Concerns
@@ -48,8 +48,7 @@ namespace Concerns
class Seeder;
/*! Tom application. */
class TINYORM_EXPORT Application : public Concerns::InteractsWithIO,
public Concerns::GuessCommandName
class TINYORM_EXPORT Application : public Concerns::GuessCommandName
{
Q_DISABLE_COPY_MOVE(Application)
@@ -67,7 +66,7 @@ namespace Concerns
friend Concerns::PrintsOptions;
// To access initializeParser() and createCommand()
friend Concerns::CallsCommands;
// To access createCommandsVector(), errorWall(), exitApplication()
// To access createCommandsVector(), io(), exitApplication()
friend Concerns::GuessCommandName;
/*! Alias for the ConnectionResolverInterface. */
@@ -83,7 +82,7 @@ namespace Concerns
std::vector<std::shared_ptr<Migration>> migrations = {},
std::vector<std::shared_ptr<Seeder>> seeders = {});
/*! Virtual destructor. */
~Application() override = default;
~Application() override;
/*! Instantiate/initialize all migration classes. */
template<typename ...Migrations>
@@ -250,6 +249,8 @@ namespace Concerns
/*! Get database connection resolver. */
std::shared_ptr<ConnectionResolverInterface> connectionResolver() const noexcept;
/*! Get the IO aka InteractsWithIO (methods for the console output/input). */
const Concerns::InteractsWithIO &io() const noexcept;
/*! Throw if no connection configuration is registered. */
void throwIfNoConnectionConfig() const;
@@ -302,6 +303,9 @@ namespace Concerns
/*! Application options (more info at the cpp file beginning). */
QList<CommandLineOption> m_options;
/*! IO aka InteractsWithIO instance (methods for the console output/input). */
std::unique_ptr<Concerns::InteractsWithIO> m_io;
/* Auto tests helpers */
#ifdef TINYTOM_TESTS_CODE
public:
@@ -27,7 +27,7 @@ namespace Commands::Migrations
{
Q_DISABLE_COPY_MOVE(StatusCommand)
/*! Alias for the tabulate row. */
/*! Alias for the table row. */
using TableRow = InteractsWithIO::TableRow;
public:
+24 -66
View File
@@ -7,30 +7,20 @@ TINY_SYSTEM_HEADER
#include <QStringList>
#include <tabulate/table.hpp>
/* This header exists from the tabulate v1.4.0, it has been added in the middle of v1.3.0
and didn't exist at the v1.3.0 release. */
#if __has_include(<tabulate/tabulate.hpp>)
# include <tabulate/tabulate.hpp>
#endif
#include <iostream>
#include <orm/macros/commonnamespace.hpp>
#include <orm/macros/export.hpp>
class QCommandLineParser;
namespace tabulate
{
class Table;
}
TINYORM_BEGIN_COMMON_NAMESPACE
// Defines to detect the tabulate version
#ifndef TABULATE_VERSION_MAJOR
# define TABULATE_VERSION_MAJOR 0
# define TABULATE_VERSION_MINOR 0
# define TABULATE_VERSION_PATCH 0
#endif
#define TINY_TABULATE_VERSION QT_VERSION_CHECK(TABULATE_VERSION_MAJOR, \
TABULATE_VERSION_MINOR, \
TABULATE_VERSION_PATCH)
namespace Tom
{
class Application;
@@ -47,29 +37,10 @@ namespace Concerns
// To access private constructor and errorWallInternal() (used by logException())
friend Tom::Application;
/*! Constructor (used by TomApplication::logException()). */
explicit InteractsWithIO(bool noAnsi);
public:
#if TINY_TABULATE_VERSION >= QT_VERSION_CHECK(1, 5, 0)
/*! Alias for the tabulate cell. */
using TableCell = tabulate::Table::Row_t::value_type;
#elif TINY_TABULATE_VERSION == QT_VERSION_CHECK(1, 4, 0)
/*! Alias for the tabulate cell. */
using TableCell = std::variant<std::string, const char *, tabulate::Table>;
#else
/*! Alias for the tabulate cell. */
using TableCell = std::variant<std::string, tabulate::Table>;
#endif
#if TINY_TABULATE_VERSION >= QT_VERSION_CHECK(1, 5, 0)
/*! Alias for the tabulate row. */
using TableRow = tabulate::Table::Row_t;
// Check the type because we have no control over this type
static_assert (std::is_same_v<TableRow, std::vector<TableCell>>,
"The InteractsWithIO::TableRow must be the std::vector<TableCell> type.");
#else
/*! Alias for the tabulate row. */
using TableRow = std::vector<TableCell>;
#endif
/*! Constructor. */
explicit InteractsWithIO(const QCommandLineParser &parser);
/*! Virtual destructor. */
@@ -153,9 +124,17 @@ namespace Concerns
const InteractsWithIO &newLineErr(quint16 count = 1,
Verbosity verbosity = Normal) const;
/*! Alias for the table cell. */
using TableCell = std::variant<std::string, const char *, std::string_view>;
/*! Alias for the table row. */
using TableRow = std::vector<TableCell>;
/*! Format the tabulate table callback type. */
using FormatTableCallback = std::function<void(tabulate::Table &,
const InteractsWithIO &)>;
/*! Format input to textual table. */
const InteractsWithIO &
table(const TableRow &header, const std::vector<TableRow> &rows,
const FormatTableCallback &formatCallback = nullptr,
Verbosity verbosity = Normal) const;
/*! Confirm a question with the user. */
@@ -165,6 +144,12 @@ namespace Concerns
static QString stripAnsiTags(QString string);
/* Getters / Setters */
/*! Should the given output use ANSI? (ANSI is disabled without TTY). */
bool isAnsiOutput(const std::ostream &cout = std::cout) const;
/*! Should the given output use ANSI? (ANSI is disabled without TTY),
wide version. */
bool isAnsiWOutput(const std::wostream &cout = std::wcout) const;
/*! Run the given callable with disabled ANSI output support. */
void withoutAnsi(const std::function<void()> &callback);
/*! Enable ANSI support. */
@@ -179,13 +164,6 @@ namespace Concerns
inline InteractsWithIO &setAnsi(bool value) noexcept;
protected:
/*! Default constructor (used by the TomApplication, instance is initialized
later in the TomApplication::parseCommandLine()). */
InteractsWithIO();
/*! Initialize instance like the second constructor do, allows to create
an instance in two steps. */
void initialize(const QCommandLineParser &parser);
/*! Get a current verbosity level. */
inline Verbosity verbosity() const noexcept;
/*! Is quiet verbosity level? */
@@ -200,9 +178,6 @@ namespace Concerns
inline bool isDebugVerbosity() const noexcept;
private:
/*! Constructor (used by TomApplication::logException()). */
explicit InteractsWithIO(bool noAnsi);
/*! Replace text tags with ANSI sequences. */
static QString parseOutput(QString string, bool isAnsi = true);
@@ -219,35 +194,18 @@ namespace Concerns
/*! Determine whether discard output by the current and the given verbosity. */
bool dontOutput(Verbosity verbosity) const;
/*! Should the given output use ANSI? (ANSI is disabled without TTY). */
bool isAnsiOutput(const std::ostream &cout = std::cout) const;
/*! Should the given output use ANSI? (ANSI is disabled without TTY),
wide version. */
bool isAnsiWOutput(const std::wostream &cout = std::wcout) const;
/*! Write a string as error output (red box with a white text). */
QString errorWallInternal(const QString &string) const;
/*! Compute a reserve value for the QStringList lines. */
static QStringList::size_type
computeReserveForErrorWall(const QStringList &splitted, int maxLineWidth);
/*! Alias for the tabulate color. */
using Color = tabulate::Color;
/*! Default tabulate table colors. */
struct TableColors
{
Color green = Color::green;
Color red = Color::red;
};
/*! Initialize tabulate table colors by supported ANSI. */
TableColors initializeTableColors() const;
/*! Is the input interactive? (don't ask any interactive question if false) */
bool m_interactive = true;
/*! Current application verbosity (defined by passed command-line options). */
Verbosity m_verbosity = Normal;
/*! Current application ANSI passed by command-line option (nullopt is auto). */
std::optional<bool> m_ansi = std::nullopt;
std::optional<bool> m_ansi;
/*! Describes features of the current terminal. */
std::unique_ptr<Terminal> m_terminal;
};
+30 -15
View File
@@ -50,6 +50,7 @@
#include "tom/commands/migrations/rollbackcommand.hpp"
#include "tom/commands/migrations/statuscommand.hpp"
#include "tom/commands/migrations/uninstallcommand.hpp"
#include "tom/concerns/interactswithio.hpp"
#include "tom/exceptions/runtimeerror.hpp"
#include "tom/migrationrepository.hpp"
#include "tom/migrator.hpp"
@@ -195,6 +196,7 @@ Application::Application(int &argc, char *argv[], std::shared_ptr<DatabaseManage
, m_seedersPath(initializePath(TINY_STRINGIFY(TINYTOM_SEEDERS_DIR)))
, m_migrations(std::move(migrations))
, m_seeders(std::move(seeders))
, m_io(nullptr) // Instantiated after the command-line is parsed
{
// Enable UTF-8 encoding and VT100 support
Terminal::initialize();
@@ -208,6 +210,9 @@ Application::Application(int &argc, char *argv[], std::shared_ptr<DatabaseManage
initializeParser(m_parser);
}
// Needed by a unique_ptr()
Application::~Application() = default;
int Application::run()
{
// Throw if no database connection configuration is registered
@@ -352,10 +357,10 @@ void Application::parseCommandLine()
initializeEnvironment();
/* Command-line arguments are parsed now, so the InteractsWithIO() base class can be
initialized. Nothing bad to divide it into two steps as output to the console
is not needed until here. */
Concerns::InteractsWithIO::initialize(m_parser);
/* Command-line arguments are parsed now, so the InteractsWithIO() class can be
instantiated. There's nothing wrong with being so late as output to the console
is not needed until now. */
m_io = std::make_unique<Concerns::InteractsWithIO>(m_parser);
if (m_parser.isSet(nointeraction))
m_interactive = false;
@@ -423,7 +428,7 @@ void Application::handleEmptyCommandName(const QString &name,
T_UNLIKELY
case ShowErrorWall:
errorWall(u"Command '%1' is not defined."_s.arg(name));
io().errorWall(u"Command '%1' is not defined."_s.arg(name));
exitApplication(EXIT_FAILURE);
@@ -448,23 +453,23 @@ void Application::showVersion() const
void Application::printVersion() const
{
note(u"TinyORM "_s, false);
io().note(u"TinyORM "_s, false);
info(TINYORM_VERSION_STR);
io().info(TINYORM_VERSION_STR);
}
void Application::printFullVersions() const
{
note(u"tom "_s, false);
info(TINYTOM_VERSION_STR);
io().note(u"tom "_s, false);
io().info(TINYTOM_VERSION_STR);
for (const auto versionsSubsection = createVersionsSubsection();
const auto &[subsectionName, abouts] : versionsSubsection
) {
// Subsection name is optional
if (subsectionName) {
newLine();
comment(*subsectionName);
io().newLine();
io().comment(*subsectionName);
}
/*! Alias for the std::map<QString, AboutValue>. */
@@ -473,14 +478,14 @@ void Application::printFullVersions() const
Q_ASSERT(std::holds_alternative<AboutItemsType>(abouts));
for (const auto &[name, about] : std::get<AboutItemsType>(abouts)) {
note(NOSPACE.arg(name).arg(SPACE), false);
info(about.value, false);
io().note(NOSPACE.arg(name).arg(SPACE), false);
io().info(about.value, false);
// Item components
if (const auto &components = about.components; components)
muted(SPACE + PARENTH_ONE.arg(components->join(COMMA)), false);
io().muted(SPACE + PARENTH_ONE.arg(components->join(COMMA)), false);
newLine();
io().newLine();
}
}
}
@@ -854,6 +859,16 @@ Application::connectionResolver() const noexcept
return std::dynamic_pointer_cast<ConnectionResolverInterface>(m_db);
}
const Concerns::InteractsWithIO &Application::io() const noexcept
{
/* This is our internal thing so the Q_ASSERT() is enough. I tried to make it public
because the InteractsWithIO() class contains useful methods, but it's not fully
ready until the TomApplication::run() method call and that's a problem.
And that's why I made it protected. 🫤 */
Q_ASSERT(m_io);
return *m_io;
}
void Application::throwIfNoConnectionConfig() const
{
// Nothing to do, some database connection configuration/s are already registered
@@ -2,6 +2,8 @@
#include <QCommandLineParser>
#include <tabulate/table.hpp>
#include <orm/db.hpp>
#include "tom/migrationrepository.hpp"
@@ -9,6 +11,12 @@
#include <range/v3/view/remove_if.hpp>
/* This header exists from the tabulate v1.4.0, it has been added in the middle of v1.3.0
and doesn't exist at the v1.3.0 release. */
#if __has_include(<tabulate/tabulate.hpp>)
# include <tabulate/tabulate.hpp>
#endif
#ifdef TINYTOM_TESTS_CODE
# include <range/v3/algorithm/transform.hpp>
# include <range/v3/view/move.hpp>
@@ -46,6 +54,67 @@ QList<CommandLineOption> StatusCommand::optionsSignature() const
};
}
namespace
{
/*! Alias for the tabulate color. */
using Color = tabulate::Color;
/*! Default tabulate table colors. */
struct TableColors
{
Color green = Color::green;
Color red = Color::red;
};
/*! Initialize tabulate table colors by supported ANSI. */
inline TableColors initializeTableColors(const bool isAnsiOutput)
{
/* Even if I detect ANSI support as true, tabulate has its own detection logic,
it only checks isatty(), it has some consequences, eg. no colors when output
is redirected and --ansi was passed to the tom application, practically
all logic in the isAnsiOutput() will be skipped because of this internal
tabulate logic, not a big deal though. */
if (isAnsiOutput)
return {}; // Use default colors
// Disable ANSI colors if eg. --no-ansi (or not supported)
return {Color::none, Color::none};
}
/*! Callback function to format the tabulate table. */
void formatStatusTable(tabulate::Table &table, const Concerns::InteractsWithIO &io)
{
// Initialize tabulate table colors by supported ANSI
const auto [green, red] = initializeTableColors(io.isAnsiOutput());
// Format table
// thead - green text for all columns
table.row(0).format().font_color(green);
// tbody
for (std::size_t i = 1; i < table.size() ; ++i) {
auto &row = table.row(i);
// Remove all lines between rows in the tbody (leave only the first one)
if (i > 1)
row.format().hide_border_top();
// Ran? column : Yes - green, No - red
{
auto &cell0 = row.cell(0);
auto &format = cell0.format();
if (cell0.get_text() == "Yes")
format.font_color(green);
else
format.font_color(red);
}
// Align the Batch column to the right (must be after the hide_border_top())
row.cell(2).format().font_align(tabulate::FontAlign::right);
}
}
} // namespace
int StatusCommand::run()
{
Command::run();
@@ -74,7 +143,7 @@ int StatusCommand::run()
m_status = statusForUnitTest(std::move(migrations));
else
#endif
table({"Ran?", "Migration", "Batch"}, migrations);
table({"Ran?", "Migration", "Batch"}, migrations, formatStatusTable);
return EXIT_SUCCESS;
}
@@ -117,6 +186,21 @@ StatusCommand::getStatusFor(const QList<QVariant> &ran,
}
#ifdef TINYTOM_TESTS_CODE
// Prepare the tabulate version
#ifndef TABULATE_VERSION_MAJOR
# define TABULATE_VERSION_MAJOR 0
#endif
#ifndef TABULATE_VERSION_MINOR
# define TABULATE_VERSION_MINOR 0
#endif
#ifndef TABULATE_VERSION_PATCH
# define TABULATE_VERSION_PATCH 0
#endif
/*! Tabulate version suitable for the QT_VERSION_CHECK comparison. */
#define TINY_TABULATE_VERSION QT_VERSION_CHECK(TABULATE_VERSION_MAJOR, \
TABULATE_VERSION_MINOR, \
TABULATE_VERSION_PATCH)
namespace
{
#if TINY_TABULATE_VERSION >= QT_VERSION_CHECK(1, 3, 0)
+1 -1
View File
@@ -97,7 +97,7 @@ void GuessCommandName::printAmbiguousCommands(
})
| ranges::to<QStringList>();
application().errorWall(
application().io().errorWall(
u"Command \"%1\" is ambiguous.\n\nDid you mean one of these?\n%2"_s
.arg(commandName, formattedCommands.join(NEWLINE)));
+67 -90
View File
@@ -2,9 +2,11 @@
#include <QCommandLineParser>
#include <cmath>
#include <range/v3/range/conversion.hpp>
#include <range/v3/view/transform.hpp>
#include <tabulate/table.hpp>
#include <orm/constants.hpp>
#include <orm/utils/string.hpp>
#include "tom/terminal.hpp"
@@ -31,6 +33,13 @@ using Tom::Constants::quiet;
namespace Tom::Concerns
{
/* private */
InteractsWithIO::InteractsWithIO(const bool noAnsi)
: m_ansi(initializeNoAnsi(noAnsi))
, m_terminal(std::make_unique<Terminal>())
{}
/* public */
InteractsWithIO::InteractsWithIO(const QCommandLineParser &parser)
@@ -43,28 +52,6 @@ InteractsWithIO::InteractsWithIO(const QCommandLineParser &parser)
// Needed by a unique_ptr()
InteractsWithIO::~InteractsWithIO() = default;
/* protected */
// Needed by a unique_ptr()
InteractsWithIO::InteractsWithIO()
: m_terminal(nullptr)
{}
void InteractsWithIO::initialize(const QCommandLineParser &parser)
{
m_interactive = !parser.isSet(nointeraction);
m_verbosity = initializeVerbosity(parser);
m_ansi = initializeAnsi(parser);
m_terminal = std::make_unique<Terminal>();
}
/* private */
InteractsWithIO::InteractsWithIO(const bool noAnsi)
: m_ansi(initializeNoAnsi(noAnsi))
, m_terminal(std::make_unique<Terminal>())
{}
/* public */
const InteractsWithIO &
@@ -270,9 +257,38 @@ InteractsWithIO::newLineErr(const quint16 count, const Verbosity verbosity) cons
return *this;
}
namespace
{
/*! Alias for the tabulate cell. */
using TabulateCell = Table::Row_t::value_type;
/*! Alias for the tabulate row. */
using TabulateRow = Table::Row_t;
/*! Convert our table row to a tabulate table row. */
TabulateRow convertToTabulateRow(const InteractsWithIO::TableRow &row)
{
return row
| ranges::views::transform([](const auto &cell) -> TabulateCell
{
if (std::holds_alternative<const char *>(cell))
return std::get<const char *>(cell);
if (std::holds_alternative<std::string>(cell))
return std::get<std::string>(cell);
if (std::holds_alternative<std::string_view>(cell))
return std::string(std::get<std::string_view>(cell));
Q_UNREACHABLE();
})
| ranges::to<TabulateRow>();
}
} // namespace
const InteractsWithIO &
InteractsWithIO::table(const TableRow &header, const std::vector<TableRow> &rows,
const Verbosity verbosity) const
InteractsWithIO::table(
const TableRow &header, const std::vector<TableRow> &rows,
const FormatTableCallback &formatCallback, const Verbosity verbosity) const
{
if (dontOutput(verbosity))
return *this;
@@ -288,40 +304,15 @@ InteractsWithIO::table(const TableRow &header, const std::vector<TableRow> &rows
#endif
// thead
table.add_row(header);
table.add_row(convertToTabulateRow(header));
// tbody
for (const auto &row : rows)
table.add_row(row);
table.add_row(convertToTabulateRow(row));
// Initialize tabulate table colors by supported ANSI
const auto [green, red] = initializeTableColors();
// Format table
// Green text in header
table.row(0).format().font_color(green);
for (std::size_t i = 1; i <= rows.size() ; ++i) {
auto &row = table.row(i);
// Remove line between rows in the tbody
if (i > 1)
row.format().hide_border_top();
// Align the Batch column to the right (must be after the hide_border_top())
row.cell(2).format().font_align(tabulate::FontAlign::right);
// Ran? column : Yes - green, No - red
{
auto &cell0 = row.cell(0);
auto &format = cell0.format();
if (cell0.get_text() == "Yes")
format.font_color(green);
else
format.font_color(red);
}
}
// Format the tabulate table using the given callback
if (formatCallback)
std::invoke(formatCallback, table, *this);
std::cout << table << std::endl; // NOLINT(performance-avoid-endl)
@@ -392,6 +383,26 @@ QString InteractsWithIO::stripAnsiTags(QString string)
/* Getters / Setters */
bool InteractsWithIO::isAnsiOutput(const std::ostream &cout) const
{
// ANSI was explicitly set on the command-line, respect it
if (m_ansi)
return *m_ansi;
// Instead autodetect
return m_terminal->hasColorSupport(cout);
}
bool InteractsWithIO::isAnsiWOutput(const std::wostream &wcout) const
{
// ANSI was explicitly set on the command-line, respect it
if (m_ansi)
return *m_ansi;
// Instead autodetect
return m_terminal->hasWColorSupport(wcout);
}
void InteractsWithIO::withoutAnsi(const std::function<void()> &callback)
{
// Nothing to do, ANSI is already disabled
@@ -505,26 +516,6 @@ bool InteractsWithIO::dontOutput(const Verbosity verbosity) const
return verbosity > m_verbosity;
}
bool InteractsWithIO::isAnsiOutput(const std::ostream &cout) const
{
// ANSI was explicitly set on the command-line, respect it
if (m_ansi)
return *m_ansi;
// Instead autodetect
return m_terminal->hasColorSupport(cout);
}
bool InteractsWithIO::isAnsiWOutput(const std::wostream &wcout) const
{
// ANSI was explicitly set on the command-line, respect it
if (m_ansi)
return *m_ansi;
// Instead autodetect
return m_terminal->hasWColorSupport(wcout);
}
namespace
{
/*! Get max. line size after the split with the newline in all rendered lines. */
@@ -616,20 +607,6 @@ InteractsWithIO::computeReserveForErrorWall(const QStringList &splitted,
return size;
}
InteractsWithIO::TableColors InteractsWithIO::initializeTableColors() const
{
/* Even is I detect ANSI support as true, tabulate has it's own detection logic,
it only check isatty(), it has some consequences, eg. no colors when output
is redirected and --ansi was passed to the tom application, practically all the
logic in the isAnsiOutput() will be skipped because of this tabulate internal
logic, not a big deal though. */
if (isAnsiOutput())
return {};
// Disable coloring if no-ansi
return {Color::none, Color::none};
}
} // namespace Tom::Concerns
TINYORM_END_COMMON_NAMESPACE