added IPC cilent for tui

This commit is contained in:
Lasan Mahaliyana
2026-06-21 14:55:49 +05:30
parent d8a64867be
commit 3175b646b9
16 changed files with 533 additions and 33 deletions
+26
View File
@@ -1,5 +1,6 @@
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_cc//cc:cc_test.bzl", "cc_test")
load("@hedron_compile_commands//:refresh_compile_commands.bzl", "refresh_compile_commands")
@@ -51,6 +52,31 @@ cc_library(
],
)
cc_library(
name = "ipc_client",
srcs = [
"ipc_client.cc",
],
hdrs = [
"ipc_client.h",
],
deps = [
"@nlohmann_json//:json",
],
)
cc_test(
name = "ipc_client_test",
srcs = [
"ipc_client_test.cc",
],
deps = [
":ipc_client",
"@com_google_googletest//:gtest_main",
"@nlohmann_json//:json",
],
)
cc_library(
name = "file_picker",
srcs = [
+18 -4
View File
@@ -2,6 +2,8 @@
#include <unistd.h>
#include <string>
#include "ftxui/component/component.hpp"
#include "sharing/linux/tui/ui/home_screen.h"
@@ -33,6 +35,12 @@ int TuiApp::Run() {
selected_file_ = path;
current_page_ = Page::Sharing;
},
.incoming_share_device_name = incoming_share_device_name_,
.incoming_share_device_type = incoming_share_device_type_,
.on_incoming_share_accept =
[this]() { current_page_ = Page::FilePicker; },
.on_incoming_share_decline =
[this]() { current_page_ = Page::FilePicker; },
});
auto app =
@@ -48,10 +56,16 @@ bool TuiApp::HandleEvent(Event event) {
return true;
}
if (event == Event::Backspace && current_page_ == Page::Sharing) {
current_page_ = Page::FilePicker;
selected_file_.clear();
return true;
if (event == Event::Backspace) {
if (current_page_ == Page::Sharing) {
current_page_ = Page::FilePicker;
selected_file_.clear();
return true;
}
if (current_page_ == Page::IncomingShare) {
current_page_ = Page::FilePicker;
return true;
}
}
return false;
+5 -2
View File
@@ -1,11 +1,12 @@
#pragma once
#include <string>
#include "ftxui/component/screen_interactive.hpp"
#include "sharing/linux/tui/components/share_target.h"
#include "sharing/linux/tui/file_picker.h"
#include "sharing/linux/tui/page.h"
#include <string>
namespace nearby::sharing::linux_tui {
using namespace ftxui;
@@ -23,6 +24,8 @@ class TuiApp {
Page current_page_ = Page::FilePicker;
std::string hostname_;
std::string selected_file_;
std::string incoming_share_device_name_ = "Lasan's A55";
ShareTargetType incoming_share_device_type_ = ShareTargetType::kPhone;
};
} // namespace nearby::sharing::linux_tui
+16
View File
@@ -35,6 +35,22 @@ cc_library(
":icons",
],
)
cc_library(
name = "incoming_share_card",
srcs = [
"incoming_share_card.cc",
],
hdrs = [
"incoming_share_card.h",
],
deps = [
":share_target",
"//sharing/linux/tui:palette",
"@ftxui//:ftxui",
],
)
cc_library(
name = "file_picker_card",
srcs = [
@@ -0,0 +1,44 @@
#include "sharing/linux/tui/components/incoming_share_card.h"
#include "ftxui/component/component.hpp"
#include "ftxui/dom/elements.hpp"
#include "sharing/linux/tui/components/share_target.h"
#include "sharing/linux/tui/palette.h"
namespace nearby::sharing::linux_tui {
using namespace ftxui;
Component IncomingShareCard(IncomingShareCardOptions options) {
auto accept_button = Button("Accept", [options] {
if (options.on_accept) {
options.on_accept();
}
});
auto decline_button = Button("Decline", [options] {
if (options.on_decline) {
options.on_decline();
}
});
auto controls = Container::Horizontal({accept_button, decline_button});
return Renderer(controls, [accept_button, decline_button, options] {
return vbox({
window(text(" Incoming share ") | center,
vbox({
paragraph("Do you want to accept this share?") |
bold | center,
separator(),
ShareTarget(options.device_name, options.device_type),
separator(),
hbox({
accept_button->Render() | flex,
separator(),
decline_button->Render() | flex,
}),
})),
}) |
borderStyled(Palette::border) | bgcolor(Palette::surface) |
size(WIDTH, GREATER_THAN, 40);
});
}
} // namespace nearby::sharing::linux_tui
@@ -0,0 +1,21 @@
#pragma once
#include <functional>
#include <string>
#include "ftxui/component/component.hpp"
#include "sharing/linux/tui/components/share_target.h"
namespace nearby::sharing::linux_tui {
using namespace ftxui;
struct IncomingShareCardOptions {
std::string device_name;
ShareTargetType device_type = ShareTargetType::kUnknown;
std::function<void()> on_accept;
std::function<void()> on_decline;
};
Component IncomingShareCard(IncomingShareCardOptions options);
} // namespace nearby::sharing::linux_tui
+2 -2
View File
@@ -25,8 +25,8 @@ Element ShareTarget(std::string device_name, ShareTargetType device_type) {
{icon, separatorLight() | dim,
paragraph(device_name) | color(Palette::secondary) | bold | center
}) |
size(WIDTH, EQUAL, 14) | border;
}) |bgcolor(Palette::surface)|
size(WIDTH, EQUAL, 14) | borderStyled(Palette::border);
};
} // namespace nearby::sharing::linux_tui
+145
View File
@@ -0,0 +1,145 @@
#include "sharing/linux/tui/ipc_client.h"
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <cstring>
#include <utility>
namespace nearby::sharing::linux_tui {
IpcClient::IpcClient(std::string socket_path)
: socket_path_(std::move(socket_path)) {}
IpcClient::~IpcClient() {
Stop();
}
bool IpcClient::Start(EventHandler event_handler) {
Stop();
event_handler_ = std::move(event_handler);
socket_fd_ = socket(AF_UNIX, SOCK_STREAM, 0);
if (socket_fd_ < 0) {
Emit({{"event", "ipc_disconnected"},
{"message", "failed to create socket"}});
return false;
}
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path_.c_str(), sizeof(addr.sun_path) - 1);
if (connect(socket_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) <
0) {
close(socket_fd_);
socket_fd_ = -1;
Emit({{"event", "ipc_disconnected"},
{"message", "failed to connect to daemon"}});
return false;
}
running_.store(true);
connected_.store(true);
read_thread_ = std::thread([this] { ReadLoop(); });
Emit({{"event", "ipc_connected"}});
return true;
}
void IpcClient::Stop() {
running_.store(false);
connected_.store(false);
{
std::lock_guard<std::mutex> lock(write_mutex_);
if (socket_fd_ >= 0) {
shutdown(socket_fd_, SHUT_RDWR);
close(socket_fd_);
socket_fd_ = -1;
}
}
if (read_thread_.joinable()) {
read_thread_.join();
}
}
bool IpcClient::Send(const nlohmann::json& message) {
return SendLine(message.dump());
}
void IpcClient::ReadLoop() {
std::string buffer;
char chunk[1024]{};
while (running_.load()) {
ssize_t received = recv(socket_fd_, chunk, sizeof(chunk), 0);
if (received > 0) {
buffer.append(chunk, static_cast<size_t>(received));
size_t newline = std::string::npos;
while ((newline = buffer.find('\n')) != std::string::npos) {
std::string line = buffer.substr(0, newline);
buffer.erase(0, newline + 1);
if (line.empty()) {
continue;
}
try {
Emit(nlohmann::json::parse(line));
} catch (const nlohmann::json::exception& error) {
Emit({{"event", "ipc_error"},
{"message",
std::string("malformed daemon JSON: ") + error.what()}});
}
}
continue;
}
if (received < 0 && errno == EINTR) {
continue;
}
break;
}
connected_.store(false);
if (running_.load()) {
Emit({{"event", "ipc_disconnected"}, {"message", "daemon disconnected"}});
}
}
void IpcClient::Emit(nlohmann::json event) {
if (event_handler_) {
event_handler_(event);
}
}
bool IpcClient::SendLine(std::string_view line) {
std::lock_guard<std::mutex> lock(write_mutex_);
if (socket_fd_ < 0) {
return false;
}
std::string data(line);
if (data.empty() || data.back() != '\n') {
data.push_back('\n');
}
size_t total_sent = 0;
while (total_sent < data.size()) {
ssize_t sent = send(socket_fd_, data.data() + total_sent,
data.size() - total_sent, MSG_NOSIGNAL);
if (sent > 0) {
total_sent += static_cast<size_t>(sent);
continue;
}
if (sent < 0 && errno == EINTR) {
continue;
}
return false;
}
return true;
}
} // namespace nearby::sharing::linux_tui
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <atomic>
#include <functional>
#include <mutex>
#include <string>
#include <string_view>
#include <thread>
#include "nlohmann/json.hpp"
namespace nearby::sharing::linux_tui {
class IpcClient {
public:
using EventHandler = std::function<void(const nlohmann::json& event)>;
explicit IpcClient(std::string socket_path = "/tmp/nearby_sharing_sock");
~IpcClient();
IpcClient(const IpcClient&) = delete;
IpcClient& operator=(const IpcClient&) = delete;
bool Start(EventHandler event_handler);
void Stop();
bool Send(const nlohmann::json& message);
bool connected() const { return connected_.load(); }
private:
void ReadLoop();
void Emit(nlohmann::json event);
bool SendLine(std::string_view line);
std::string socket_path_;
EventHandler event_handler_;
std::thread read_thread_;
mutable std::mutex write_mutex_;
std::atomic<bool> running_{false};
std::atomic<bool> connected_{false};
int socket_fd_ = -1;
};
} // namespace nearby::sharing::linux_tui
+135
View File
@@ -0,0 +1,135 @@
#include "sharing/linux/tui/ipc_client.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <condition_variable>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "gtest/gtest.h"
#include "nlohmann/json.hpp"
namespace nearby::sharing::linux_tui {
namespace {
std::string TestSocketPath() {
return "/tmp/nearby_tui_ipc_client_test_" + std::to_string(getpid());
}
int CreateServerSocket(const std::string& socket_path) {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
EXPECT_GE(fd, 0);
unlink(socket_path.c_str());
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1);
EXPECT_EQ(bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)), 0);
EXPECT_EQ(listen(fd, 1), 0);
return fd;
}
std::string ReadLine(int fd) {
std::string line;
char byte = '\0';
while (recv(fd, &byte, 1, 0) == 1) {
if (byte == '\n') {
return line;
}
line.push_back(byte);
}
return line;
}
void SendAll(int fd, const std::string& data) {
size_t sent_total = 0;
while (sent_total < data.size()) {
ssize_t sent = send(fd, data.data() + sent_total, data.size() - sent_total,
MSG_NOSIGNAL);
ASSERT_GT(sent, 0);
sent_total += static_cast<size_t>(sent);
}
}
} // namespace
TEST(IpcClientTest, SendsJsonCommandWithNewline) {
std::string socket_path = TestSocketPath();
int server_fd = CreateServerSocket(socket_path);
std::string received;
std::thread server_thread([&] {
int client_fd = accept(server_fd, nullptr, nullptr);
ASSERT_GE(client_fd, 0);
received = ReadLine(client_fd);
close(client_fd);
});
IpcClient client(socket_path);
ASSERT_TRUE(client.Start([](const nlohmann::json&) {}));
EXPECT_TRUE(client.Send({{"command", "start_receive"}}));
client.Stop();
server_thread.join();
close(server_fd);
unlink(socket_path.c_str());
nlohmann::json command = nlohmann::json::parse(received);
EXPECT_EQ(command["command"], "start_receive");
}
TEST(IpcClientTest, ParsesSplitJsonEvents) {
std::string socket_path = TestSocketPath();
int server_fd = CreateServerSocket(socket_path);
std::mutex mutex;
std::condition_variable cv;
std::vector<nlohmann::json> events;
std::thread server_thread([&] {
int client_fd = accept(server_fd, nullptr, nullptr);
ASSERT_GE(client_fd, 0);
SendAll(client_fd,
R"({"event":"target_discovered","share_target":{"id":1}})"
"\n"
R"({"event":"incoming)");
SendAll(client_fd, R"(_transfer","share_target":{"id":2},"transfer":{}})"
"\n");
close(client_fd);
});
IpcClient client(socket_path);
ASSERT_TRUE(client.Start([&](const nlohmann::json& event) {
if (event.value("event", std::string()).rfind("ipc_", 0) == 0) {
return;
}
std::lock_guard<std::mutex> lock(mutex);
events.push_back(event);
cv.notify_one();
}));
{
std::unique_lock<std::mutex> lock(mutex);
cv.wait_for(lock, std::chrono::seconds(2),
[&] { return events.size() >= 2; });
}
client.Stop();
server_thread.join();
close(server_fd);
unlink(socket_path.c_str());
ASSERT_GE(events.size(), 2u);
EXPECT_EQ(events[0]["event"], "target_discovered");
EXPECT_EQ(events[1]["event"], "incoming_transfer");
}
} // namespace nearby::sharing::linux_tui
+1
View File
@@ -5,6 +5,7 @@ namespace nearby::sharing::linux_tui {
enum class Page {
FilePicker,
Sharing,
IncomingShare,
};
} // namespace nearby::sharing::linux_tui
+31 -10
View File
@@ -6,20 +6,41 @@ namespace nearby::sharing::linux_tui {
using namespace ftxui;
struct Palette {
inline static const Color primary =
Color::RGB(0xF1, 0xF9, 0xF6); // Off-white mint tint
// === Core Typography & Brand ===
// Primary light text for high-contrast readability against dark backgrounds
inline static const Color primary = Color::RGB(0xF1, 0xF9, 0xF6); // Off-white mint tint
inline static const Color secondary =
Color::RGB(0x8A, 0xAF, 0xA4); // Muted sage gray
// Secondary text, inactive states, or subtle accents
inline static const Color secondary = Color::RGB(0x8A, 0xAF, 0xA4); // Muted sage gray
inline static const Color accent =
Color::RGB(0xA3, 0xD9, 0xC9); // Original Mint Leaf
// Main brand accent, focused element backgrounds, or prominent icons (Unchanged)
inline static const Color accent = Color::RGB(0xA3, 0xD9, 0xC9); // Original Mint Leaf
inline static const Color active =
Color::RGB(0x23, 0x3D, 0x34); // Deep forest green tint
// === Surface & Structure ===
// Active menu item highlights, selection cards, or focused component backgrounds
inline static const Color active = Color::RGB(0x23, 0x3D, 0x34); // Deep forest green tint
inline static const Color base =
Color::RGB(0x12, 0x1D, 0x1A); // Near-black deep mint/charcoal
// Base application window background
inline static const Color base = Color::RGB(0x12, 0x1D, 0x1A); // Near-black deep mint/charcoal
// Explicit component surface background (e.g., sidebars, modals, or inactive cards)
inline static const Color surface = Color::RGB(0x19, 0x2A, 0x25); // Mid-tone dark green
// Standard UI borders, dividers, and subtle grid lines
inline static const Color border = Color::RGB(0x32, 0x52, 0x47); // Defined slate green
// Extremely muted text, placeholders, or disabled options
inline static const Color disabled = Color::RGB(0x56, 0x73, 0x6B); // Ghostly sage green
// === Functional / Status Elements ===
// Critical errors, destructive actions, or alerts (toned down for dark mode)
inline static const Color error = Color::RGB(0xE0, 0x7A, 0x7A); // Soft desaturated coral/red
// Warnings, pending indicators, or high-priority notifications
inline static const Color warning = Color::RGB(0xE6, 0xC2, 0x80); // Soft amber gold
// Success messages, online badges, or completed operations
inline static const Color success = Color::RGB(0x81, 0xC7, 0x9D); // Vibrant spring mint
};
} // namespace nearby::sharing::linux_tui
+1
View File
@@ -56,6 +56,7 @@ cc_library(
":file_selected_screen",
":home_header",
":sidebar",
"//sharing/linux/tui/components:incoming_share_card",
"//sharing/linux/tui:file_picker",
"//sharing/linux/tui:page",
"//sharing/linux/tui:palette",
+19 -11
View File
@@ -1,6 +1,6 @@
#include "sharing/linux/tui/ui/file_selected_screen.h"
#include "sharing/linux/tui/components/share_target.h"
#include "sharing/linux/tui/components/share_target.h"
#include "sharing/linux/tui/palette.h"
namespace nearby::sharing::linux_tui {
@@ -10,20 +10,28 @@ Element FileSelectedScreen(const std::string& selected_file) {
FlexboxConfig config;
config.direction = FlexboxConfig::Direction::Row;
config.wrap = FlexboxConfig::Wrap::Wrap;
config.gap_x = 2;
config.gap_x = 1;
config.gap_y = 1;
return vbox({
flexbox({
ShareTarget("Lasan's A55", ShareTargetType::kPhone),
ShareTarget("lasan-laptop", ShareTargetType::kLaptop),
ShareTarget("lasan-laptop", ShareTargetType::kLaptop),
ShareTarget("lasan-laptop", ShareTargetType::kLaptop),
ShareTarget("Lasan's S9+", ShareTargetType::kTablet),
ShareTarget("Lasan's S9+", ShareTargetType::kTablet),
hbox(
{filler(),
flexbox(
{
ShareTarget("Lasan's A55", ShareTargetType::kPhone),
ShareTarget("lasan-laptop", ShareTargetType::kLaptop),
ShareTarget("lasan-laptop", ShareTargetType::kLaptop),
ShareTarget("lasan-laptop", ShareTargetType::kLaptop),
ShareTarget("Lasan's S9+", ShareTargetType::kTablet),
ShareTarget("Lasan's S9+", ShareTargetType::kTablet),
}, config) | flex
},
config) |
flex
}),
filler(),
}) |
borderStyled(Palette::primary) | flex;
flex;
}
} // namespace nearby::sharing::linux_tui
+21 -4
View File
@@ -2,6 +2,7 @@
#include "ftxui/dom/elements.hpp"
#include "sharing/linux/tui/components/file_picker_card.h"
#include "sharing/linux/tui/components/incoming_share_card.h"
#include "sharing/linux/tui/palette.h"
#include "sharing/linux/tui/ui/file_selected_screen.h"
#include "sharing/linux/tui/ui/home_header.h"
@@ -14,17 +15,32 @@ Component HomeScreen(HomeScreenOptions options) {
auto file_picker_card =
FilePickerCard({.file_picker = options.file_picker,
.on_file_selected = options.on_file_selected});
auto incoming_share_card =
IncomingShareCard({.device_name = options.incoming_share_device_name,
.device_type = options.incoming_share_device_type,
.on_accept = options.on_incoming_share_accept,
.on_decline = options.on_incoming_share_decline});
auto content = Container::Vertical({file_picker_card, incoming_share_card});
return Renderer(file_picker_card, [file_picker_card, options] {
return Renderer(content, [file_picker_card, incoming_share_card, options] {
const std::string selected_file =
options.selected_file == nullptr ? "" : *options.selected_file;
const Page current_page = options.current_page == nullptr
? Page::FilePicker
: *options.current_page;
auto main_panel = current_page == Page::FilePicker
? file_picker_card->Render()
: FileSelectedScreen(selected_file);
Element main_panel;
switch (current_page) {
case Page::FilePicker:
main_panel = file_picker_card->Render();
break;
case Page::IncomingShare:
main_panel = incoming_share_card->Render();
break;
case Page::Sharing:
main_panel = FileSelectedScreen(selected_file);
break;
}
return vbox({
HomeHeader() | color(Palette::accent),
@@ -32,6 +48,7 @@ Component HomeScreen(HomeScreenOptions options) {
hbox({
Sidebar({.hostname = options.hostname,
.selected_file = selected_file}),
separator() | color(Palette::border),
main_panel | flex,
}) | flex,
}) |
+5
View File
@@ -4,6 +4,7 @@
#include <string>
#include "ftxui/component/component.hpp"
#include "sharing/linux/tui/components/share_target.h"
#include "sharing/linux/tui/file_picker.h"
#include "sharing/linux/tui/page.h"
@@ -16,6 +17,10 @@ struct HomeScreenOptions {
const std::string* selected_file = nullptr;
FilePicker* file_picker = nullptr;
std::function<void(std::string)> on_file_selected;
std::string incoming_share_device_name;
ShareTargetType incoming_share_device_type = ShareTargetType::kUnknown;
std::function<void()> on_incoming_share_accept;
std::function<void()> on_incoming_share_decline;
};
Component HomeScreen(HomeScreenOptions options);