diff --git a/src/json_ui.cc b/src/json_ui.cc new file mode 100644 index 00000000..67b21bda --- /dev/null +++ b/src/json_ui.cc @@ -0,0 +1,407 @@ +#include "json_ui.hh" + +#include "display_buffer.hh" +#include "keys.hh" +#include "file.hh" +#include "event_manager.hh" +#include "value.hh" +#include "unit_tests.hh" + +#include + +namespace Kakoune +{ + +template +String to_json(ArrayView array) +{ + String res; + for (auto& elem : array) + { + if (not res.empty()) + res += ", "; + res += to_json(elem); + } + return "[" + res + "]"; +} + +template +String to_json(const Vector& vec) { return to_json(ArrayView{vec}); } + +String to_json(int i) { return to_string(i); } +String to_json(StringView s) { return format(R"("{}")", escape(s, "\"", '\\')); } + +String to_json(Color color) +{ + if (color.color == Kakoune::Color::RGB) + { + char buffer[10]; + sprintf(buffer, R"("#%02x%02x%02x")", color.r, color.g, color.b); + return buffer; + } + return to_json(color_to_str(color)); +} + +String to_json(Attribute attributes) +{ + struct { Attribute attr; StringView name; } + attrs[] { + { Attribute::Exclusive, "exclusive" }, + { Attribute::Underline, "underline" }, + { Attribute::Reverse, "reverse" }, + { Attribute::Blink, "blink" }, + { Attribute::Bold, "bold" }, + { Attribute::Dim, "dim" }, + { Attribute::Italic, "italic" }, + }; + + String res; + for (auto& attr : attrs) + { + if (not (attributes & attr.attr)) + continue; + + if (not res.empty()) + res += ", "; + res += to_json(attr.name); + } + return "[" + res + "]"; +} + +String to_json(Face face) +{ + return format(R"(\{ "fg": {}, "bg": {}, "attributes": {} })", + to_json(face.fg), to_json(face.bg), to_json(face.attributes)); +} + +String to_json(const DisplayAtom& atom) +{ + return format(R"(\{ "face": {}, "contents": {} })", to_json(atom.face), to_json(atom.content())); +} + +String to_json(const DisplayLine& line) +{ + return to_json(line.atoms()); +} + +String to_json(CharCoord coord) +{ + return format(R"(\{ "line": {}, "column": {} })", coord.line, coord.column); +} + +String to_json(MenuStyle style) +{ + switch (style) + { + case MenuStyle::Prompt: return R"("prompt")"; + case MenuStyle::Inline: return R"("inline")"; + } + return ""; +} + +String to_json(InfoStyle style) +{ + switch (style) + { + case InfoStyle::Prompt: return R"("prompt")"; + case InfoStyle::Inline: return R"("inline")"; + case InfoStyle::InlineAbove: return R"("inlineAbove")"; + case InfoStyle::InlineBelow: return R"("inlineBelow")"; + case InfoStyle::MenuDoc: return R"("menuDoc")"; + } + return ""; +} + +String concat() +{ + return ""; +} + +template +String concat(First&& first, Args&&... args) +{ + if (sizeof...(Args) != 0) + return to_json(first) + ", " + concat(args...); + return to_json(first); +} + +template +void rpc_call(StringView method, Args&&... args) +{ + auto q = format(R"(\{ "jsonrpc": "2.0", "method": "{}", "params": [{}] }{})", + method, concat(std::forward(args)...), "\n"); + + write_stdout(q); +} + +JsonUI::JsonUI() + : m_stdin_watcher{0, [this](FDWatcher&, EventMode mode) { + parse_requests(mode); + }}, m_dimensions{24, 80} +{ + set_signal_handler(SIGINT, SIG_DFL); +} + +void JsonUI::draw(const DisplayBuffer& display_buffer, + const Face& default_face) +{ + rpc_call("draw", display_buffer.lines(), default_face); +} + +void JsonUI::draw_status(const DisplayLine& status_line, + const DisplayLine& mode_line, + const Face& default_face) +{ + rpc_call("draw_status", status_line, mode_line, default_face); +} + +bool JsonUI::is_key_available() +{ + return not m_pending_keys.empty(); +} + +Key JsonUI::get_key() +{ + kak_assert(not m_pending_keys.empty()); + Key key = m_pending_keys.front(); + m_pending_keys.erase(m_pending_keys.begin()); + return key; +} + +void JsonUI::menu_show(ConstArrayView items, + CharCoord anchor, Face fg, Face bg, + MenuStyle style) +{ + rpc_call("menu_show", items, anchor, fg, bg, style); +} + +void JsonUI::menu_select(int selected) +{ + rpc_call("menu_show", selected); +} + +void JsonUI::menu_hide() +{ + rpc_call("menu_hide"); +} + +void JsonUI::info_show(StringView title, StringView content, + CharCoord anchor, Face face, + InfoStyle style) +{ + rpc_call("info_show", title, content, anchor, face, style); +} + +void JsonUI::info_hide() +{ + rpc_call("info_hide"); +} + +void JsonUI::refresh() +{ + rpc_call("refresh"); +} + +void JsonUI::set_input_callback(InputCallback callback) +{ + m_input_callback = std::move(callback); +} + +void JsonUI::set_ui_options(const Options& options) +{ + // rpc_call("set_ui_options", options); +} + +CharCoord JsonUI::dimensions() +{ + return m_dimensions; +} + +using JsonArray = Vector; +using JsonObject = IdMap; + +static bool is_digit(char c) { return c >= '0' and c <= '9'; } + +std::tuple +parse_json(const char* pos, const char* end) +{ + using Result = std::tuple; + + if (not skip_while(pos, end, is_blank)) + return {}; + + if (is_digit(*pos)) + { + auto digit_end = pos; + skip_while(digit_end, end, is_digit); + return Result{ Value{str_to_int({pos, end})}, digit_end }; + } + if (*pos == '"') + { + String value; + bool escaped = false; + ++pos; + for (auto string_end = pos; string_end != end; ++string_end) + { + if (escaped) + { + escaped = false; + value += StringView{pos, string_end}; + value.back() = *string_end; + pos = string_end+1; + continue; + } + if (*string_end == '\\') + escaped = true; + if (*string_end == '"') + { + value += StringView{pos, string_end}; + return Result{std::move(value), string_end+1}; + } + } + return {}; + } + if (*pos == '[') + { + JsonArray array; + ++pos; + while (true) + { + Value element; + std::tie(element, pos) = parse_json(pos, end); + if (not element) + return {}; + array.push_back(std::move(element)); + if (not skip_while(pos, end, is_blank)) + return {}; + + if (*pos == ',') + { + ++pos; + continue; + } + if (*pos == ']') + return Result{std::move(array), pos+1}; + else + throw runtime_error("unable to parse array, expected , or ]"); + } + } + if (*pos == '{') + { + JsonObject object; + ++pos; + while (true) + { + Value name_value; + std::tie(name_value, pos) = parse_json(pos, end); + if (not name_value) + return {}; + + String& name = name_value.as(); + if (not skip_while(pos, end, is_blank)) + return {}; + if (*pos++ != ':') + throw runtime_error("expected :"); + + Value element; + std::tie(element, pos) = parse_json(pos, end); + if (not element) + return {}; + object.append({ std::move(name), std::move(element) }); + if (not skip_while(pos, end, is_blank)) + return {}; + + if (*pos == ',') + { + ++pos; + continue; + } + if (*pos == '}') + return Result{std::move(object), pos+1}; + else + throw runtime_error("unable to parse object, expected , or }"); + } + } + return {}; +} + +void JsonUI::eval_json(const Value& json) +{ + const JsonObject& object = json.as(); + auto json_it = object.find("jsonrpc"); + if (json_it == object.end() or json_it->value.as() != "2.0") + throw runtime_error("invalid json rpc request"); + + auto method_it = object.find("method"); + if (method_it == object.end()) + throw runtime_error("invalid json rpc request (method missing)"); + StringView method = method_it->value.as(); + + auto params_it = object.find("params"); + if (params_it == object.end()) + throw runtime_error("invalid json rpc request (params missing)"); + const JsonArray& params = params_it->value.as(); + + if (method == "keys") + { + for (auto& key_val : params) + { + for (auto& key : parse_keys(key_val.as())) + m_pending_keys.push_back(key); + } + } + else + throw runtime_error("unknown method"); +} + +static bool stdin_ready() +{ + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(0, &rfds); + + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 0; + + return select(1, &rfds, nullptr, nullptr, &tv) == 1; +} + +void JsonUI::parse_requests(EventMode mode) +{ + constexpr size_t bufsize = 1024; + char buf[bufsize]; + while (stdin_ready()) + { + ssize_t size = read(0, buf, bufsize); + if (size == -1 or size == 0) + break; + + m_incoming_text += StringView{buf, buf + size}; + } + + if (not m_incoming_text.empty()) + { + Value json; + const char* pos; + std::tie(json, pos) = parse_json(m_incoming_text.begin(), m_incoming_text.end()); + if (json) + { + eval_json(json); + m_incoming_text = String{pos, m_incoming_text.end()}; + } + } + + while (not m_pending_keys.empty()) + m_input_callback(mode); +} + +UnitTest test_json_parser{[]() +{ + StringView json = R"({ "jsonrpc": "2.0", "method": "keys", "params": [ "b", "l", "a", "h" ] })"; + auto value = std::get<0>(parse_json(json.begin(), json.end())); + kak_assert(value); +}}; + + +} diff --git a/src/json_ui.hh b/src/json_ui.hh new file mode 100644 index 00000000..7e2af6d0 --- /dev/null +++ b/src/json_ui.hh @@ -0,0 +1,63 @@ +#ifndef json_ui_hh_INCLUDED +#define json_ui_hh_INCLUDED + +#include "user_interface.hh" +#include "event_manager.hh" +#include "coord.hh" + +namespace Kakoune +{ + +class Value; + +class JsonUI : public UserInterface +{ +public: + JsonUI(); + + JsonUI(const JsonUI&) = delete; + JsonUI& operator=(const JsonUI&) = delete; + + void draw(const DisplayBuffer& display_buffer, + const Face& default_face) override; + + void draw_status(const DisplayLine& status_line, + const DisplayLine& mode_line, + const Face& default_face) override; + + bool is_key_available() override; + Key get_key() override; + + void menu_show(ConstArrayView items, + CharCoord anchor, Face fg, Face bg, + MenuStyle style) override; + void menu_select(int selected) override; + void menu_hide() override; + + void info_show(StringView title, StringView content, + CharCoord anchor, Face face, + InfoStyle style) override; + void info_hide() override; + + void refresh() override; + + void set_input_callback(InputCallback callback) override; + + void set_ui_options(const Options& options) override; + + CharCoord dimensions() override; + +private: + void parse_requests(EventMode mode); + void eval_json(const Value& value); + + InputCallback m_input_callback; + FDWatcher m_stdin_watcher; + Vector m_pending_keys; + CharCoord m_dimensions; + String m_incoming_text; +}; + +} + +#endif // json_ui_hh_INCLUDED diff --git a/src/main.cc b/src/main.cc index 11f6c11d..3f1cd10b 100644 --- a/src/main.cc +++ b/src/main.cc @@ -14,6 +14,7 @@ #include "insert_completer.hh" #include "shared_string.hh" #include "ncurses_ui.hh" +#include "json_ui.hh" #include "parameters_parser.hh" #include "register_manager.hh" #include "remote.hh" @@ -406,13 +407,18 @@ void signal_handler(int signal) abort(); } -int run_client(StringView session, StringView init_command) +int run_client(StringView session, StringView init_command, bool json_ui = false) { try { EventManager event_manager; - RemoteClient client{session, make_unique(), - get_env_vars(), init_command}; + std::unique_ptr ui; + if (json_ui) + ui = make_unique(); + else + ui = make_unique(); + + RemoteClient client{session, std::move(ui), get_env_vars(), init_command}; while (true) event_manager.handle_next_events(EventMode::Normal); } @@ -700,6 +706,7 @@ int main(int argc, char* argv[]) { "f", { true, "act as a filter, executing given keys on given files" } }, { "q", { false, "in filter mode, be quiet about errors applying keys" } }, { "u", { false, "use a dummy user interface, for testing purposes" } }, + { "j", { false, "use a jsonrpc user interface, only available in client mode" } }, { "l", { false, "list existing sessions" } } } }; try @@ -754,7 +761,7 @@ int main(int argc, char* argv[]) for (auto name : parser) new_files += format("edit '{}';", escape(real_path(name), "'", '\\')); - return run_client(*server_session, new_files + init_command); + return run_client(*server_session, new_files + init_command, (bool)parser.get_switch("j")); } else {