From bbff20b1b4b7a8c92d093246d00735dbfdb2fc70 Mon Sep 17 00:00:00 2001 From: muk Date: Tue, 27 Jan 2026 00:56:55 +0000 Subject: [PATCH] Add C/C++ FFI interface for native integration This adds a stable C-compatible FFI layer that enables C/C++ applications to integrate feedtui as a library. Features: - Lifecycle APIs: feedtui_init, feedtui_run, feedtui_shutdown - Config from file path or inline TOML string - Error handling with last_error retrieval - Version and feature queries - Opaque handles for safe memory management Build artifacts: - libfeedtui.so/.dylib/.dll (shared library) - libfeedtui.a/.lib (static library) - feedtui.h (C header) Documentation: - Comprehensive FFI guide in docs/FFI.md - C and C++ examples in examples/cpp/ - Makefile for building examples Usage: cargo build --release --features ffi Closes #12 Co-Authored-By: Claude Opus 4.5 --- Cargo.toml | 13 ++ docs/FFI.md | 399 ++++++++++++++++++++++++++++++++++++++++++ examples/cpp/Makefile | 73 ++++++++ examples/cpp/main.cpp | 123 +++++++++++++ examples/cpp/simple.c | 41 +++++ include/feedtui.h | 194 ++++++++++++++++++++ src/ffi.rs | 369 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 47 +++++ 8 files changed, 1259 insertions(+) create mode 100644 docs/FFI.md create mode 100644 examples/cpp/Makefile create mode 100644 examples/cpp/main.cpp create mode 100644 examples/cpp/simple.c create mode 100644 include/feedtui.h create mode 100644 src/ffi.rs create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 9743686..7d66c95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,19 @@ readme = "README.md" keywords = ["tui", "dashboard", "terminal", "rss", "stocks"] categories = ["command-line-utilities"] +[lib] +name = "feedtui" +path = "src/lib.rs" +crate-type = ["lib", "cdylib", "staticlib"] + +[[bin]] +name = "feedtui" +path = "src/main.rs" + +[features] +default = [] +ffi = [] + [dependencies] ratatui = "0.29" crossterm = "0.28" diff --git a/docs/FFI.md b/docs/FFI.md new file mode 100644 index 0000000..a0a9f55 --- /dev/null +++ b/docs/FFI.md @@ -0,0 +1,399 @@ +# feedtui FFI (Foreign Function Interface) + +This document describes how to use feedtui from C/C++ applications through its FFI interface. + +## Overview + +feedtui provides a C-compatible FFI layer that allows C/C++ applications to: +- Initialize the feedtui TUI application +- Run the dashboard (blocking or controlled) +- Cleanly shutdown and free resources + +The interface follows a lifecycle pattern with opaque handles, making it safe and easy to integrate. + +## Building with FFI Support + +### Prerequisites + +- Rust toolchain (1.70+) +- C/C++ compiler (GCC, Clang, or MSVC) +- OpenSSL development libraries + +### Build the Library + +```bash +# Build the shared and static libraries with FFI support +cargo build --release --features ffi + +# The libraries will be in target/release/ +# - libfeedtui.so (Linux shared) +# - libfeedtui.dylib (macOS shared) +# - libfeedtui.dll (Windows shared) +# - libfeedtui.a (static) +``` + +### Library Artifacts + +After building, you'll find: + +| Platform | Shared Library | Static Library | +|----------|---------------|----------------| +| Linux | `libfeedtui.so` | `libfeedtui.a` | +| macOS | `libfeedtui.dylib` | `libfeedtui.a` | +| Windows | `feedtui.dll` | `feedtui.lib` | + +## API Reference + +### Header File + +Include the header in your C/C++ code: + +```c +#include "feedtui.h" +``` + +The header is located at `include/feedtui.h`. + +### Types + +#### `FeedtuiHandle` + +An opaque pointer to the feedtui instance. Do not attempt to access its internal structure. + +```c +typedef struct FeedtuiHandle FeedtuiHandle; +``` + +#### `FeedtuiResult` + +Result codes returned by FFI functions: + +```c +typedef enum FeedtuiResult { + FEEDTUI_SUCCESS = 0, // Operation completed successfully + FEEDTUI_INVALID_HANDLE = 1, // Invalid or null handle + FEEDTUI_INVALID_CONFIG_PATH = 2, + FEEDTUI_CONFIG_LOAD_ERROR = 3, + FEEDTUI_RUNTIME_ERROR = 4, + FEEDTUI_APP_ERROR = 5, + FEEDTUI_PANIC = 6 // Rust panic (check last_error) +} FeedtuiResult; +``` + +### Functions + +#### `feedtui_init` + +Initialize a new feedtui instance with a config file. + +```c +FeedtuiHandle* feedtui_init(const char* config_path); +``` + +**Parameters:** +- `config_path`: Path to TOML config file (UTF-8), or `NULL` for default config + +**Returns:** Handle pointer on success, `NULL` on failure + +**Example:** +```c +// Use default configuration +FeedtuiHandle* handle = feedtui_init(NULL); + +// Use custom config file +FeedtuiHandle* handle = feedtui_init("/home/user/.feedtui/config.toml"); +``` + +#### `feedtui_init_with_config` + +Initialize with an inline TOML configuration string. + +```c +FeedtuiHandle* feedtui_init_with_config(const char* config_toml); +``` + +**Parameters:** +- `config_toml`: TOML configuration as a null-terminated string + +**Example:** +```c +const char* config = + "[general]\n" + "refresh_interval_secs = 60\n" + "\n" + "[[widgets]]\n" + "type = \"hackernews\"\n" + "title = \"HN\"\n" + "story_count = 10\n" + "position = { row = 0, col = 0 }\n"; + +FeedtuiHandle* handle = feedtui_init_with_config(config); +``` + +#### `feedtui_run` + +Run the feedtui TUI application. This function blocks until the user quits. + +```c +int feedtui_run(FeedtuiHandle* handle); +``` + +**Parameters:** +- `handle`: Valid handle from `feedtui_init` or `feedtui_init_with_config` + +**Returns:** `FEEDTUI_SUCCESS` (0) on success, error code otherwise + +**Note:** The terminal will be taken over for the TUI. Press 'q' to quit. + +#### `feedtui_shutdown` + +Free all resources and invalidate the handle. + +```c +void feedtui_shutdown(FeedtuiHandle* handle); +``` + +**Parameters:** +- `handle`: Handle to shutdown, or `NULL` (no-op) + +**Note:** After calling this, the handle must not be used again. + +#### `feedtui_get_last_error` + +Get the last error message. + +```c +const char* feedtui_get_last_error(const FeedtuiHandle* handle); +``` + +**Returns:** Error string or `NULL` if no error. Do not free this pointer. + +#### `feedtui_version` + +Get the library version string. + +```c +const char* feedtui_version(void); +``` + +**Returns:** Version string (e.g., "0.1.1"). Do not free this pointer. + +#### `feedtui_has_feature` + +Check if a feature was enabled at compile time. + +```c +int feedtui_has_feature(const char* feature); +``` + +**Returns:** 1 if enabled, 0 if not, -1 if invalid feature name + +## Complete Examples + +### Minimal C Example + +```c +#include +#include "feedtui.h" + +int main(void) { + FeedtuiHandle* handle = feedtui_init(NULL); + if (!handle) { + fprintf(stderr, "Failed to initialize\n"); + return 1; + } + + int result = feedtui_run(handle); + + if (result != FEEDTUI_SUCCESS) { + const char* err = feedtui_get_last_error(handle); + if (err) fprintf(stderr, "Error: %s\n", err); + } + + feedtui_shutdown(handle); + return result; +} +``` + +### C++ Example with Custom Config + +```cpp +#include +#include +#include "feedtui.h" + +int main() { + // Build a custom configuration + std::string config = R"( +[general] +refresh_interval_secs = 30 +theme = "dark" + +[[widgets]] +type = "hackernews" +title = "Hacker News" +story_count = 20 +story_type = "top" +position = { row = 0, col = 0 } + +[[widgets]] +type = "rss" +title = "Tech News" +feeds = [ + "https://feeds.arstechnica.com/arstechnica/technology-lab" +] +max_items = 15 +position = { row = 0, col = 1 } +)"; + + std::cout << "feedtui version: " << feedtui_version() << std::endl; + + FeedtuiHandle* handle = feedtui_init_with_config(config.c_str()); + if (!handle) { + std::cerr << "Failed to initialize feedtui" << std::endl; + return 1; + } + + int result = feedtui_run(handle); + + if (result != FEEDTUI_SUCCESS) { + const char* err = feedtui_get_last_error(handle); + if (err) { + std::cerr << "Error: " << err << std::endl; + } + } + + feedtui_shutdown(handle); + return result; +} +``` + +## Build Instructions + +### Linux (GCC) + +```bash +# Build the Rust library +cargo build --release --features ffi + +# Compile C code +gcc -o myapp myapp.c \ + -I/path/to/feedtui/include \ + -L/path/to/feedtui/target/release \ + -lfeedtui \ + -lpthread -ldl -lm + +# Run (set library path) +LD_LIBRARY_PATH=/path/to/feedtui/target/release ./myapp +``` + +### macOS (Clang) + +```bash +# Build the Rust library +cargo build --release --features ffi + +# Compile C code +clang -o myapp myapp.c \ + -I/path/to/feedtui/include \ + -L/path/to/feedtui/target/release \ + -lfeedtui \ + -framework Security -framework CoreFoundation + +# Run +DYLD_LIBRARY_PATH=/path/to/feedtui/target/release ./myapp +``` + +### CMake + +```cmake +cmake_minimum_required(VERSION 3.15) +project(MyFeedtuiApp) + +# Find the feedtui library +set(FEEDTUI_DIR "/path/to/feedtui") +set(FEEDTUI_INCLUDE "${FEEDTUI_DIR}/include") +set(FEEDTUI_LIB "${FEEDTUI_DIR}/target/release") + +add_executable(myapp main.cpp) + +target_include_directories(myapp PRIVATE ${FEEDTUI_INCLUDE}) +target_link_directories(myapp PRIVATE ${FEEDTUI_LIB}) +target_link_libraries(myapp feedtui pthread dl m) +``` + +## Thread Safety + +- All FFI functions must be called from the same thread +- Do not call FFI functions concurrently +- The TUI takes exclusive control of the terminal + +## Memory Management + +- Handles returned by `feedtui_init*` must be freed with `feedtui_shutdown` +- String pointers returned by `feedtui_get_last_error` and `feedtui_version` are owned by the library - do not free them +- Error strings are valid until the next FFI call or shutdown + +## Error Handling + +1. Check if functions return `NULL` (init functions) or non-zero (run) +2. Use `feedtui_get_last_error` to get detailed error messages +3. Always call `feedtui_shutdown` even after errors + +## Configuration Reference + +The TOML configuration format is the same as the CLI version. See the main README for full configuration options. + +### Minimal Configuration + +```toml +[general] +refresh_interval_secs = 60 + +[[widgets]] +type = "hackernews" +title = "HN" +story_count = 10 +story_type = "top" +position = { row = 0, col = 0 } +``` + +### Available Widget Types + +- `hackernews` - Hacker News stories +- `rss` - RSS feed aggregator +- `stocks` - Stock quotes +- `sports` - Sports scores +- `github` - GitHub dashboard +- `youtube` - YouTube videos +- `creature` - Virtual pet companion + +## Troubleshooting + +### Library not found at runtime + +Set the library path: +```bash +# Linux +export LD_LIBRARY_PATH=/path/to/target/release:$LD_LIBRARY_PATH + +# macOS +export DYLD_LIBRARY_PATH=/path/to/target/release:$DYLD_LIBRARY_PATH +``` + +### Linking errors + +Ensure you're linking all required system libraries: +- Linux: `-lpthread -ldl -lm` +- macOS: `-framework Security -framework CoreFoundation` + +### Terminal not restored after crash + +If feedtui crashes without proper cleanup, run: +```bash +reset +``` + +## License + +MIT License - See LICENSE file for details. diff --git a/examples/cpp/Makefile b/examples/cpp/Makefile new file mode 100644 index 0000000..5a95cdd --- /dev/null +++ b/examples/cpp/Makefile @@ -0,0 +1,73 @@ +# Makefile for feedtui C++ example +# +# This Makefile demonstrates how to build a C++ application that uses +# the feedtui FFI interface. + +# Configuration +CXX ?= g++ +CXXFLAGS = -std=c++17 -Wall -Wextra -O2 +LDFLAGS = + +# Paths +FEEDTUI_ROOT = ../.. +INCLUDE_DIR = $(FEEDTUI_ROOT)/include +LIB_DIR = $(FEEDTUI_ROOT)/target/release + +# Platform-specific settings +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Linux) + LDFLAGS += -lpthread -ldl -lm + LIB_NAME = libfeedtui.so + RUN_PREFIX = LD_LIBRARY_PATH=$(LIB_DIR) +endif +ifeq ($(UNAME_S),Darwin) + LDFLAGS += -framework Security -framework CoreFoundation + LIB_NAME = libfeedtui.dylib + RUN_PREFIX = DYLD_LIBRARY_PATH=$(LIB_DIR) +endif + +# Targets +TARGET = feedtui_example + +.PHONY: all clean run build-lib + +all: $(TARGET) + +# Build the C++ example +$(TARGET): main.cpp $(LIB_DIR)/$(LIB_NAME) + $(CXX) $(CXXFLAGS) -o $@ main.cpp \ + -I$(INCLUDE_DIR) \ + -L$(LIB_DIR) \ + -lfeedtui \ + $(LDFLAGS) + +# Build the Rust library with FFI support +$(LIB_DIR)/$(LIB_NAME): build-lib + +build-lib: + @echo "Building feedtui library with FFI support..." + cd $(FEEDTUI_ROOT) && cargo build --release --features ffi + +# Run the example +run: $(TARGET) + $(RUN_PREFIX) ./$(TARGET) + +# Run with custom config +run-config: $(TARGET) + $(RUN_PREFIX) ./$(TARGET) -c ../../examples/config.toml + +# Clean build artifacts +clean: + rm -f $(TARGET) + +# Show help +help: + @echo "feedtui C++ Example Makefile" + @echo "" + @echo "Targets:" + @echo " all - Build the example (default)" + @echo " build-lib - Build the Rust library with FFI support" + @echo " run - Build and run with default config" + @echo " run-config- Build and run with example config file" + @echo " clean - Remove build artifacts" + @echo " help - Show this help message" diff --git a/examples/cpp/main.cpp b/examples/cpp/main.cpp new file mode 100644 index 0000000..4d381a5 --- /dev/null +++ b/examples/cpp/main.cpp @@ -0,0 +1,123 @@ +/** + * @file main.cpp + * @brief Example of using feedtui from C++ + * + * This example demonstrates how to initialize, run, and shutdown + * the feedtui terminal dashboard from a C++ application. + * + * Build instructions: + * # First build the Rust library with FFI support + * cargo build --release --features ffi + * + * # Then compile this C++ example + * g++ -o feedtui_example main.cpp \ + * -I../../include \ + * -L../../target/release \ + * -lfeedtui \ + * -lpthread -ldl -lm + * + * # Run (may need to set library path) + * LD_LIBRARY_PATH=../../target/release ./feedtui_example + */ + +#include +#include +#include + +#include "feedtui.h" + +// Example TOML configuration with a simple HN widget +const char* DEFAULT_CONFIG = R"( +[general] +refresh_interval_secs = 60 +theme = "dark" + +[[widgets]] +type = "hackernews" +title = "Hacker News" +story_count = 15 +story_type = "top" +position = { row = 0, col = 0 } +)"; + +void print_usage(const char* program_name) { + printf("Usage: %s [options]\n", program_name); + printf("\n"); + printf("Options:\n"); + printf(" -c, --config Path to TOML config file\n"); + printf(" -v, --version Print version and exit\n"); + printf(" -h, --help Print this help message\n"); + printf("\n"); + printf("If no config file is specified, a default configuration with\n"); + printf("Hacker News widget will be used.\n"); +} + +int main(int argc, char* argv[]) { + const char* config_path = nullptr; + bool use_embedded_config = true; + + // Parse command line arguments + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--version") == 0) { + printf("feedtui version: %s\n", feedtui_version()); + printf("FFI support: %s\n", feedtui_has_feature("ffi") == 1 ? "yes" : "no"); + return 0; + } + else if ((strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) && i + 1 < argc) { + config_path = argv[++i]; + use_embedded_config = false; + } + else { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + // Print version info + printf("feedtui C++ Example\n"); + printf("Library version: %s\n", feedtui_version()); + printf("\n"); + + // Initialize feedtui + FeedtuiHandle* handle = nullptr; + + if (use_embedded_config) { + printf("Using embedded default configuration...\n"); + handle = feedtui_init_with_config(DEFAULT_CONFIG); + } else { + printf("Loading config from: %s\n", config_path); + handle = feedtui_init(config_path); + } + + if (!handle) { + fprintf(stderr, "Error: Failed to initialize feedtui\n"); + return 1; + } + + printf("Starting feedtui... (press 'q' to quit)\n"); + printf("\n"); + + // Run the TUI (this blocks until user quits) + int result = feedtui_run(handle); + + // Check for errors + if (result != FEEDTUI_SUCCESS) { + const char* error = feedtui_get_last_error(handle); + if (error) { + fprintf(stderr, "Error: %s\n", error); + } else { + fprintf(stderr, "Error: Unknown error (code %d)\n", result); + } + } + + // Clean up + feedtui_shutdown(handle); + + printf("\nfeedtui terminated with code: %d\n", result); + return result; +} diff --git a/examples/cpp/simple.c b/examples/cpp/simple.c new file mode 100644 index 0000000..10ca25c --- /dev/null +++ b/examples/cpp/simple.c @@ -0,0 +1,41 @@ +/** + * @file simple.c + * @brief Minimal C example of using feedtui + * + * Build: + * cargo build --release --features ffi + * gcc -o simple simple.c -I../../include -L../../target/release -lfeedtui -lpthread -ldl -lm + * + * Run: + * LD_LIBRARY_PATH=../../target/release ./simple + */ + +#include +#include "feedtui.h" + +int main(void) { + printf("feedtui version: %s\n", feedtui_version()); + + /* Initialize with default configuration */ + FeedtuiHandle* handle = feedtui_init(NULL); + if (!handle) { + fprintf(stderr, "Failed to initialize feedtui\n"); + return 1; + } + + /* Run the TUI (blocks until user quits) */ + int result = feedtui_run(handle); + + /* Check for errors */ + if (result != FEEDTUI_SUCCESS) { + const char* err = feedtui_get_last_error(handle); + if (err) { + fprintf(stderr, "Error: %s\n", err); + } + } + + /* Clean up */ + feedtui_shutdown(handle); + + return result; +} diff --git a/include/feedtui.h b/include/feedtui.h new file mode 100644 index 0000000..a0e4d03 --- /dev/null +++ b/include/feedtui.h @@ -0,0 +1,194 @@ +/** + * @file feedtui.h + * @brief C/C++ interface for feedtui terminal dashboard + * + * This header provides a C-compatible interface for integrating feedtui + * into C/C++ applications. The interface follows a lifecycle pattern: + * + * 1. Initialize with feedtui_init() or feedtui_init_with_config() + * 2. Run the TUI with feedtui_run() + * 3. Clean up with feedtui_shutdown() + * + * @example + * ```c + * #include "feedtui.h" + * + * int main(void) { + * FeedtuiHandle* handle = feedtui_init(NULL); + * if (!handle) { + * fprintf(stderr, "Failed to initialize feedtui\n"); + * return 1; + * } + * + * int result = feedtui_run(handle); + * feedtui_shutdown(handle); + * + * return result; + * } + * ``` + * + * @copyright MIT License + */ + +#ifndef FEEDTUI_H +#define FEEDTUI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque handle to a feedtui instance. + * + * Users should not attempt to access the internal structure. + * Use the provided functions to interact with the handle. + */ +typedef struct FeedtuiHandle FeedtuiHandle; + +/** + * @brief Result codes returned by feedtui functions. + */ +typedef enum FeedtuiResult { + /** Operation completed successfully */ + FEEDTUI_SUCCESS = 0, + /** Invalid or null handle provided */ + FEEDTUI_INVALID_HANDLE = 1, + /** Invalid or null config path */ + FEEDTUI_INVALID_CONFIG_PATH = 2, + /** Failed to load configuration */ + FEEDTUI_CONFIG_LOAD_ERROR = 3, + /** Failed to initialize runtime */ + FEEDTUI_RUNTIME_ERROR = 4, + /** Application error during execution */ + FEEDTUI_APP_ERROR = 5, + /** Panic occurred (check feedtui_get_last_error for details) */ + FEEDTUI_PANIC = 6 +} FeedtuiResult; + +/** + * @brief Initialize a new feedtui instance. + * + * @param config_path Path to the TOML configuration file (UTF-8 encoded, null-terminated). + * If NULL, uses the default configuration. + * + * @return A pointer to a FeedtuiHandle on success, or NULL on failure. + * The caller is responsible for calling feedtui_shutdown() to free the handle. + * + * @note The handle must not be used after calling feedtui_shutdown(). + * + * @example + * ```c + * // Use default config + * FeedtuiHandle* handle = feedtui_init(NULL); + * + * // Use custom config file + * FeedtuiHandle* handle = feedtui_init("/home/user/.feedtui/config.toml"); + * ``` + */ +FeedtuiHandle* feedtui_init(const char* config_path); + +/** + * @brief Initialize feedtui with a configuration string. + * + * This function allows passing the TOML configuration directly as a string, + * which is useful for embedded configurations or testing. + * + * @param config_toml TOML configuration content as a UTF-8 null-terminated string. + * Must not be NULL. + * + * @return A pointer to a FeedtuiHandle on success, or NULL on failure. + * + * @example + * ```c + * const char* config = + * "[general]\n" + * "refresh_interval_secs = 60\n" + * "\n" + * "[[widgets]]\n" + * "type = \"hackernews\"\n" + * "title = \"HN\"\n" + * "story_count = 10\n" + * "story_type = \"top\"\n" + * "position = { row = 0, col = 0 }\n"; + * + * FeedtuiHandle* handle = feedtui_init_with_config(config); + * ``` + */ +FeedtuiHandle* feedtui_init_with_config(const char* config_toml); + +/** + * @brief Run the feedtui application. + * + * This function blocks until the user quits the application (e.g., by pressing 'q'). + * The terminal will be taken over for the TUI display. + * + * @param handle A valid handle obtained from feedtui_init() or feedtui_init_with_config(). + * + * @return FEEDTUI_SUCCESS (0) on successful completion, or an error code on failure. + * + * @note This function must not be called concurrently from multiple threads. + * @note On error, call feedtui_get_last_error() to get the error message. + */ +int feedtui_run(FeedtuiHandle* handle); + +/** + * @brief Shutdown and free the feedtui instance. + * + * This function frees all resources associated with the handle. + * After calling this function, the handle is invalid and must not be used. + * + * @param handle A valid handle obtained from feedtui_init() or feedtui_init_with_config(). + * If NULL, this function does nothing. + * + * @note It is safe to call this function with a NULL handle. + * @note After calling this function, the handle must not be used again. + */ +void feedtui_shutdown(FeedtuiHandle* handle); + +/** + * @brief Get the last error message. + * + * @param handle A valid handle obtained from feedtui_init() or feedtui_init_with_config(). + * + * @return A pointer to a null-terminated UTF-8 string containing the last error message, + * or NULL if no error has occurred or if the handle is invalid. + * + * @note The returned string is owned by the handle and remains valid until: + * - The next FFI function call on this handle + * - feedtui_shutdown() is called + * + * @note Do not free the returned pointer. + */ +const char* feedtui_get_last_error(const FeedtuiHandle* handle); + +/** + * @brief Get the version string of feedtui. + * + * @return A pointer to a null-terminated UTF-8 string containing the version. + * The returned string is statically allocated and valid for the program's lifetime. + * + * @note Do not free the returned pointer. + */ +const char* feedtui_version(void); + +/** + * @brief Check if feedtui was compiled with a specific feature. + * + * @param feature The feature name to check (null-terminated UTF-8 string). + * + * @return 1 if the feature is enabled, 0 if not, -1 if the feature name is invalid. + * + * @example + * ```c + * if (feedtui_has_feature("ffi") == 1) { + * printf("FFI support is enabled\n"); + * } + * ``` + */ +int feedtui_has_feature(const char* feature); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* FEEDTUI_H */ diff --git a/src/ffi.rs b/src/ffi.rs new file mode 100644 index 0000000..3f37b91 --- /dev/null +++ b/src/ffi.rs @@ -0,0 +1,369 @@ +//! FFI (Foreign Function Interface) module for feedtui +//! +//! This module provides a C-compatible interface for integrating feedtui +//! into C/C++ applications. It exposes lifecycle-style APIs for initializing, +//! running, and shutting down the feedtui TUI application. +//! +//! # Safety +//! +//! All functions in this module that cross the FFI boundary are marked as `unsafe` +//! and must be called with valid parameters. The caller is responsible for ensuring +//! that pointers are valid and that the functions are called in the correct order. +//! +//! # Thread Safety +//! +//! The feedtui application is single-threaded. All FFI functions must be called +//! from the same thread. Calling from multiple threads simultaneously is undefined +//! behavior. +//! +//! # Example (C++) +//! +//! ```cpp +//! #include "feedtui.h" +//! +//! int main() { +//! // Initialize with default config +//! FeedtuiHandle* handle = feedtui_init(nullptr); +//! if (!handle) { +//! fprintf(stderr, "Failed to initialize feedtui\n"); +//! return 1; +//! } +//! +//! // Run the TUI (blocks until user quits) +//! int result = feedtui_run(handle); +//! +//! // Clean up +//! feedtui_shutdown(handle); +//! +//! return result; +//! } +//! ``` + +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_int}; +use std::panic::{self, AssertUnwindSafe}; +use std::path::PathBuf; +use std::ptr; + +use crate::app::App; +use crate::config::Config; + +/// Opaque handle to the feedtui application instance. +/// +/// This struct is opaque to C code - users should only interact with it +/// through the provided functions. +pub struct FeedtuiHandle { + app: Option, + config: Config, + runtime: Option, + last_error: Option, +} + +/// Result codes returned by FFI functions +#[repr(C)] +pub enum FeedtuiResult { + /// Operation completed successfully + Success = 0, + /// Invalid or null handle provided + InvalidHandle = 1, + /// Invalid or null config path + InvalidConfigPath = 2, + /// Failed to load configuration + ConfigLoadError = 3, + /// Failed to initialize runtime + RuntimeError = 4, + /// Application error during execution + AppError = 5, + /// Panic occurred (check last_error for details) + Panic = 6, +} + +/// Initialize a new feedtui instance. +/// +/// # Arguments +/// +/// * `config_path` - Path to the TOML configuration file (UTF-8 encoded, null-terminated). +/// If NULL, uses the default configuration. +/// +/// # Returns +/// +/// A pointer to a `FeedtuiHandle` on success, or NULL on failure. +/// The caller is responsible for calling `feedtui_shutdown` to free the handle. +/// +/// # Safety +/// +/// * `config_path` must be NULL or a valid null-terminated UTF-8 string. +/// * The returned handle must not be used after calling `feedtui_shutdown`. +#[no_mangle] +pub unsafe extern "C" fn feedtui_init(config_path: *const c_char) -> *mut FeedtuiHandle { + let result = panic::catch_unwind(AssertUnwindSafe(|| { + // Load config + let config = if config_path.is_null() { + Config::default() + } else { + let path_str = match CStr::from_ptr(config_path).to_str() { + Ok(s) => s, + Err(_) => return ptr::null_mut(), + }; + let path = PathBuf::from(path_str); + match Config::load(&path) { + Ok(c) => c, + Err(_) => return ptr::null_mut(), + } + }; + + // Create tokio runtime + let runtime = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(_) => return ptr::null_mut(), + }; + + // Create handle + let handle = Box::new(FeedtuiHandle { + app: None, + config, + runtime: Some(runtime), + last_error: None, + }); + + Box::into_raw(handle) + })); + + match result { + Ok(handle) => handle, + Err(_) => ptr::null_mut(), + } +} + +/// Initialize feedtui with a configuration string. +/// +/// # Arguments +/// +/// * `config_toml` - TOML configuration content as a UTF-8 null-terminated string. +/// +/// # Returns +/// +/// A pointer to a `FeedtuiHandle` on success, or NULL on failure. +/// +/// # Safety +/// +/// * `config_toml` must be a valid null-terminated UTF-8 string. +#[no_mangle] +pub unsafe extern "C" fn feedtui_init_with_config(config_toml: *const c_char) -> *mut FeedtuiHandle { + let result = panic::catch_unwind(AssertUnwindSafe(|| { + if config_toml.is_null() { + return ptr::null_mut(); + } + + let config_str = match CStr::from_ptr(config_toml).to_str() { + Ok(s) => s, + Err(_) => return ptr::null_mut(), + }; + + let config: Config = match toml::from_str(config_str) { + Ok(c) => c, + Err(_) => return ptr::null_mut(), + }; + + let runtime = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(_) => return ptr::null_mut(), + }; + + let handle = Box::new(FeedtuiHandle { + app: None, + config, + runtime: Some(runtime), + last_error: None, + }); + + Box::into_raw(handle) + })); + + match result { + Ok(handle) => handle, + Err(_) => ptr::null_mut(), + } +} + +/// Run the feedtui application. +/// +/// This function blocks until the user quits the application (e.g., by pressing 'q'). +/// +/// # Arguments +/// +/// * `handle` - A valid handle obtained from `feedtui_init` or `feedtui_init_with_config`. +/// +/// # Returns +/// +/// * `FeedtuiResult::Success` (0) on successful completion. +/// * Other error codes on failure. +/// +/// # Safety +/// +/// * `handle` must be a valid pointer returned by `feedtui_init` or `feedtui_init_with_config`. +/// * This function must not be called concurrently from multiple threads. +#[no_mangle] +pub unsafe extern "C" fn feedtui_run(handle: *mut FeedtuiHandle) -> c_int { + if handle.is_null() { + return FeedtuiResult::InvalidHandle as c_int; + } + + let result = panic::catch_unwind(AssertUnwindSafe(|| { + let handle = &mut *handle; + + let runtime = match handle.runtime.as_ref() { + Some(rt) => rt, + None => return FeedtuiResult::RuntimeError as c_int, + }; + + // Create and run the app + let mut app = App::new(handle.config.clone()); + + match runtime.block_on(app.run()) { + Ok(_) => FeedtuiResult::Success as c_int, + Err(e) => { + handle.last_error = CString::new(e.to_string()).ok(); + FeedtuiResult::AppError as c_int + } + } + })); + + match result { + Ok(code) => code, + Err(_) => FeedtuiResult::Panic as c_int, + } +} + +/// Shutdown and free the feedtui instance. +/// +/// # Arguments +/// +/// * `handle` - A valid handle obtained from `feedtui_init` or `feedtui_init_with_config`. +/// After this call, the handle is invalid and must not be used. +/// +/// # Safety +/// +/// * `handle` must be a valid pointer returned by `feedtui_init` or `feedtui_init_with_config`, +/// or NULL (in which case this function does nothing). +/// * After calling this function, `handle` must not be used again. +#[no_mangle] +pub unsafe extern "C" fn feedtui_shutdown(handle: *mut FeedtuiHandle) { + if handle.is_null() { + return; + } + + let _ = panic::catch_unwind(AssertUnwindSafe(|| { + let _ = Box::from_raw(handle); + })); +} + +/// Get the last error message. +/// +/// # Arguments +/// +/// * `handle` - A valid handle obtained from `feedtui_init` or `feedtui_init_with_config`. +/// +/// # Returns +/// +/// A pointer to a null-terminated UTF-8 string containing the last error message, +/// or NULL if no error has occurred or if the handle is invalid. +/// +/// The returned string is owned by the handle and remains valid until: +/// - The next FFI function call on this handle +/// - `feedtui_shutdown` is called +/// +/// # Safety +/// +/// * `handle` must be a valid pointer returned by `feedtui_init` or `feedtui_init_with_config`. +#[no_mangle] +pub unsafe extern "C" fn feedtui_get_last_error(handle: *const FeedtuiHandle) -> *const c_char { + if handle.is_null() { + return ptr::null(); + } + + let handle = &*handle; + match &handle.last_error { + Some(err) => err.as_ptr(), + None => ptr::null(), + } +} + +/// Get the version string of feedtui. +/// +/// # Returns +/// +/// A pointer to a null-terminated UTF-8 string containing the version. +/// The returned string is statically allocated and valid for the program's lifetime. +#[no_mangle] +pub extern "C" fn feedtui_version() -> *const c_char { + static VERSION: &[u8] = concat!(env!("CARGO_PKG_VERSION"), "\0").as_bytes(); + VERSION.as_ptr() as *const c_char +} + +/// Check if feedtui was compiled with a specific feature. +/// +/// # Arguments +/// +/// * `feature` - The feature name to check (null-terminated UTF-8 string). +/// +/// # Returns +/// +/// 1 if the feature is enabled, 0 if not, -1 if the feature name is invalid. +/// +/// # Safety +/// +/// * `feature` must be a valid null-terminated UTF-8 string or NULL. +#[no_mangle] +pub unsafe extern "C" fn feedtui_has_feature(feature: *const c_char) -> c_int { + if feature.is_null() { + return -1; + } + + let feature_str = match CStr::from_ptr(feature).to_str() { + Ok(s) => s, + Err(_) => return -1, + }; + + match feature_str { + "ffi" => 1, + _ => 0, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version() { + let version = feedtui_version(); + assert!(!version.is_null()); + let version_str = unsafe { CStr::from_ptr(version) }; + assert!(!version_str.to_str().unwrap().is_empty()); + } + + #[test] + fn test_init_with_null() { + let handle = unsafe { feedtui_init(ptr::null()) }; + assert!(!handle.is_null()); + unsafe { feedtui_shutdown(handle) }; + } + + #[test] + fn test_has_feature() { + assert_eq!(unsafe { feedtui_has_feature(ptr::null()) }, -1); + + let ffi_feature = CString::new("ffi").unwrap(); + assert_eq!(unsafe { feedtui_has_feature(ffi_feature.as_ptr()) }, 1); + + let unknown_feature = CString::new("unknown").unwrap(); + assert_eq!(unsafe { feedtui_has_feature(unknown_feature.as_ptr()) }, 0); + } + + #[test] + fn test_shutdown_null() { + // Should not panic + unsafe { feedtui_shutdown(ptr::null_mut()) }; + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..aad7957 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,47 @@ +//! feedtui - A configurable terminal dashboard for stocks, news, sports, and social feeds +//! +//! This crate provides a terminal-based dashboard (TUI) built with ratatui that displays +//! various feeds including Hacker News, RSS feeds, stock prices, sports scores, and more. +//! +//! # Features +//! +//! - **Hacker News**: View top, new, and best stories +//! - **RSS Feeds**: Aggregate multiple RSS feeds +//! - **Stocks**: Real-time stock quotes (requires API) +//! - **Sports**: Live scores and schedules +//! - **GitHub**: Notifications, PRs, and commits +//! - **YouTube**: Latest videos from channels +//! - **Virtual Pet**: Interactive creature companion +//! +//! # FFI Support +//! +//! When compiled with the `ffi` feature, this crate provides a C-compatible interface +//! for embedding feedtui in C/C++ applications. See the [`ffi`] module for details. +//! +//! # Example (Rust) +//! +//! ```no_run +//! use feedtui::config::Config; +//! use feedtui::app::App; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! let config = Config::default(); +//! let mut app = App::new(config); +//! app.run().await +//! } +//! ``` + +pub mod app; +pub mod config; +pub mod creature; +pub mod event; +pub mod feeds; +pub mod ui; + +#[cfg(feature = "ffi")] +pub mod ffi; + +// Re-export FFI functions at crate root for easier linking +#[cfg(feature = "ffi")] +pub use ffi::*;