add command executor.

main
bog 2023-10-04 11:38:14 +02:00
parent 83cf9af94e
commit 1d2394068d
15 changed files with 544 additions and 53 deletions

View File

@ -11,6 +11,10 @@ twq_lib = static_library('tiwiq',
sources: [ sources: [
'src/core/KeyMod.cpp', 'src/core/KeyMod.cpp',
'src/core/Shortcut.cpp', 'src/core/Shortcut.cpp',
'src/core/Command.cpp',
'src/core/Context.cpp',
'src/core/Binding.cpp',
'src/core/Executor.cpp',
], ],
dependencies: [ dependencies: [
dependency('ncursesw') dependency('ncursesw')
@ -34,6 +38,7 @@ executable('twq-tests',
sources: [ sources: [
'tests/main.cpp', 'tests/main.cpp',
'tests/Shortcut.cpp', 'tests/Shortcut.cpp',
'tests/Executor.cpp',
], ],
dependencies: [ dependencies: [
twq_dep, twq_dep,

View File

@ -25,7 +25,7 @@
} }
#define TWQ_ASSERT(COND, MSG) \ #define TWQ_ASSERT(COND, MSG) \
if (! COND) { \ if (! (COND) ) { \
std::cerr << MSG << std::endl; \ std::cerr << MSG << std::endl; \
abort(); \ abort(); \
} }

18
src/core/Binding.cpp Normal file
View File

@ -0,0 +1,18 @@
#include "Binding.hpp"
namespace twq
{
namespace core
{
/*explicit*/ Binding::Binding(std::shared_ptr<Shortcut> shortcut,
std::shared_ptr<Command> command)
: m_shortcut { shortcut }
, m_command { command }
{
}
/*virtual*/ Binding::~Binding()
{
}
}
}

28
src/core/Binding.hpp Normal file
View File

@ -0,0 +1,28 @@
#ifndef twq_core_BINDING_HPP
#define twq_core_BINDING_HPP
#include "Command.hpp"
#include "Shortcut.hpp"
namespace twq
{
namespace core
{
class Binding
{
public:
explicit Binding(std::shared_ptr<Shortcut> shortcut,
std::shared_ptr<Command> command);
virtual ~Binding();
std::weak_ptr<Shortcut> shortcut() const { return m_shortcut; }
std::weak_ptr<Command> command() const { return m_command; }
private:
std::shared_ptr<Shortcut> m_shortcut;
std::shared_ptr<Command> m_command;
};
}
}
#endif

15
src/core/Command.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "Command.hpp"
namespace twq
{
namespace core
{
/*explicit*/ Command::Command()
{
}
/*virtual*/ Command::~Command()
{
}
}
}

24
src/core/Command.hpp Normal file
View File

@ -0,0 +1,24 @@
#ifndef twq_core_COMMAND_HPP
#define twq_core_COMMAND_HPP
#include "Context.hpp"
namespace twq
{
namespace core
{
class Command
{
public:
explicit Command();
virtual ~Command();
virtual void execute(Context& context) = 0;
virtual void undo(Context& context) = 0;
private:
};
}
}
#endif

15
src/core/Context.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "Context.hpp"
namespace twq
{
namespace core
{
/*explicit*/ Context::Context()
{
}
/*virtual*/ Context::~Context()
{
}
}
}

19
src/core/Context.hpp Normal file
View File

@ -0,0 +1,19 @@
#ifndef twq_core_CONTEXT_HPP
#define twq_core_CONTEXT_HPP
namespace twq
{
namespace core
{
class Context
{
public:
explicit Context();
virtual ~Context();
private:
};
}
}
#endif

50
src/core/Executor.cpp Normal file
View File

@ -0,0 +1,50 @@
#include "Executor.hpp"
namespace twq
{
namespace core
{
/*explicit*/ Executor::Executor()
{
}
/*virtual*/ Executor::~Executor()
{
}
void Executor::register_binding(Binding& binding)
{
m_entries.push_back({binding, 0});
}
void Executor::update(Context& context, KeyMod const& keymod)
{
for (auto& entry: m_entries)
{
auto shortcut = entry.binding.shortcut().lock();
if (shortcut->get(entry.progression).equals(keymod))
{
entry.progression++;
}
else
{
entry.progression = 0;
}
if (entry.progression >=
shortcut->count())
{
entry.binding.command().lock()->execute(context);
for (auto& entry: m_entries)
{
entry.progression = 0;
}
return;
}
}
}
}
}

30
src/core/Executor.hpp Normal file
View File

@ -0,0 +1,30 @@
#ifndef twq_core_EXECUTOR_HPP
#define twq_core_EXECUTOR_HPP
#include "Binding.hpp"
namespace twq
{
namespace core
{
struct Entry {
Binding binding;
size_t progression = 0;
};
class Executor
{
public:
explicit Executor();
virtual ~Executor();
void register_binding(Binding& binding);
void update(Context& context, KeyMod const& keymod);
private:
std::vector<Entry> m_entries;
};
}
}
#endif

View File

@ -10,6 +10,64 @@ namespace twq
return KeyMod { key_type, mods, std::nullopt }; return KeyMod { key_type, mods, std::nullopt };
} }
/*explicit*/ KeyMod::KeyMod(std::string const& repr)
{
std::vector<ModType> mods;
if (repr.find("C-") != std::string::npos)
{
mods.push_back(MOD_LCTRL);
}
if (repr.find("A-") != std::string::npos)
{
mods.push_back(MOD_ALT);
}
ssize_t k = repr.size() - 1;
while (k >= 0 && std::isspace(repr[k]))
{
k--;
}
std::string value;
while (k >= 0
&& !std::isspace(repr[k])
&& repr[k] != '-')
{
value = repr[k] + value;
k--;
}
if (value.size() == 1)
{
m_key_type = KEY_TEXT;
m_mods = mods;
m_text = value.front();
return;
}
else
{
for (size_t i=0; i<KEY_COUNT; i++)
{
std::string val = KeyTypeStr[i] + strlen("KEY_");
if (val == value)
{
m_key_type = (KeyType) i;
m_mods = mods;
return;
}
}
throw invalid_keymod_error {"cannot convert '"
+ repr
+ "' to keymod."};
}
}
/*explicit*/ KeyMod::KeyMod(char text, /*explicit*/ KeyMod::KeyMod(char text,
std::vector<ModType> const& mods) std::vector<ModType> const& mods)
: m_key_type { KEY_TEXT } : m_key_type { KEY_TEXT }
@ -38,6 +96,36 @@ namespace twq
std::find(std::begin(m_mods), std::end(m_mods), mod_type); std::find(std::begin(m_mods), std::end(m_mods), mod_type);
} }
bool KeyMod::equals(KeyMod const& rhs) const
{
if (m_key_type != rhs.m_key_type)
{
return false;
}
if (m_key_type == KEY_TEXT
&& *m_text != *rhs.m_text)
{
return false;
}
if (m_mods.size() != rhs.m_mods.size())
{
return false;
}
for (size_t i=0; i<m_mods.size(); i++)
{
if (m_mods[i] != rhs.m_mods[i])
{
return false;
}
}
return true;
}
std::string KeyMod::string() const std::string KeyMod::string() const
{ {
std::stringstream ss; std::stringstream ss;

View File

@ -20,6 +20,8 @@ namespace twq
{ {
namespace core namespace core
{ {
TWQ_ERROR(invalid_keymod_error);
TWQ_ENUM(KeyType, KEY_TYPES); TWQ_ENUM(KeyType, KEY_TYPES);
TWQ_ENUM(ModType, MOD_TYPES); TWQ_ENUM(ModType, MOD_TYPES);
@ -29,8 +31,11 @@ namespace twq
static KeyMod key(KeyType key_type, static KeyMod key(KeyType key_type,
std::vector<ModType> const& mods={}); std::vector<ModType> const& mods={});
explicit KeyMod(std::string const& repr);
explicit KeyMod(char text, std::vector<ModType> const& mods={}); explicit KeyMod(char text, std::vector<ModType> const& mods={});
explicit KeyMod(KeyType key_type, explicit KeyMod(KeyType key_type,
std::vector<ModType> const& mods, std::vector<ModType> const& mods,
std::optional<char> text); std::optional<char> text);
@ -41,6 +46,8 @@ namespace twq
bool has_mod(ModType mod_type) const; bool has_mod(ModType mod_type) const;
bool equals(KeyMod const& rhs) const;
std::string string() const; std::string string() const;
virtual ~KeyMod(); virtual ~KeyMod();

View File

@ -1,4 +1,5 @@
#include "Shortcut.hpp" #include "Shortcut.hpp"
#include "src/commons.hpp"
#include "src/core/KeyMod.hpp" #include "src/core/KeyMod.hpp"
namespace twq namespace twq
@ -14,56 +15,6 @@ namespace twq
std::string buffer; std::string buffer;
std::vector<ModType> mods; std::vector<ModType> mods;
auto create_keymod = [&](){
if (buffer.find("C-") != std::string::npos)
{
mods.push_back(MOD_LCTRL);
}
if (buffer.find("A-") != std::string::npos)
{
mods.push_back(MOD_ALT);
}
ssize_t k = buffer.size() - 1;
while (k >= 0 && std::isspace(buffer[k]))
{
k--;
}
std::string value;
while (k >= 0
&& !std::isspace(buffer[k])
&& buffer[k] != '-')
{
value = buffer[k] + value;
k--;
}
if (value.size() == 1)
{
return KeyMod {KEY_TEXT, mods, value.front()};
}
else
{
for (size_t i=0; i<KEY_COUNT; i++)
{
std::string val = KeyTypeStr[i] + strlen("KEY_");
if (val == value)
{
return KeyMod {(KeyType) i, mods, std::nullopt};
}
}
throw invalid_shortcut_error {"cannot convert '"
+ repr
+ "' to shortcut."};
}
};
for (size_t i=0; i<repr.size(); i++) for (size_t i=0; i<repr.size(); i++)
{ {
char c = repr[i]; char c = repr[i];
@ -72,7 +23,16 @@ namespace twq
{ {
if (!buffer.empty()) if (!buffer.empty())
{ {
push(create_keymod()); try
{
push(KeyMod {buffer});
}
catch (invalid_keymod_error const& err)
{
throw invalid_shortcut_error {"cannot convert '"
+ repr
+ "' to shortcut"};
}
} }
mods.clear(); mods.clear();
@ -86,7 +46,16 @@ namespace twq
if (!buffer.empty()) if (!buffer.empty())
{ {
push(create_keymod()); try
{
push(KeyMod {buffer});
}
catch (invalid_keymod_error const& err)
{
throw invalid_shortcut_error {"cannot convert '"
+ repr
+ "' to shortcut"};
}
} }
} }
@ -94,6 +63,15 @@ namespace twq
{ {
} }
KeyMod Shortcut::get(size_t index) const
{
TWQ_ASSERT(index < count(), "cannot get shortcut at index '"
+ std::to_string(index)
+ "'");
return m_keymods[index];
}
void Shortcut::push(KeyMod const& keymod) void Shortcut::push(KeyMod const& keymod)
{ {
m_keymods.push_back(keymod); m_keymods.push_back(keymod);

View File

@ -16,6 +16,9 @@ namespace twq
explicit Shortcut(std::string const& repr); explicit Shortcut(std::string const& repr);
virtual ~Shortcut(); virtual ~Shortcut();
size_t count() const { return m_keymods.size(); }
KeyMod get(size_t index) const;
void push(KeyMod const& keymod); void push(KeyMod const& keymod);
std::string string() const; std::string string() const;

211
tests/Executor.cpp Normal file
View File

@ -0,0 +1,211 @@
#include <catch2/catch.hpp>
#include "../src/core/Executor.hpp"
using namespace twq::core;
class ExecutorTest
{
public:
explicit ExecutorTest() {}
virtual ~ExecutorTest() {}
protected:
};
struct CommandMock: public Command
{
int execute_count = 0;
void execute(Context&) override
{
execute_count++;
}
void undo(Context&) override
{
}
};
TEST_CASE_METHOD(ExecutorTest, "Executor_one_key")
{
Context ctx;
auto cmd = std::make_shared<CommandMock>();
Binding b {std::make_shared<Shortcut>("a"), cmd};
Executor exec;
exec.register_binding(b);
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(1 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(2 == cmd->execute_count);
}
TEST_CASE_METHOD(ExecutorTest, "Executor_two_same_keys")
{
Context ctx;
auto cmd = std::make_shared<CommandMock>();
Binding b {std::make_shared<Shortcut>("a a"), cmd};
Executor exec;
exec.register_binding(b);
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(1 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(1 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(2 == cmd->execute_count);
}
TEST_CASE_METHOD(ExecutorTest, "Executor_two_keys")
{
Context ctx;
auto cmd = std::make_shared<CommandMock>();
Binding b {std::make_shared<Shortcut>("a b"), cmd};
Executor exec;
exec.register_binding(b);
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(1 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(1 == cmd->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(2 == cmd->execute_count);
}
TEST_CASE_METHOD(ExecutorTest, "Executor_wrong_key")
{
Context ctx;
auto cmd = std::make_shared<CommandMock>();
Binding b {std::make_shared<Shortcut>("a b"), cmd};
Executor exec;
exec.register_binding(b);
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"c"});
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"a"});
REQUIRE(0 == cmd->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(1 == cmd->execute_count);
}
TEST_CASE_METHOD(ExecutorTest, "Executor_two_commands")
{
Context ctx;
auto cmd0 = std::make_shared<CommandMock>();
Binding b0 {std::make_shared<Shortcut>("C-a b"), cmd0};
auto cmd1 = std::make_shared<CommandMock>();
Binding b1 {std::make_shared<Shortcut>("C-a c"), cmd1};
Executor exec;
exec.register_binding(b0);
exec.register_binding(b1);
SECTION("execute first")
{
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"C-a"});
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(1 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
}
SECTION("execute second")
{
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"C-a"});
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"c"});
REQUIRE(0 == cmd0->execute_count);
REQUIRE(1 == cmd1->execute_count);
}
SECTION("fail first, try but fail second")
{
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"C-a"});
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"d"});
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
}
}
TEST_CASE_METHOD(ExecutorTest, "Executor_two_sequentials_commands")
{
Context ctx;
auto cmd0 = std::make_shared<CommandMock>();
Binding b0 {std::make_shared<Shortcut>("C-a b"), cmd0};
auto cmd1 = std::make_shared<CommandMock>();
Binding b1 {std::make_shared<Shortcut>("b c"), cmd1};
Executor exec;
exec.register_binding(b0);
exec.register_binding(b1);
SECTION("execute sequentially")
{
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"C-a"});
REQUIRE(0 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(1 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"c"});
REQUIRE(1 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"b"});
REQUIRE(1 == cmd0->execute_count);
REQUIRE(0 == cmd1->execute_count);
exec.update(ctx, KeyMod {"c"});
REQUIRE(1 == cmd0->execute_count);
REQUIRE(1 == cmd1->execute_count);
}
}