Separate events between normal and urgent ones
Run urgent ones while executing %sh blocks. Fixes #236
This commit is contained in:
parent
0272da65c0
commit
49931fbf05
|
@ -105,7 +105,10 @@ Buffer* create_fifo_buffer(String name, int fd, bool scroll)
|
||||||
ValueId fifo_watcher_id = s_fifo_watcher_id;
|
ValueId fifo_watcher_id = s_fifo_watcher_id;
|
||||||
|
|
||||||
std::unique_ptr<FDWatcher, decltype(watcher_deleter)> watcher(
|
std::unique_ptr<FDWatcher, decltype(watcher_deleter)> watcher(
|
||||||
new FDWatcher(fd, [buffer, scroll, fifo_watcher_id](FDWatcher& watcher) {
|
new FDWatcher(fd, [buffer, scroll, fifo_watcher_id](FDWatcher& watcher, EventMode mode) {
|
||||||
|
if (mode != EventMode::Normal)
|
||||||
|
return;
|
||||||
|
|
||||||
constexpr size_t buffer_size = 2048;
|
constexpr size_t buffer_size = 2048;
|
||||||
// if we read data slower than it arrives in the fifo, limiting the
|
// if we read data slower than it arrives in the fifo, limiting the
|
||||||
// iteration number allows us to go back go back to the event loop and
|
// iteration number allows us to go back go back to the event loop and
|
||||||
|
|
|
@ -7,8 +7,12 @@
|
||||||
#include "file.hh"
|
#include "file.hh"
|
||||||
#include "remote.hh"
|
#include "remote.hh"
|
||||||
#include "client_manager.hh"
|
#include "client_manager.hh"
|
||||||
|
#include "event_manager.hh"
|
||||||
#include "window.hh"
|
#include "window.hh"
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
namespace Kakoune
|
namespace Kakoune
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -34,14 +38,44 @@ Client::~Client()
|
||||||
m_window->options().unregister_watcher(*this);
|
m_window->options().unregister_watcher(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::handle_available_input()
|
void Client::handle_available_input(EventMode mode)
|
||||||
{
|
{
|
||||||
while (m_ui->is_key_available())
|
if (mode == EventMode::Normal)
|
||||||
{
|
{
|
||||||
m_input_handler.handle_key(m_ui->get_key());
|
try
|
||||||
m_input_handler.clear_mode_trash();
|
{
|
||||||
|
for (auto& key : m_pending_keys)
|
||||||
|
{
|
||||||
|
m_input_handler.handle_key(key);
|
||||||
|
m_input_handler.clear_mode_trash();
|
||||||
|
}
|
||||||
|
m_pending_keys.clear();
|
||||||
|
|
||||||
|
while (m_ui->is_key_available())
|
||||||
|
{
|
||||||
|
m_input_handler.handle_key(m_ui->get_key());
|
||||||
|
m_input_handler.clear_mode_trash();
|
||||||
|
}
|
||||||
|
context().window().forget_timestamp();
|
||||||
|
}
|
||||||
|
catch (Kakoune::runtime_error& error)
|
||||||
|
{
|
||||||
|
context().print_status({ error.what(), get_face("Error") });
|
||||||
|
context().hooks().run_hook("RuntimeError", error.what(), context());
|
||||||
|
}
|
||||||
|
catch (Kakoune::client_removed&)
|
||||||
|
{
|
||||||
|
ClientManager::instance().remove_client(*this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Key key = m_ui->get_key();
|
||||||
|
if (key == ctrl('c'))
|
||||||
|
killpg(getpgrp(), SIGINT);
|
||||||
|
else
|
||||||
|
m_pending_keys.push_back(key);
|
||||||
}
|
}
|
||||||
context().window().forget_timestamp();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Client::print_status(DisplayLine status_line)
|
void Client::print_status(DisplayLine status_line)
|
||||||
|
|
|
@ -14,6 +14,9 @@ namespace Kakoune
|
||||||
class UserInterface;
|
class UserInterface;
|
||||||
class Window;
|
class Window;
|
||||||
class String;
|
class String;
|
||||||
|
struct Key;
|
||||||
|
|
||||||
|
enum class EventMode;
|
||||||
|
|
||||||
class Client : public SafeCountable, public OptionManagerWatcher
|
class Client : public SafeCountable, public OptionManagerWatcher
|
||||||
{
|
{
|
||||||
|
@ -28,7 +31,7 @@ public:
|
||||||
Client(Client&&) = delete;
|
Client(Client&&) = delete;
|
||||||
|
|
||||||
// handle all the keys currently available in the user interface
|
// handle all the keys currently available in the user interface
|
||||||
void handle_available_input();
|
void handle_available_input(EventMode mode);
|
||||||
|
|
||||||
void print_status(DisplayLine status_line);
|
void print_status(DisplayLine status_line);
|
||||||
|
|
||||||
|
@ -64,6 +67,8 @@ private:
|
||||||
DisplayLine m_status_line;
|
DisplayLine m_status_line;
|
||||||
DisplayLine m_pending_status_line;
|
DisplayLine m_pending_status_line;
|
||||||
DisplayLine m_mode_line;
|
DisplayLine m_mode_line;
|
||||||
|
|
||||||
|
std::vector<Key> m_pending_keys;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,26 +50,19 @@ Client* ClientManager::create_client(std::unique_ptr<UserInterface>&& ui,
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
client->ui().set_input_callback([client, this]() {
|
client->ui().set_input_callback([client](EventMode mode) {
|
||||||
try
|
client->handle_available_input(mode);
|
||||||
{
|
|
||||||
client->handle_available_input();
|
|
||||||
}
|
|
||||||
catch (Kakoune::runtime_error& error)
|
|
||||||
{
|
|
||||||
client->context().print_status({ error.what(), get_face("Error") });
|
|
||||||
client->context().hooks().run_hook("RuntimeError", error.what(),
|
|
||||||
client->context());
|
|
||||||
}
|
|
||||||
catch (Kakoune::client_removed&)
|
|
||||||
{
|
|
||||||
ClientManager::instance().remove_client(*client);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClientManager::handle_available_inputs() const
|
||||||
|
{
|
||||||
|
for (auto& client : m_clients)
|
||||||
|
client->handle_available_input(EventMode::Normal);
|
||||||
|
}
|
||||||
|
|
||||||
void ClientManager::remove_client(Client& client)
|
void ClientManager::remove_client(Client& client)
|
||||||
{
|
{
|
||||||
for (auto it = m_clients.begin(); it != m_clients.end(); ++it)
|
for (auto it = m_clients.begin(); it != m_clients.end(); ++it)
|
||||||
|
|
|
@ -35,6 +35,7 @@ public:
|
||||||
|
|
||||||
void redraw_clients() const;
|
void redraw_clients() const;
|
||||||
void clear_mode_trashes() const;
|
void clear_mode_trashes() const;
|
||||||
|
void handle_available_inputs() const;
|
||||||
|
|
||||||
Client* get_client_ifp(StringView name);
|
Client* get_client_ifp(StringView name);
|
||||||
Client& get_client(StringView name);
|
Client& get_client(StringView name);
|
||||||
|
|
|
@ -16,8 +16,13 @@ FDWatcher::~FDWatcher()
|
||||||
EventManager::instance().m_fd_watchers.erase(this);
|
EventManager::instance().m_fd_watchers.erase(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer::Timer(TimePoint date, Callback callback)
|
void FDWatcher::run(EventMode mode)
|
||||||
: m_date{date}, m_callback{std::move(callback)}
|
{
|
||||||
|
m_callback(*this, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer::Timer(TimePoint date, Callback callback, EventMode mode)
|
||||||
|
: m_date{date}, m_callback{std::move(callback)}, m_mode(mode)
|
||||||
{
|
{
|
||||||
if (EventManager::has_instance())
|
if (EventManager::has_instance())
|
||||||
EventManager::instance().m_timers.insert(this);
|
EventManager::instance().m_timers.insert(this);
|
||||||
|
@ -29,10 +34,15 @@ Timer::~Timer()
|
||||||
EventManager::instance().m_timers.erase(this);
|
EventManager::instance().m_timers.erase(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timer::run()
|
void Timer::run(EventMode mode)
|
||||||
{
|
{
|
||||||
m_date = TimePoint::max();
|
if (mode & m_mode)
|
||||||
m_callback(*this);
|
{
|
||||||
|
m_date = TimePoint::max();
|
||||||
|
m_callback(*this);
|
||||||
|
}
|
||||||
|
else // try again a little later
|
||||||
|
m_date = Clock::now() + std::chrono::milliseconds{10};
|
||||||
}
|
}
|
||||||
|
|
||||||
EventManager::EventManager()
|
EventManager::EventManager()
|
||||||
|
@ -46,7 +56,7 @@ EventManager::~EventManager()
|
||||||
kak_assert(m_timers.empty());
|
kak_assert(m_timers.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EventManager::handle_next_events()
|
void EventManager::handle_next_events(EventMode mode)
|
||||||
{
|
{
|
||||||
std::vector<pollfd> events;
|
std::vector<pollfd> events;
|
||||||
events.reserve(m_fd_watchers.size());
|
events.reserve(m_fd_watchers.size());
|
||||||
|
@ -76,7 +86,7 @@ void EventManager::handle_next_events()
|
||||||
auto it = find_if(m_fd_watchers,
|
auto it = find_if(m_fd_watchers,
|
||||||
[fd](FDWatcher* w) { return w->fd() == fd; });
|
[fd](FDWatcher* w) { return w->fd() == fd; });
|
||||||
if (it != m_fd_watchers.end())
|
if (it != m_fd_watchers.end())
|
||||||
(*it)->run();
|
(*it)->run(mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +94,7 @@ void EventManager::handle_next_events()
|
||||||
for (auto& timer : m_timers)
|
for (auto& timer : m_timers)
|
||||||
{
|
{
|
||||||
if (timer->next_date() <= now)
|
if (timer->next_date() <= now)
|
||||||
timer->run();
|
timer->run(mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#define event_manager_hh_INCLUDED
|
#define event_manager_hh_INCLUDED
|
||||||
|
|
||||||
#include "utils.hh"
|
#include "utils.hh"
|
||||||
|
#include "flags.hh"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
@ -9,20 +10,28 @@
|
||||||
namespace Kakoune
|
namespace Kakoune
|
||||||
{
|
{
|
||||||
|
|
||||||
|
enum class EventMode
|
||||||
|
{
|
||||||
|
Normal = 1 << 0,
|
||||||
|
Urgent = 1 << 1
|
||||||
|
};
|
||||||
|
|
||||||
|
template<> struct WithBitOps<EventMode> : std::true_type {};
|
||||||
|
|
||||||
class FDWatcher
|
class FDWatcher
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using Callback = std::function<void (FDWatcher& watcher)>;
|
using Callback = std::function<void (FDWatcher& watcher, EventMode mode)>;
|
||||||
FDWatcher(int fd, Callback callback);
|
FDWatcher(int fd, Callback callback);
|
||||||
~FDWatcher();
|
~FDWatcher();
|
||||||
|
|
||||||
int fd() const { return m_fd; }
|
int fd() const { return m_fd; }
|
||||||
void run() { m_callback(*this); }
|
void run(EventMode mode);
|
||||||
private:
|
private:
|
||||||
FDWatcher(const FDWatcher&) = delete;
|
FDWatcher(const FDWatcher&) = delete;
|
||||||
|
|
||||||
int m_fd;
|
int m_fd;
|
||||||
Callback m_callback;
|
Callback m_callback;
|
||||||
};
|
};
|
||||||
|
|
||||||
using Clock = std::chrono::steady_clock;
|
using Clock = std::chrono::steady_clock;
|
||||||
|
@ -33,15 +42,17 @@ class Timer
|
||||||
public:
|
public:
|
||||||
using Callback = std::function<void (Timer& timer)>;
|
using Callback = std::function<void (Timer& timer)>;
|
||||||
|
|
||||||
Timer(TimePoint date, Callback callback);
|
Timer(TimePoint date, Callback callback,
|
||||||
|
EventMode mode = EventMode::Normal);
|
||||||
~Timer();
|
~Timer();
|
||||||
|
|
||||||
TimePoint next_date() const { return m_date; }
|
TimePoint next_date() const { return m_date; }
|
||||||
void set_next_date(TimePoint date) { m_date = date; }
|
void set_next_date(TimePoint date) { m_date = date; }
|
||||||
void run();
|
void run(EventMode mode);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TimePoint m_date;
|
TimePoint m_date;
|
||||||
|
EventMode m_mode;
|
||||||
Callback m_callback;
|
Callback m_callback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,7 +67,7 @@ public:
|
||||||
EventManager();
|
EventManager();
|
||||||
~EventManager();
|
~EventManager();
|
||||||
|
|
||||||
void handle_next_events();
|
void handle_next_events(EventMode mode);
|
||||||
|
|
||||||
// force the watchers associated with fd to be executed
|
// force the watchers associated with fd to be executed
|
||||||
// on next handle_next_events call.
|
// on next handle_next_events call.
|
||||||
|
|
|
@ -305,7 +305,7 @@ int run_client(StringView session, StringView init_command)
|
||||||
RemoteClient client{session, make_unique<NCursesUI>(),
|
RemoteClient client{session, make_unique<NCursesUI>(),
|
||||||
get_env_vars(), init_command};
|
get_env_vars(), init_command};
|
||||||
while (true)
|
while (true)
|
||||||
event_manager.handle_next_events();
|
event_manager.handle_next_events(EventMode::Normal);
|
||||||
}
|
}
|
||||||
catch (peer_disconnected&)
|
catch (peer_disconnected&)
|
||||||
{
|
{
|
||||||
|
@ -407,7 +407,8 @@ int run_server(StringView session, StringView init_command,
|
||||||
|
|
||||||
while (not terminate and (not client_manager.empty() or daemon))
|
while (not terminate and (not client_manager.empty() or daemon))
|
||||||
{
|
{
|
||||||
event_manager.handle_next_events();
|
event_manager.handle_next_events(EventMode::Normal);
|
||||||
|
client_manager.handle_available_inputs();
|
||||||
client_manager.clear_mode_trashes();
|
client_manager.clear_mode_trashes();
|
||||||
buffer_manager.clear_buffer_trash();
|
buffer_manager.clear_buffer_trash();
|
||||||
client_manager.redraw_clients();
|
client_manager.redraw_clients();
|
||||||
|
|
|
@ -219,8 +219,10 @@ void on_sigint(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
NCursesUI::NCursesUI()
|
NCursesUI::NCursesUI()
|
||||||
: m_stdin_watcher{0, [this](FDWatcher&){ if (m_input_callback)
|
: m_stdin_watcher{0, [this](FDWatcher&, EventMode mode) {
|
||||||
m_input_callback(); }}
|
if (m_input_callback)
|
||||||
|
m_input_callback(mode);
|
||||||
|
}}
|
||||||
{
|
{
|
||||||
initscr();
|
initscr();
|
||||||
raw();
|
raw();
|
||||||
|
|
|
@ -18,9 +18,6 @@
|
||||||
#include "user_interface.hh"
|
#include "user_interface.hh"
|
||||||
#include "window.hh"
|
#include "window.hh"
|
||||||
|
|
||||||
#include <signal.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
namespace Kakoune
|
namespace Kakoune
|
||||||
{
|
{
|
||||||
|
|
||||||
|
@ -1400,8 +1397,6 @@ KeyMap keymap =
|
||||||
|
|
||||||
{ Key::PageUp, { "scroll one page up", scroll<Key::PageUp> } },
|
{ Key::PageUp, { "scroll one page up", scroll<Key::PageUp> } },
|
||||||
{ Key::PageDown, { "scroll one page down", scroll<Key::PageDown> } },
|
{ Key::PageDown, { "scroll one page down", scroll<Key::PageDown> } },
|
||||||
|
|
||||||
{ ctrl('c'), { "interupt", [](Context&, int) { killpg(getpgrp(), SIGINT); } } },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -282,9 +282,9 @@ private:
|
||||||
|
|
||||||
|
|
||||||
RemoteUI::RemoteUI(int socket)
|
RemoteUI::RemoteUI(int socket)
|
||||||
: m_socket_watcher(socket, [this](FDWatcher&) {
|
: m_socket_watcher(socket, [this](FDWatcher&, EventMode mode) {
|
||||||
if (m_input_callback)
|
if (m_input_callback)
|
||||||
m_input_callback();
|
m_input_callback(mode);
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
write_debug("remote client connected: " +
|
write_debug("remote client connected: " +
|
||||||
|
@ -453,9 +453,9 @@ RemoteClient::RemoteClient(StringView session, std::unique_ptr<UserInterface>&&
|
||||||
msg.write(key);
|
msg.write(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
m_ui->set_input_callback([this]{ write_next_key(); });
|
m_ui->set_input_callback([this](EventMode){ write_next_key(); });
|
||||||
|
|
||||||
m_socket_watcher.reset(new FDWatcher{sock, [this](FDWatcher&){ process_available_messages(); }});
|
m_socket_watcher.reset(new FDWatcher{sock, [this](FDWatcher&, EventMode){ process_available_messages(); }});
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemoteClient::process_available_messages()
|
void RemoteClient::process_available_messages()
|
||||||
|
@ -560,7 +560,10 @@ class Server::Accepter
|
||||||
public:
|
public:
|
||||||
Accepter(int socket)
|
Accepter(int socket)
|
||||||
: m_socket_watcher(socket,
|
: m_socket_watcher(socket,
|
||||||
[this](FDWatcher&) { handle_available_input(); })
|
[this](FDWatcher&, EventMode mode) {
|
||||||
|
if (mode == EventMode::Normal)
|
||||||
|
handle_available_input();
|
||||||
|
})
|
||||||
{}
|
{}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -626,7 +629,7 @@ Server::Server(String session_name)
|
||||||
if (listen(listen_sock, 4) == -1)
|
if (listen(listen_sock, 4) == -1)
|
||||||
throw runtime_error("unable to listen on socket "_str + addr.sun_path);
|
throw runtime_error("unable to listen on socket "_str + addr.sun_path);
|
||||||
|
|
||||||
auto accepter = [this](FDWatcher& watcher) {
|
auto accepter = [this](FDWatcher& watcher, EventMode mode) {
|
||||||
sockaddr_un client_addr;
|
sockaddr_un client_addr;
|
||||||
socklen_t client_addr_len = sizeof(sockaddr_un);
|
socklen_t client_addr_len = sizeof(sockaddr_un);
|
||||||
int sock = accept(watcher.fd(), (sockaddr*) &client_addr,
|
int sock = accept(watcher.fd(), (sockaddr*) &client_addr,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include "context.hh"
|
#include "context.hh"
|
||||||
#include "debug.hh"
|
#include "debug.hh"
|
||||||
|
#include "event_manager.hh"
|
||||||
#include "file.hh"
|
#include "file.hh"
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
@ -59,25 +60,34 @@ String ShellManager::pipe(StringView input,
|
||||||
write(write_pipe[1], input.data(), (int)input.length());
|
write(write_pipe[1], input.data(), (int)input.length());
|
||||||
close(write_pipe[1]);
|
close(write_pipe[1]);
|
||||||
|
|
||||||
char buffer[1024];
|
String error;
|
||||||
while (size_t size = read(read_pipe[0], buffer, 1024))
|
|
||||||
{
|
{
|
||||||
if (size == -1)
|
auto pipe_reader = [](String& output, bool& closed) {
|
||||||
break;
|
return [&output, &closed](FDWatcher& watcher, EventMode) {
|
||||||
output += String(buffer, buffer+size);
|
if (closed)
|
||||||
}
|
return;
|
||||||
close(read_pipe[0]);
|
const int fd = watcher.fd();
|
||||||
|
char buffer[1024];
|
||||||
|
size_t size = read(fd, buffer, 1024);
|
||||||
|
if (size <= 0)
|
||||||
|
{
|
||||||
|
close(fd);
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
output += String(buffer, buffer+size);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
String errorout;
|
bool stdout_closed = false, stderr_closed = false;
|
||||||
while (size_t size = read(error_pipe[0], buffer, 1024))
|
FDWatcher stdout_watcher{read_pipe[0], pipe_reader(output, stdout_closed)};
|
||||||
{
|
FDWatcher stderr_watcher{error_pipe[0], pipe_reader(error, stderr_closed)};
|
||||||
if (size == -1)
|
|
||||||
break;
|
while (not stdout_closed or not stderr_closed)
|
||||||
errorout += String(buffer, buffer+size);
|
EventManager::instance().handle_next_events(EventMode::Urgent);
|
||||||
}
|
}
|
||||||
close(error_pipe[0]);
|
|
||||||
if (not errorout.empty())
|
if (not error.empty())
|
||||||
write_debug("shell stderr: <<<\n" + errorout + ">>>");
|
write_debug("shell stderr: <<<\n" + error + ">>>");
|
||||||
|
|
||||||
waitpid(pid, exit_status, 0);
|
waitpid(pid, exit_status, 0);
|
||||||
if (exit_status)
|
if (exit_status)
|
||||||
|
@ -105,13 +115,7 @@ String ShellManager::pipe(StringView input,
|
||||||
{
|
{
|
||||||
auto& match = *it;
|
auto& match = *it;
|
||||||
|
|
||||||
StringView name;
|
StringView name = StringView(match[1].first, match[1].second);
|
||||||
if (match[1].matched)
|
|
||||||
name = StringView(match[1].first, match[1].second);
|
|
||||||
else if (match[2].matched)
|
|
||||||
name = StringView(match[2].first, match[2].second);
|
|
||||||
else
|
|
||||||
kak_assert(false);
|
|
||||||
kak_assert(name.length() > 0);
|
kak_assert(name.length() > 0);
|
||||||
|
|
||||||
auto local_var = env_vars.find(name);
|
auto local_var = env_vars.find(name);
|
||||||
|
|
|
@ -32,7 +32,9 @@ enum class InfoStyle
|
||||||
MenuDoc
|
MenuDoc
|
||||||
};
|
};
|
||||||
|
|
||||||
using InputCallback = std::function<void()>;
|
enum class EventMode;
|
||||||
|
|
||||||
|
using InputCallback = std::function<void(EventMode mode)>;
|
||||||
|
|
||||||
class UserInterface : public SafeCountable
|
class UserInterface : public SafeCountable
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue
Block a user