init project
This commit is contained in:
commit
821ea7eed0
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
build/
|
||||||
|
.cache/
|
46
CMakeLists.txt
Normal file
46
CMakeLists.txt
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Set the minimum required version of CMake
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
project(Cl3 VERSION 1.0)
|
||||||
|
|
||||||
|
# Set the C++ standard to C++20
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||||
|
|
||||||
|
# Enable "compile_commands.json" output for use with various tools
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# Add the Catch2 library
|
||||||
|
find_package(Catch2 REQUIRED)
|
||||||
|
find_package(yaml-cpp REQUIRED)
|
||||||
|
find_package(Lua 5.1 REQUIRED)
|
||||||
|
|
||||||
|
# Add all source files to a single target
|
||||||
|
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||||
|
list(FILTER SOURCES EXCLUDE REGEX ".*main.cpp$")
|
||||||
|
add_executable(cl ${SOURCES} src/main.cpp)
|
||||||
|
target_include_directories(cl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${LUA_INCLUDE_DIR})
|
||||||
|
target_link_libraries(cl PRIVATE yaml-cpp tmuxub ${LUA_LIBRARIES})
|
||||||
|
target_compile_options(cl PRIVATE -g)
|
||||||
|
|
||||||
|
# Add all test files to a single target
|
||||||
|
file(GLOB_RECURSE TEST_SOURCES "tests/*.cpp")
|
||||||
|
add_executable(test_cl ${TEST_SOURCES} ${SOURCES})
|
||||||
|
target_link_libraries(test_cl PRIVATE Catch2::Catch2WithMain yaml-cpp ${LUA_LIBRARIES})
|
||||||
|
target_include_directories(test_cl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CATCH_INCLUDE_DIRS} ${LUA_INCLUDE_DIR})
|
||||||
|
|
||||||
|
# Enable testing
|
||||||
|
enable_testing()
|
||||||
|
|
||||||
|
# Add the tests to the test suite
|
||||||
|
include(Catch)
|
||||||
|
catch_discover_tests(test_cl)
|
||||||
|
add_test(NAME TestCl COMMAND test_cl)
|
||||||
|
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
tmuxub
|
||||||
|
GIT_REPOSITORY https://gitlab.com/Onyad/tmuxub
|
||||||
|
GIT_TAG origin/main
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(tmuxub)
|
28
include/lua/loader.hpp
Normal file
28
include/lua/loader.hpp
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
#include <lua.hpp>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class LuaLoader {
|
||||||
|
public:
|
||||||
|
LuaLoader();
|
||||||
|
|
||||||
|
~LuaLoader();
|
||||||
|
|
||||||
|
void AddLuaPath(const std::string&);
|
||||||
|
void AddCPath(const std::string&);
|
||||||
|
void Call(const std::string& lua_module, const std::string& function);
|
||||||
|
void LoadToGlobal(const std::string&);
|
||||||
|
|
||||||
|
std::vector<std::string> GetListScripts(const std::string&) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Import(const std::string&, const std::string&, int);
|
||||||
|
void LoadImpl(const std::string&);
|
||||||
|
|
||||||
|
void ForEachByStringArray(std::function<void(const std::string&)>);
|
||||||
|
void ParseRequire();
|
||||||
|
void ParseImport();
|
||||||
|
|
||||||
|
lua_State* L;
|
||||||
|
};
|
32
include/projects/project.hpp
Normal file
32
include/projects/project.hpp
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class Project {
|
||||||
|
public:
|
||||||
|
Project(const std::string& path, const std::string& path_to_scripts, const std::string& name);
|
||||||
|
Project(const Project&) = delete;
|
||||||
|
Project(Project&&) = default;
|
||||||
|
|
||||||
|
const std::filesystem::path& GetPath() const;
|
||||||
|
const std::filesystem::path& GetPathToScripts() const;
|
||||||
|
const std::string& GetName() const;
|
||||||
|
bool IsSubDirectory(const std::filesystem::path&) const;
|
||||||
|
|
||||||
|
void SetPath(const std::string& path);
|
||||||
|
void SetPathToScripts(const std::string& path_to_scripts);
|
||||||
|
void SetName(const std::string& name);
|
||||||
|
|
||||||
|
void PrintInfo() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
std::filesystem::path path_to_scripts_;
|
||||||
|
std::string name_;
|
||||||
|
};
|
||||||
|
|
||||||
|
Project CreateProject();
|
||||||
|
Project CreateProject(const std::string& name);
|
24
include/projects/project_list.hpp
Normal file
24
include/projects/project_list.hpp
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <deque>
|
||||||
|
#include <string>
|
||||||
|
#include "projects/project.hpp"
|
||||||
|
|
||||||
|
class ProjectList {
|
||||||
|
public:
|
||||||
|
ProjectList();
|
||||||
|
|
||||||
|
const std::deque<Project>& GetProjects();
|
||||||
|
void AddProject(Project&&);
|
||||||
|
|
||||||
|
const Project& GetProject(
|
||||||
|
const std::filesystem::path& path = std::filesystem::current_path()) const;
|
||||||
|
void LoadFromYaml(const std::string& file_path);
|
||||||
|
void SaveToYaml(const std::string& file_path) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::deque<Project> projects_;
|
||||||
|
};
|
54
include/utils/command_parser.hpp
Normal file
54
include/utils/command_parser.hpp
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
enum class PatternType { FixedWord, AnyWord, AnyWords };
|
||||||
|
|
||||||
|
struct Pattern {
|
||||||
|
PatternType type;
|
||||||
|
std::string fixed_word;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Callback = std::variant<std::function<void(const std::vector<std::string>&)>,
|
||||||
|
std::function<bool(const std::vector<std::string>&)>>;
|
||||||
|
|
||||||
|
class Parser {
|
||||||
|
public:
|
||||||
|
void AddRule(const std::vector<Pattern>& pattern, const auto& callback,
|
||||||
|
bool find_in_suggestion = true) {
|
||||||
|
if constexpr (std::is_same_v<
|
||||||
|
decltype(callback(std::declval<const std::vector<std::string>&>())), void>) {
|
||||||
|
AddRuleImpl(pattern, std::function<void(const std::vector<std::string>&)>(callback),
|
||||||
|
find_in_suggestion);
|
||||||
|
} else {
|
||||||
|
AddRuleImpl(pattern, std::function<bool(const std::vector<std::string>&)>(callback),
|
||||||
|
find_in_suggestion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Parse(int argc, char* argv[]) const;
|
||||||
|
std::vector<std::string> GetPossibleAdditions(int argc, char* argv[]) const;
|
||||||
|
std::vector<std::string> GetPossibleAdditions(const std::vector<std::string>&) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void AddRuleImpl(const std::vector<Pattern>& pattern, const Callback& callback,
|
||||||
|
bool find_in_suggestion);
|
||||||
|
bool Match(const std::vector<Pattern>& pattern, const std::vector<std::string>& args) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Rule {
|
||||||
|
Rule(const std::vector<Pattern>& pattern, Callback callback, bool find_in_suggestion)
|
||||||
|
: pattern(pattern), callback(callback), find_in_suggestion(find_in_suggestion) {}
|
||||||
|
std::vector<Pattern> pattern;
|
||||||
|
Callback callback;
|
||||||
|
bool find_in_suggestion;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Rule> rules_;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Pattern> MakePattern(
|
||||||
|
std::initializer_list<std::variant<const char*, PatternType>> elements);
|
6
include/utils/generate_project_directory.hpp
Normal file
6
include/utils/generate_project_directory.hpp
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
std::string generate_project_directory(const std::string& project_name);
|
17
include/utils/local.hpp
Normal file
17
include/utils/local.hpp
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
inline std::filesystem::path GetLocalPath() {
|
||||||
|
const char* home_dir = getenv("HOME");
|
||||||
|
if (home_dir == nullptr) {
|
||||||
|
std::cerr << "Home directory was not declared" << std::endl;
|
||||||
|
throw std::runtime_error("Was not found home directory");
|
||||||
|
}
|
||||||
|
std::filesystem::path result = std::filesystem::path(home_dir) / ".local/share/cl3";
|
||||||
|
std::filesystem::create_directories(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
154
src/lua/loader.cpp
Normal file
154
src/lua/loader.cpp
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <lua/loader.hpp>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
LuaLoader::LuaLoader() : L(luaL_newstate()) {
|
||||||
|
luaL_openlibs(L);
|
||||||
|
AddLuaPath("./?.lua");
|
||||||
|
AddCPath("./?.so");
|
||||||
|
AddCPath("./?.dylib");
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::AddLuaPath(const std::string& new_path) {
|
||||||
|
lua_getglobal(L, "package");
|
||||||
|
lua_getfield(L, -1, "path");
|
||||||
|
lua_pushstring(L, (std::string(";") + new_path).data());
|
||||||
|
lua_concat(L, 2);
|
||||||
|
lua_setfield(L, -2, "path");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::AddCPath(const std::string& new_path) {
|
||||||
|
lua_getglobal(L, "package");
|
||||||
|
lua_getfield(L, -1, "cpath");
|
||||||
|
lua_pushstring(L, (std::string(";") + new_path).data());
|
||||||
|
lua_concat(L, 2);
|
||||||
|
lua_setfield(L, -2, "cpath");
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::LoadImpl(const std::string& name) {
|
||||||
|
lua_getglobal(L, "require");
|
||||||
|
lua_pushstring(L, name.data());
|
||||||
|
|
||||||
|
if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
|
||||||
|
std::cerr << "Error call script: " << name.data() << std::endl;
|
||||||
|
std::cerr << lua_tostring(L, -1) << std::endl;
|
||||||
|
throw std::logic_error("Error call script: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseRequire();
|
||||||
|
ParseImport();
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::LoadToGlobal(const std::string& name) {
|
||||||
|
LoadImpl(name);
|
||||||
|
lua_setglobal(L, name.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> LuaLoader::GetListScripts(const std::string& lua_module) const {
|
||||||
|
std::vector<std::string> result;
|
||||||
|
|
||||||
|
lua_getglobal(L, lua_module.data());
|
||||||
|
lua_pushnil(L);
|
||||||
|
|
||||||
|
while (lua_next(L, -2) != 0) {
|
||||||
|
if (!lua_isfunction(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
result.push_back(lua_tostring(L, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::ForEachByStringArray(std::function<void(const std::string&)> callback) {
|
||||||
|
if (lua_isnil(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lua_isstring(L, -1)) {
|
||||||
|
callback(lua_tostring(L, -1));
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return;
|
||||||
|
} else if (lua_istable(L, -1)) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
|
||||||
|
while (lua_next(L, -2) != 0) {
|
||||||
|
if (!lua_isstring(L, -1)) {
|
||||||
|
throw std::logic_error("Error in for each by string array");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(lua_tostring(L, -1));
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::logic_error("Error in parse requires ");
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::ParseRequire() {
|
||||||
|
if (!lua_istable(L, -1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_getfield(L, -1, "require");
|
||||||
|
|
||||||
|
ForEachByStringArray(
|
||||||
|
[this](const std::string& new_module_name) { LoadToGlobal(new_module_name); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::Import(const std::string& name, const std::string& field, int table) {
|
||||||
|
LoadImpl(name);
|
||||||
|
lua_getfield(L, -1, field.data());
|
||||||
|
lua_setfield(L, table, field.data());
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::ParseImport() {
|
||||||
|
if (!lua_istable(L, -1)) {
|
||||||
|
lua_pop(L, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_getfield(L, -1, "import");
|
||||||
|
|
||||||
|
ForEachByStringArray([this, table = lua_absindex(L, -2)](const std::string& new_module_name) {
|
||||||
|
int dot_position = new_module_name.find(".");
|
||||||
|
|
||||||
|
if (dot_position == std::string::npos) {
|
||||||
|
throw std::logic_error("Can't find dot in import");
|
||||||
|
}
|
||||||
|
|
||||||
|
Import(new_module_name.substr(0, dot_position), new_module_name.substr(dot_position + 1),
|
||||||
|
table);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void LuaLoader::Call(const std::string& lua_module, const std::string& function) {
|
||||||
|
lua_getglobal(L, lua_module.data());
|
||||||
|
lua_getfield(L, -1, function.data());
|
||||||
|
|
||||||
|
if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
|
||||||
|
std::cout << "Error in call " << lua_module << "." << function << std::endl;
|
||||||
|
std::cerr << lua_tostring(L, -1) << std::endl;
|
||||||
|
throw std::logic_error("Error in call " + lua_module + "." + function);
|
||||||
|
}
|
||||||
|
|
||||||
|
lua_pop(L, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
LuaLoader::~LuaLoader() {
|
||||||
|
lua_close(L);
|
||||||
|
}
|
142
src/main.cpp
Normal file
142
src/main.cpp
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include "utils/command_parser.hpp"
|
||||||
|
#include "projects/project_list.hpp"
|
||||||
|
#include "utils/local.hpp"
|
||||||
|
#include "lua/loader.hpp"
|
||||||
|
|
||||||
|
#include <tmuxub/tmuxub.hpp>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
void OpenFileInEditor(const std::filesystem::path& filename) {
|
||||||
|
const char* editor = getenv("EDITOR");
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
editor = "vim";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto str = filename.string();
|
||||||
|
|
||||||
|
std::vector<const char*> argv = {editor, str.data(), nullptr};
|
||||||
|
execvp(editor, const_cast<char* const*>(argv.data()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
ProjectList projects;
|
||||||
|
projects.LoadFromYaml(GetLocalPath() / "projects.yml");
|
||||||
|
|
||||||
|
LuaLoader lua;
|
||||||
|
lua.AddLuaPath((GetLocalPath() / "globals/") / "?.lua");
|
||||||
|
lua.AddCPath((GetLocalPath() / "globals/") / "?.so");
|
||||||
|
|
||||||
|
Parser parser;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const auto& project = projects.GetProject();
|
||||||
|
lua.AddLuaPath(project.GetPathToScripts() / "?.lua");
|
||||||
|
|
||||||
|
std::vector<fs::path> scripts;
|
||||||
|
for (auto entry : fs::directory_iterator(project.GetPathToScripts())) {
|
||||||
|
auto filename = entry.path().filename().string();
|
||||||
|
if (filename.ends_with(".lua")) {
|
||||||
|
scripts.push_back(filename.substr(0, filename.size() - 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto script_name : scripts) {
|
||||||
|
lua.LoadToGlobal(script_name);
|
||||||
|
|
||||||
|
if (script_name == "main") {
|
||||||
|
auto all_scripts = lua.GetListScripts("main");
|
||||||
|
|
||||||
|
for (auto& script : all_scripts) {
|
||||||
|
parser.AddRule(MakePattern({script.data()}), [script, &lua, &project](auto& args) {
|
||||||
|
fs::current_path(project.GetPath());
|
||||||
|
lua.Call("main", script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auto all_scripts = lua.GetListScripts(script_name);
|
||||||
|
|
||||||
|
for (auto& script : all_scripts) {
|
||||||
|
parser.AddRule(MakePattern({script_name.string().data(), script.data()}),
|
||||||
|
[script, &lua, &project, script_name](auto& args) {
|
||||||
|
std::filesystem::current_path(project.GetPath());
|
||||||
|
lua.Call(script_name, script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::find(scripts.begin(), scripts.end(), "main") != scripts.end()) {
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& project : projects.GetProjects()) {
|
||||||
|
parser.AddRule(MakePattern({".", "open", project.GetName().data()}), [&project](auto& args) {
|
||||||
|
tmuxub::Tmux t(GetLocalPath() / "tmuxsocket");
|
||||||
|
|
||||||
|
if (t.HasSession(project.GetName())) {
|
||||||
|
t.ConnectToSession(project.GetName());
|
||||||
|
t.Exec();
|
||||||
|
} else {
|
||||||
|
fs::current_path(project.GetPath());
|
||||||
|
t.CreateSession(project.GetName());
|
||||||
|
t.Exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "try open: " << project.GetName() << std::endl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({"help"}), [](auto& args) { std::cout << "It's help" << std::endl; });
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({".", "new"}),
|
||||||
|
[&projects](auto& args) { projects.AddProject(CreateProject()); });
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({".", "new", PatternType::AnyWord}),
|
||||||
|
[&projects](auto& args) { projects.AddProject(CreateProject(args[3])); });
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({"."}), [&projects](auto& args) {
|
||||||
|
const auto& project = projects.GetProject();
|
||||||
|
OpenFileInEditor((project.GetPathToScripts() / "main.lua").string());
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({".", "global", PatternType::AnyWord}), [&projects](auto& args) {
|
||||||
|
OpenFileInEditor(GetLocalPath() / "globals" / (args[3] + ".lua"));
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({".", "config"}), [&projects](auto& args) {
|
||||||
|
const auto& project = projects.GetProject();
|
||||||
|
OpenFileInEditor((project.GetPathToScripts() / "main.lua").string());
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({".", "config", PatternType::AnyWord}), [&projects](auto& args) {
|
||||||
|
const auto& project = projects.GetProject();
|
||||||
|
OpenFileInEditor(project.GetPathToScripts() / (args[3] + ".lua"));
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({".", "projects"}),
|
||||||
|
[](auto& args) { OpenFileInEditor(GetLocalPath() / "projects.yml"); });
|
||||||
|
|
||||||
|
parser.AddRule(MakePattern({PatternType::AnyWords}),
|
||||||
|
[](auto& args) { std::cout << "Unknown command" << std::endl; });
|
||||||
|
|
||||||
|
if (getenv("CL_SUGGESTION") != nullptr) {
|
||||||
|
auto suggestions = parser.GetPossibleAdditions(argc, argv);
|
||||||
|
int x = 0;
|
||||||
|
for (auto& suggestion : suggestions) {
|
||||||
|
x++;
|
||||||
|
if (x == 7) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::cout << suggestion << " ";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parser.Parse(argc, argv);
|
||||||
|
|
||||||
|
projects.SaveToYaml(GetLocalPath() / "projects.yml");
|
||||||
|
}
|
||||||
|
}
|
59
src/projects/project.cpp
Normal file
59
src/projects/project.cpp
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#include "projects/project.hpp"
|
||||||
|
#include "utils/local.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
Project::Project(const std::string& path, const std::string& path_to_scripts,
|
||||||
|
const std::string& name)
|
||||||
|
: path_(path), path_to_scripts_(path_to_scripts), name_(name) {}
|
||||||
|
|
||||||
|
const fs::path& Project::GetPath() const {
|
||||||
|
return path_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs::path& Project::GetPathToScripts() const {
|
||||||
|
return path_to_scripts_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Project::GetName() const {
|
||||||
|
return name_;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Project::IsSubDirectory(const fs::path& path) const {
|
||||||
|
return path.string().starts_with(path_.string());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Project::SetPath(const std::string& path) {
|
||||||
|
path_ = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Project::SetPathToScripts(const std::string& path_to_scripts) {
|
||||||
|
path_to_scripts_ = path_to_scripts;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Project::SetName(const std::string& name) {
|
||||||
|
name_ = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Project::PrintInfo() const {
|
||||||
|
std::cout << "Project Name: " << name_ << "\n";
|
||||||
|
std::cout << "Path: " << path_ << "\n";
|
||||||
|
std::cout << "Path to Scripts: " << path_to_scripts_ << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
Project CreateProject() {
|
||||||
|
fs::path current_path = fs::current_path();
|
||||||
|
return CreateProject(current_path.filename().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
Project CreateProject(const std::string& name) {
|
||||||
|
fs::path current_path = fs::current_path();
|
||||||
|
std::string path = current_path.string();
|
||||||
|
|
||||||
|
std::string path_to_scripts = GetLocalPath() / "projects" / name;
|
||||||
|
fs::create_directories(path_to_scripts);
|
||||||
|
|
||||||
|
return Project(path, path_to_scripts, name);
|
||||||
|
}
|
78
src/projects/project_list.cpp
Normal file
78
src/projects/project_list.cpp
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
#include <yaml-cpp/yaml.h>
|
||||||
|
|
||||||
|
#include "projects/project_list.hpp"
|
||||||
|
|
||||||
|
ProjectList::ProjectList() {}
|
||||||
|
|
||||||
|
const std::deque<Project>& ProjectList::GetProjects() {
|
||||||
|
return projects_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectList::AddProject(Project&& project) {
|
||||||
|
bool exists =
|
||||||
|
std::find_if(projects_.begin(), projects_.end(), [&project](const Project& another) {
|
||||||
|
return another.GetName() == project.GetName();
|
||||||
|
}) != projects_.end();
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
throw std::logic_error("Project with same name exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
projects_.push_back(std::move(project));
|
||||||
|
}
|
||||||
|
|
||||||
|
const Project& ProjectList::GetProject(const std::filesystem::path& path) const {
|
||||||
|
size_t index = 0, prefix = 0;
|
||||||
|
for (size_t i = 0; i < projects_.size(); ++i) {
|
||||||
|
const auto& project = projects_[i];
|
||||||
|
if (project.IsSubDirectory(path)) {
|
||||||
|
if (prefix < project.GetPath().string().size()) {
|
||||||
|
index = i;
|
||||||
|
prefix = project.GetPath().string().size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix == 0) {
|
||||||
|
throw std::logic_error("Project not exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects_[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectList::LoadFromYaml(const std::string& file_path) {
|
||||||
|
try {
|
||||||
|
YAML::Node yaml = YAML::LoadFile(file_path);
|
||||||
|
for (const auto& project : yaml["projects"]) {
|
||||||
|
std::string path = project["path"].as<std::string>();
|
||||||
|
std::string path_to_scripts = project["path_to_scripts"].as<std::string>();
|
||||||
|
std::string name = project["name"].as<std::string>();
|
||||||
|
projects_.emplace_back(path, path_to_scripts, name);
|
||||||
|
}
|
||||||
|
} catch (YAML::Exception& e) {
|
||||||
|
std::cerr << "Error loading YAML file: " << e.what() << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProjectList::SaveToYaml(const std::string& file_path) const {
|
||||||
|
YAML::Emitter yaml;
|
||||||
|
yaml << YAML::BeginMap;
|
||||||
|
yaml << YAML::Key << "projects";
|
||||||
|
yaml << YAML::Value << YAML::BeginSeq;
|
||||||
|
for (const auto& project : projects_) {
|
||||||
|
yaml << YAML::BeginMap;
|
||||||
|
yaml << YAML::Key << "path";
|
||||||
|
yaml << YAML::Value << project.GetPath();
|
||||||
|
yaml << YAML::Key << "path_to_scripts";
|
||||||
|
yaml << YAML::Value << project.GetPathToScripts();
|
||||||
|
yaml << YAML::Key << "name";
|
||||||
|
yaml << YAML::Value << project.GetName();
|
||||||
|
yaml << YAML::EndMap;
|
||||||
|
}
|
||||||
|
yaml << YAML::EndSeq;
|
||||||
|
yaml << YAML::EndMap;
|
||||||
|
|
||||||
|
std::ofstream output(file_path);
|
||||||
|
output << yaml.c_str();
|
||||||
|
output.close();
|
||||||
|
}
|
167
src/utils/command_parser.cpp
Normal file
167
src/utils/command_parser.cpp
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
#include "utils/command_parser.hpp"
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
void Parser::AddRuleImpl(const std::vector<Pattern>& pattern, const Callback& callback,
|
||||||
|
bool find_in_suggestion) {
|
||||||
|
rules_.emplace_back(pattern, callback, find_in_suggestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Parser::Parse(int argc, char* argv[]) const {
|
||||||
|
std::vector<std::string> args(argv, argv + argc);
|
||||||
|
|
||||||
|
for (const auto& [pattern, callback, _] : rules_) {
|
||||||
|
if (Match(pattern, args)) {
|
||||||
|
if (callback.index() == 0) {
|
||||||
|
std::get<0>(callback)(args);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (std::get<1>(callback)(args)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> Parser::GetPossibleAdditions(int argc, char* argv[]) const {
|
||||||
|
if (argc != 2) {
|
||||||
|
throw std::logic_error("argc should be equal 2");
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t last = 0;
|
||||||
|
std::string s = argv[1];
|
||||||
|
|
||||||
|
std::vector<std::string> input;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < s.length(); ++i) {
|
||||||
|
if (s[i] == ' ') {
|
||||||
|
input.push_back(s.substr(last, i - last));
|
||||||
|
last = i + 1;
|
||||||
|
} else if (i + 1 == s.length()) {
|
||||||
|
input.push_back(s.substr(last, i - last + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::erase_if(input, [](auto x) { return x.empty(); });
|
||||||
|
|
||||||
|
if (s.back() == ' ') {
|
||||||
|
input.emplace_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetPossibleAdditions(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::string> Parser::GetPossibleAdditions(const std::vector<std::string>& input) const {
|
||||||
|
std::vector<std::string> suggestions;
|
||||||
|
|
||||||
|
for (const auto& [pattern, callback, check] : rules_) {
|
||||||
|
if (!check) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
size_t arg_index = 1;
|
||||||
|
size_t pattern_index = 0;
|
||||||
|
bool continue_outer_loop = false;
|
||||||
|
|
||||||
|
while (arg_index <= input.size() && pattern_index < pattern.size()) {
|
||||||
|
switch (pattern[pattern_index].type) {
|
||||||
|
case PatternType::FixedWord:
|
||||||
|
if (arg_index == input.size()) {
|
||||||
|
suggestions.push_back(pattern[pattern_index].fixed_word);
|
||||||
|
continue_outer_loop = true;
|
||||||
|
}else if (arg_index + 1 == input.size() &&
|
||||||
|
pattern[pattern_index].fixed_word.starts_with(input[arg_index])) {
|
||||||
|
suggestions.push_back(pattern[pattern_index].fixed_word);
|
||||||
|
continue_outer_loop = true;
|
||||||
|
} else if (input[arg_index] == pattern[pattern_index].fixed_word) {
|
||||||
|
++arg_index;
|
||||||
|
++pattern_index;
|
||||||
|
} else {
|
||||||
|
continue_outer_loop = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PatternType::AnyWord:
|
||||||
|
++arg_index;
|
||||||
|
++pattern_index;
|
||||||
|
break;
|
||||||
|
case PatternType::AnyWords:
|
||||||
|
++pattern_index;
|
||||||
|
|
||||||
|
if (pattern_index == pattern.size()) {
|
||||||
|
arg_index = input.size();
|
||||||
|
} else {
|
||||||
|
while (arg_index <= input.size() &&
|
||||||
|
(arg_index == input.size() ||
|
||||||
|
input[arg_index] != pattern[pattern_index].fixed_word)) {
|
||||||
|
if (arg_index == input.size()) {
|
||||||
|
suggestions.push_back(pattern[pattern_index].fixed_word);
|
||||||
|
}
|
||||||
|
++arg_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (continue_outer_loop) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// std::sort(suggestions.begin(), suggestions.end());
|
||||||
|
// suggestions.erase(std::unique(suggestions.begin(), suggestions.end()), suggestions.end());
|
||||||
|
return suggestions;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Parser::Match(const std::vector<Pattern>& pattern,
|
||||||
|
const std::vector<std::string>& args) const {
|
||||||
|
size_t arg_index = 1;
|
||||||
|
size_t pattern_index = 0;
|
||||||
|
|
||||||
|
while (arg_index < args.size() && pattern_index < pattern.size()) {
|
||||||
|
switch (pattern[pattern_index].type) {
|
||||||
|
case PatternType::FixedWord:
|
||||||
|
if (args[arg_index] == pattern[pattern_index].fixed_word) {
|
||||||
|
++arg_index;
|
||||||
|
++pattern_index;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case PatternType::AnyWord:
|
||||||
|
++arg_index;
|
||||||
|
++pattern_index;
|
||||||
|
break;
|
||||||
|
case PatternType::AnyWords:
|
||||||
|
++pattern_index;
|
||||||
|
if (pattern_index == pattern.size()) {
|
||||||
|
arg_index = args.size();
|
||||||
|
} else {
|
||||||
|
while (arg_index < args.size() && args[arg_index] != pattern[pattern_index].fixed_word) {
|
||||||
|
++arg_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern_index < pattern.size() && pattern[pattern_index].type == PatternType::AnyWords) {
|
||||||
|
pattern_index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg_index == args.size() && pattern_index == pattern.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Pattern> MakePattern(
|
||||||
|
std::initializer_list<std::variant<const char*, PatternType>> elements) {
|
||||||
|
std::vector<Pattern> pattern;
|
||||||
|
|
||||||
|
for (const auto& element : elements) {
|
||||||
|
if (std::holds_alternative<const char*>(element)) {
|
||||||
|
pattern.push_back({PatternType::FixedWord, std::get<const char*>(element)});
|
||||||
|
} else {
|
||||||
|
pattern.push_back({std::get<PatternType>(element), ""});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pattern;
|
||||||
|
}
|
29
src/utils/gererate_project_directory.cpp
Normal file
29
src/utils/gererate_project_directory.cpp
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#include "utils/generate_project_directory.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
std::string generate_project_directory(const std::string& project_name) {
|
||||||
|
const char* home_dir = std::getenv("HOME");
|
||||||
|
if (!home_dir) {
|
||||||
|
throw std::runtime_error("Could not get HOME environment variable");
|
||||||
|
}
|
||||||
|
fs::path base_dir(home_dir);
|
||||||
|
|
||||||
|
fs::path app_subdir(".local/share/cl3/projets");
|
||||||
|
fs::path app_dir = base_dir / app_subdir;
|
||||||
|
|
||||||
|
if (!fs::exists(app_dir)) {
|
||||||
|
fs::create_directory(app_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::path new_dir_path = app_dir / project_name;
|
||||||
|
|
||||||
|
if (fs::exists(new_dir_path)) {
|
||||||
|
throw std::runtime_error("Project exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_directory(new_dir_path);
|
||||||
|
|
||||||
|
return new_dir_path.string();
|
||||||
|
}
|
104
tests/utils/command_parser.cpp
Normal file
104
tests/utils/command_parser.cpp
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
#include "utils/command_parser.hpp"
|
||||||
|
|
||||||
|
TEST_CASE("Parser tests", "[parser]") {
|
||||||
|
SECTION("Fixed word pattern") {
|
||||||
|
Parser parser;
|
||||||
|
bool callbackCalled = false;
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command"}),
|
||||||
|
[&callbackCalled](const std::vector<std::string>& args) { callbackCalled = true; });
|
||||||
|
|
||||||
|
const char* argv1[] = {"./program", "command"};
|
||||||
|
parser.Parse(2, const_cast<char**>(argv1));
|
||||||
|
REQUIRE(callbackCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Any word pattern") {
|
||||||
|
Parser parser;
|
||||||
|
bool callbackCalled = false;
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command", PatternType::AnyWord}),
|
||||||
|
[&callbackCalled](const std::vector<std::string>& args) { callbackCalled = true; });
|
||||||
|
|
||||||
|
const char* argv2[] = {"./program", "command", "any_word"};
|
||||||
|
parser.Parse(3, const_cast<char**>(argv2));
|
||||||
|
REQUIRE(callbackCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Any words pattern") {
|
||||||
|
Parser parser;
|
||||||
|
bool callbackCalled = false;
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command", PatternType::AnyWords}),
|
||||||
|
[&callbackCalled](const std::vector<std::string>& args) { callbackCalled = true; });
|
||||||
|
|
||||||
|
const char* argv3[] = {"./program", "command", "word1", "word2", "word3"};
|
||||||
|
parser.Parse(5, const_cast<char**>(argv3));
|
||||||
|
REQUIRE(callbackCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Mixed pattern") {
|
||||||
|
Parser parser;
|
||||||
|
bool callbackCalled = false;
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command", PatternType::AnyWord, "option", PatternType::AnyWords}),
|
||||||
|
[&callbackCalled](const std::vector<std::string>& args) { callbackCalled = true; });
|
||||||
|
|
||||||
|
const char* argv4[] = {"./program", "command", "any_word", "option", "word1", "word2", "word3"};
|
||||||
|
parser.Parse(7, const_cast<char**>(argv4));
|
||||||
|
REQUIRE(callbackCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Non-matching pattern") {
|
||||||
|
Parser parser;
|
||||||
|
bool callbackCalled = false;
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command", "option", PatternType::AnyWords}),
|
||||||
|
[&callbackCalled](const std::vector<std::string>& args) { callbackCalled = true; });
|
||||||
|
|
||||||
|
const char* argv5[] = {"./program", "other_command", "option", "word1", "word2", "word3"};
|
||||||
|
parser.Parse(6, const_cast<char**>(argv5));
|
||||||
|
REQUIRE(!callbackCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("Multiple rules with different matches") {
|
||||||
|
Parser parser;
|
||||||
|
bool callbackCalled1 = false;
|
||||||
|
bool callbackCalled2 = false;
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command1", PatternType::AnyWords}),
|
||||||
|
[&callbackCalled1](const std::vector<std::string>& args) { callbackCalled1 = true; });
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command2", PatternType::AnyWords}),
|
||||||
|
[&callbackCalled2](const std::vector<std::string>& args) { callbackCalled2 = true; });
|
||||||
|
|
||||||
|
const char* argv6[] = {"./program", "command1", "word1", "word2", "word3"};
|
||||||
|
parser.Parse(5, const_cast<char**>(argv6));
|
||||||
|
REQUIRE(callbackCalled1);
|
||||||
|
REQUIRE(!callbackCalled2);
|
||||||
|
const char* argv7[] = {"./program", "command2", "word1", "word2", "word3"};
|
||||||
|
parser.Parse(5, const_cast<char**>(argv7));
|
||||||
|
REQUIRE(callbackCalled2);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("No matching rule") {
|
||||||
|
Parser parser;
|
||||||
|
bool callbackCalled = false;
|
||||||
|
|
||||||
|
parser.AddRule(
|
||||||
|
MakePattern({"command", "option", PatternType::AnyWords}),
|
||||||
|
[&callbackCalled](const std::vector<std::string>& args) { callbackCalled = true; });
|
||||||
|
|
||||||
|
const char* argv8[] = {"./program", "other_command", "other_option", "word1", "word2", "word3"};
|
||||||
|
parser.Parse(6, const_cast<char**>(argv8));
|
||||||
|
REQUIRE(!callbackCalled);
|
||||||
|
}
|
||||||
|
}
|
8
zsh_autocomplete/_cl
Executable file
8
zsh_autocomplete/_cl
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
_cl() {
|
||||||
|
local comp=`CL_SUGGESTION="" ${words[1]} "$words"`
|
||||||
|
|
||||||
|
read -A line <<< $comp
|
||||||
|
compadd -o nosort -- $line
|
||||||
|
}
|
||||||
|
|
||||||
|
compdef _cl cl
|
Loading…
Reference in a new issue