Initial, WIP json ui implementation
The JsonUI writes json rpc user interface commands on stdout and reads json rpc requests on stdin.
This commit is contained in:
parent
52525a156f
commit
433940485f
407
src/json_ui.cc
Normal file
407
src/json_ui.cc
Normal file
|
@ -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 <utility>
|
||||||
|
|
||||||
|
namespace Kakoune
|
||||||
|
{
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
String to_json(ArrayView<const T> array)
|
||||||
|
{
|
||||||
|
String res;
|
||||||
|
for (auto& elem : array)
|
||||||
|
{
|
||||||
|
if (not res.empty())
|
||||||
|
res += ", ";
|
||||||
|
res += to_json(elem);
|
||||||
|
}
|
||||||
|
return "[" + res + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T, MemoryDomain D>
|
||||||
|
String to_json(const Vector<T, D>& vec) { return to_json(ArrayView<const T>{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<typename First, typename... Args>
|
||||||
|
String concat(First&& first, Args&&... args)
|
||||||
|
{
|
||||||
|
if (sizeof...(Args) != 0)
|
||||||
|
return to_json(first) + ", " + concat(args...);
|
||||||
|
return to_json(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
void rpc_call(StringView method, Args&&... args)
|
||||||
|
{
|
||||||
|
auto q = format(R"(\{ "jsonrpc": "2.0", "method": "{}", "params": [{}] }{})",
|
||||||
|
method, concat(std::forward<Args>(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<DisplayLine> 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<Value>;
|
||||||
|
using JsonObject = IdMap<Value>;
|
||||||
|
|
||||||
|
static bool is_digit(char c) { return c >= '0' and c <= '9'; }
|
||||||
|
|
||||||
|
std::tuple<Value, const char*>
|
||||||
|
parse_json(const char* pos, const char* end)
|
||||||
|
{
|
||||||
|
using Result = std::tuple<Value, const char*>;
|
||||||
|
|
||||||
|
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<String>();
|
||||||
|
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<JsonObject>();
|
||||||
|
auto json_it = object.find("jsonrpc");
|
||||||
|
if (json_it == object.end() or json_it->value.as<String>() != "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<String>();
|
||||||
|
|
||||||
|
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<JsonArray>();
|
||||||
|
|
||||||
|
if (method == "keys")
|
||||||
|
{
|
||||||
|
for (auto& key_val : params)
|
||||||
|
{
|
||||||
|
for (auto& key : parse_keys(key_val.as<String>()))
|
||||||
|
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);
|
||||||
|
}};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
63
src/json_ui.hh
Normal file
63
src/json_ui.hh
Normal file
|
@ -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<DisplayLine> 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<Key> m_pending_keys;
|
||||||
|
CharCoord m_dimensions;
|
||||||
|
String m_incoming_text;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // json_ui_hh_INCLUDED
|
15
src/main.cc
15
src/main.cc
|
@ -14,6 +14,7 @@
|
||||||
#include "insert_completer.hh"
|
#include "insert_completer.hh"
|
||||||
#include "shared_string.hh"
|
#include "shared_string.hh"
|
||||||
#include "ncurses_ui.hh"
|
#include "ncurses_ui.hh"
|
||||||
|
#include "json_ui.hh"
|
||||||
#include "parameters_parser.hh"
|
#include "parameters_parser.hh"
|
||||||
#include "register_manager.hh"
|
#include "register_manager.hh"
|
||||||
#include "remote.hh"
|
#include "remote.hh"
|
||||||
|
@ -406,13 +407,18 @@ void signal_handler(int signal)
|
||||||
abort();
|
abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
int run_client(StringView session, StringView init_command)
|
int run_client(StringView session, StringView init_command, bool json_ui = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
EventManager event_manager;
|
EventManager event_manager;
|
||||||
RemoteClient client{session, make_unique<NCursesUI>(),
|
std::unique_ptr<UserInterface> ui;
|
||||||
get_env_vars(), init_command};
|
if (json_ui)
|
||||||
|
ui = make_unique<JsonUI>();
|
||||||
|
else
|
||||||
|
ui = make_unique<NCursesUI>();
|
||||||
|
|
||||||
|
RemoteClient client{session, std::move(ui), get_env_vars(), init_command};
|
||||||
while (true)
|
while (true)
|
||||||
event_manager.handle_next_events(EventMode::Normal);
|
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" } },
|
{ "f", { true, "act as a filter, executing given keys on given files" } },
|
||||||
{ "q", { false, "in filter mode, be quiet about errors applying keys" } },
|
{ "q", { false, "in filter mode, be quiet about errors applying keys" } },
|
||||||
{ "u", { false, "use a dummy user interface, for testing purposes" } },
|
{ "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" } } }
|
{ "l", { false, "list existing sessions" } } }
|
||||||
};
|
};
|
||||||
try
|
try
|
||||||
|
@ -754,7 +761,7 @@ int main(int argc, char* argv[])
|
||||||
for (auto name : parser)
|
for (auto name : parser)
|
||||||
new_files += format("edit '{}';", escape(real_path(name), "'", '\\'));
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue
Block a user