|
|
@@ -0,0 +1,301 @@
|
|
|
+#include "lumacs/keybinding.hpp"
|
|
|
+#include <sstream>
|
|
|
+#include <algorithm>
|
|
|
+#include <cctype>
|
|
|
+
|
|
|
+namespace lumacs {
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// Key Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+Key::Key(const std::string& key_name) {
|
|
|
+ *this = parse(key_name);
|
|
|
+}
|
|
|
+
|
|
|
+Key Key::parse(const std::string& key_str) {
|
|
|
+ Key key;
|
|
|
+
|
|
|
+ if (key_str.empty()) {
|
|
|
+ return key;
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string remaining = key_str;
|
|
|
+
|
|
|
+ // Parse modifiers
|
|
|
+ while (true) {
|
|
|
+ if (remaining.length() >= 2 && remaining.substr(0, 2) == "C-") {
|
|
|
+ key.ctrl = true;
|
|
|
+ remaining = remaining.substr(2);
|
|
|
+ } else if (remaining.length() >= 2 && (remaining.substr(0, 2) == "M-" || remaining.substr(0, 2) == "A-")) {
|
|
|
+ key.meta = true;
|
|
|
+ remaining = remaining.substr(2);
|
|
|
+ } else if (remaining.length() >= 2 && remaining.substr(0, 2) == "S-") {
|
|
|
+ key.shift = true;
|
|
|
+ remaining = remaining.substr(2);
|
|
|
+ } else {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // The rest is the key name
|
|
|
+ key.name = remaining;
|
|
|
+
|
|
|
+ return key;
|
|
|
+}
|
|
|
+
|
|
|
+std::string Key::to_string() const {
|
|
|
+ std::string result;
|
|
|
+
|
|
|
+ if (ctrl) result += "C-";
|
|
|
+ if (meta) result += "M-";
|
|
|
+ if (shift) result += "S-";
|
|
|
+
|
|
|
+ result += name;
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+bool Key::operator==(const Key& other) const {
|
|
|
+ return name == other.name &&
|
|
|
+ ctrl == other.ctrl &&
|
|
|
+ meta == other.meta &&
|
|
|
+ shift == other.shift;
|
|
|
+}
|
|
|
+
|
|
|
+bool Key::operator<(const Key& other) const {
|
|
|
+ if (name != other.name) return name < other.name;
|
|
|
+ if (ctrl != other.ctrl) return ctrl < other.ctrl;
|
|
|
+ if (meta != other.meta) return meta < other.meta;
|
|
|
+ return shift < other.shift;
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// KeySequence Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+KeySequence::KeySequence(const std::vector<Key>& keys) : keys_(keys) {
|
|
|
+}
|
|
|
+
|
|
|
+KeySequence::KeySequence(const std::string& key_sequence_str) {
|
|
|
+ if (key_sequence_str.empty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Split by spaces, but handle quoted strings
|
|
|
+ std::istringstream iss(key_sequence_str);
|
|
|
+ std::string token;
|
|
|
+
|
|
|
+ while (iss >> token) {
|
|
|
+ keys_.emplace_back(Key::parse(token));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void KeySequence::add_key(const Key& key) {
|
|
|
+ keys_.push_back(key);
|
|
|
+}
|
|
|
+
|
|
|
+void KeySequence::add_key(const std::string& key_str) {
|
|
|
+ keys_.emplace_back(Key::parse(key_str));
|
|
|
+}
|
|
|
+
|
|
|
+bool KeySequence::is_prefix_of(const KeySequence& other) const {
|
|
|
+ if (keys_.size() > other.keys_.size()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return std::equal(keys_.begin(), keys_.end(), other.keys_.begin());
|
|
|
+}
|
|
|
+
|
|
|
+bool KeySequence::has_prefix(const KeySequence& prefix) const {
|
|
|
+ return prefix.is_prefix_of(*this);
|
|
|
+}
|
|
|
+
|
|
|
+std::string KeySequence::to_string() const {
|
|
|
+ if (keys_.empty()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string result = keys_[0].to_string();
|
|
|
+
|
|
|
+ for (size_t i = 1; i < keys_.size(); ++i) {
|
|
|
+ result += " " + keys_[i].to_string();
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+void KeySequence::clear() {
|
|
|
+ keys_.clear();
|
|
|
+}
|
|
|
+
|
|
|
+KeySequence KeySequence::subsequence(size_t start_index) const {
|
|
|
+ if (start_index >= keys_.size()) {
|
|
|
+ return KeySequence();
|
|
|
+ }
|
|
|
+
|
|
|
+ std::vector<Key> sub_keys(keys_.begin() + start_index, keys_.end());
|
|
|
+ return KeySequence(sub_keys);
|
|
|
+}
|
|
|
+
|
|
|
+bool KeySequence::operator==(const KeySequence& other) const {
|
|
|
+ return keys_ == other.keys_;
|
|
|
+}
|
|
|
+
|
|
|
+bool KeySequence::operator<(const KeySequence& other) const {
|
|
|
+ return keys_ < other.keys_;
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// KeyBinding Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+KeyBinding::KeyBinding(const KeySequence& seq, KeyBindingFunction func, std::string desc)
|
|
|
+ : sequence(seq), function(std::move(func)), description(std::move(desc)) {
|
|
|
+}
|
|
|
+
|
|
|
+KeyBinding::KeyBinding(const std::string& key_str, KeyBindingFunction func, std::string desc)
|
|
|
+ : sequence(key_str), function(std::move(func)), description(std::move(desc)) {
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================================
|
|
|
+// KeyBindingManager Implementation
|
|
|
+// ============================================================================
|
|
|
+
|
|
|
+KeyBindingManager::KeyBindingManager()
|
|
|
+ : sequence_timeout_(std::chrono::milliseconds(1000)) {
|
|
|
+}
|
|
|
+
|
|
|
+void KeyBindingManager::bind(const KeySequence& sequence, KeyBindingFunction function, const std::string& description) {
|
|
|
+ if (sequence.empty() || !function) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ bindings_[sequence] = KeyBinding(sequence, std::move(function), description);
|
|
|
+}
|
|
|
+
|
|
|
+void KeyBindingManager::bind(const std::string& key_str, KeyBindingFunction function, const std::string& description) {
|
|
|
+ bind(KeySequence(key_str), std::move(function), description);
|
|
|
+}
|
|
|
+
|
|
|
+void KeyBindingManager::unbind(const KeySequence& sequence) {
|
|
|
+ bindings_.erase(sequence);
|
|
|
+}
|
|
|
+
|
|
|
+void KeyBindingManager::unbind(const std::string& key_str) {
|
|
|
+ unbind(KeySequence(key_str));
|
|
|
+}
|
|
|
+
|
|
|
+KeyResult KeyBindingManager::process_key(const Key& key) {
|
|
|
+ // Check for timeout first
|
|
|
+ if (is_building_sequence() && has_sequence_timed_out()) {
|
|
|
+ clear_partial_sequence();
|
|
|
+ // Fall through to process this key as a new sequence
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add this key to the current sequence
|
|
|
+ current_sequence_.add_key(key);
|
|
|
+
|
|
|
+ // Update timestamp
|
|
|
+ sequence_start_time_ = std::chrono::steady_clock::now();
|
|
|
+
|
|
|
+ // Check for exact binding
|
|
|
+ auto exact_binding = find_exact_binding(current_sequence_);
|
|
|
+ if (exact_binding.has_value()) {
|
|
|
+ // Found exact match
|
|
|
+ clear_partial_sequence();
|
|
|
+
|
|
|
+ try {
|
|
|
+ bool success = exact_binding->function();
|
|
|
+ return success ? KeyResult::Executed : KeyResult::Failed;
|
|
|
+ } catch (...) {
|
|
|
+ return KeyResult::Failed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check if this could be a prefix for other bindings
|
|
|
+ if (has_prefix_bindings_impl(current_sequence_)) {
|
|
|
+ return KeyResult::Partial;
|
|
|
+ }
|
|
|
+
|
|
|
+ // No binding found, clear sequence and return unbound
|
|
|
+ clear_partial_sequence();
|
|
|
+ return KeyResult::Unbound;
|
|
|
+}
|
|
|
+
|
|
|
+KeyResult KeyBindingManager::process_key(const std::string& key_str) {
|
|
|
+ return process_key(Key::parse(key_str));
|
|
|
+}
|
|
|
+
|
|
|
+bool KeyBindingManager::has_binding(const KeySequence& sequence) const {
|
|
|
+ return has_exact_binding(sequence) || has_prefix_bindings(sequence);
|
|
|
+}
|
|
|
+
|
|
|
+bool KeyBindingManager::has_exact_binding(const KeySequence& sequence) const {
|
|
|
+ return bindings_.find(sequence) != bindings_.end();
|
|
|
+}
|
|
|
+
|
|
|
+bool KeyBindingManager::has_prefix_bindings(const KeySequence& sequence) const {
|
|
|
+ return has_prefix_bindings_impl(sequence);
|
|
|
+}
|
|
|
+
|
|
|
+void KeyBindingManager::clear_partial_sequence() {
|
|
|
+ current_sequence_.clear();
|
|
|
+ sequence_start_time_ = std::chrono::steady_clock::time_point{};
|
|
|
+}
|
|
|
+
|
|
|
+bool KeyBindingManager::is_building_sequence() const {
|
|
|
+ return !current_sequence_.empty();
|
|
|
+}
|
|
|
+
|
|
|
+std::string KeyBindingManager::current_sequence_display() const {
|
|
|
+ if (current_sequence_.empty()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ return current_sequence_.to_string() + "-";
|
|
|
+}
|
|
|
+
|
|
|
+std::vector<KeyBinding> KeyBindingManager::get_all_bindings() const {
|
|
|
+ std::vector<KeyBinding> result;
|
|
|
+ result.reserve(bindings_.size());
|
|
|
+
|
|
|
+ for (const auto& [sequence, binding] : bindings_) {
|
|
|
+ result.push_back(binding);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
+void KeyBindingManager::set_sequence_timeout(std::chrono::milliseconds timeout) {
|
|
|
+ sequence_timeout_ = timeout;
|
|
|
+}
|
|
|
+
|
|
|
+bool KeyBindingManager::has_sequence_timed_out() const {
|
|
|
+ if (!is_building_sequence()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto now = std::chrono::steady_clock::now();
|
|
|
+ return (now - sequence_start_time_) > sequence_timeout_;
|
|
|
+}
|
|
|
+
|
|
|
+std::optional<KeyBinding> KeyBindingManager::find_exact_binding(const KeySequence& sequence) const {
|
|
|
+ auto it = bindings_.find(sequence);
|
|
|
+ if (it != bindings_.end()) {
|
|
|
+ return it->second;
|
|
|
+ }
|
|
|
+ return std::nullopt;
|
|
|
+}
|
|
|
+
|
|
|
+bool KeyBindingManager::has_prefix_bindings_impl(const KeySequence& sequence) const {
|
|
|
+ // Check if any registered binding has this sequence as a prefix
|
|
|
+ for (const auto& [binding_sequence, binding] : bindings_) {
|
|
|
+ if (sequence.is_prefix_of(binding_sequence) && sequence != binding_sequence) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace lumacs
|