11 Commits

30 changed files with 4605 additions and 889 deletions

163
build.sh
View File

@@ -1,49 +1,170 @@
#!/bin/bash
set -e
# nt MUSL static build script (emulates c-relay style)
# Produces build/nt_static_x86_64 on x86_64 hosts.
# nt MUSL static build script
# Produces build/nt_static_x86_64 or build/nt_static_arm64
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
DOCKERFILE="$SCRIPT_DIR/Dockerfile.alpine-musl"
OUTPUT_NAME="nt_static_x86_64"
TARGET_ARCH=""
show_usage() {
echo "nt static builder"
echo ""
echo "USAGE:"
echo " $0 [--arch <x86_64|arm64>]"
echo ""
echo "OPTIONS:"
echo " --arch <arch> Target architecture: x86_64 or arm64"
echo " -h, --help Show this help"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--arch)
if [[ -z "$2" ]]; then
print_error "--arch requires a value: x86_64 or arm64"
exit 1
fi
case "$2" in
x86_64|arm64)
TARGET_ARCH="$2"
;;
*)
print_error "Unsupported architecture '$2'"
print_error "Supported values: x86_64, arm64"
exit 1
;;
esac
shift 2
;;
-h|--help)
show_usage
exit 0
;;
*)
print_error "Unknown argument '$1'"
show_usage
exit 1
;;
esac
done
if ! command -v docker >/dev/null 2>&1; then
echo "[ERROR] docker is required"
print_error "docker is required"
exit 1
fi
if ! docker info >/dev/null 2>&1; then
echo "[ERROR] docker daemon is not running"
print_error "docker daemon is not running"
exit 1
fi
HOST_UNAME="$(uname -m)"
case "$HOST_UNAME" in
x86_64)
HOST_ARCH="x86_64"
;;
aarch64|arm64)
HOST_ARCH="arm64"
;;
*)
HOST_ARCH="unknown"
;;
esac
if [[ -z "$TARGET_ARCH" ]]; then
if [[ "$HOST_ARCH" == "unknown" ]]; then
TARGET_ARCH="x86_64"
print_warning "Unknown host architecture '$HOST_UNAME'; defaulting target arch to x86_64"
else
TARGET_ARCH="$HOST_ARCH"
fi
fi
case "$TARGET_ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_NAME="nt_static_x86_64"
;;
arm64)
PLATFORM="linux/arm64"
OUTPUT_NAME="nt_static_arm64"
;;
*)
print_error "Unsupported target architecture '$TARGET_ARCH'"
print_error "Supported values: x86_64, arm64"
exit 1
;;
esac
if [[ "$HOST_ARCH" != "unknown" && "$HOST_ARCH" != "$TARGET_ARCH" ]]; then
print_status "Cross-building from host '$HOST_ARCH' to target '$TARGET_ARCH'"
if ! docker buildx version >/dev/null 2>&1; then
print_error "docker buildx is required for cross-architecture builds"
print_error "Install/enable buildx + binfmt and retry."
echo "docker run --privileged --rm tonistiigi/binfmt --install arm64" >&2
exit 1
fi
print_success "docker buildx is available"
if ! docker run --rm --platform "$PLATFORM" alpine:3.19 /bin/true >/dev/null 2>&1; then
print_error "Cross-architecture execution is not available (binfmt/QEMU not configured)"
print_error "Enable binfmt support and retry:"
echo "docker run --privileged --rm tonistiigi/binfmt --install arm64" >&2
exit 1
fi
fi
mkdir -p "$BUILD_DIR"
echo "[INFO] Building MUSL static binary via Docker (output stage)..."
docker build \
--platform linux/amd64 \
-f "$DOCKERFILE" \
-t nt-musl-output:latest \
.
print_status "Building MUSL static binary for $TARGET_ARCH ($PLATFORM)..."
(
cd "$SCRIPT_DIR"
docker build \
--platform "$PLATFORM" \
-f Dockerfile.alpine-musl \
-t nt-musl-output:latest \
.
)
echo "[INFO] Extracting binary..."
print_status "Extracting /nt_static from image..."
CONTAINER_ID=$(docker create nt-musl-output:latest /nt_static)
if ! docker cp "$CONTAINER_ID:/nt_static" "$BUILD_DIR/$OUTPUT_NAME"; then
echo "[ERROR] Could not copy /nt_static from output image"
print_error "Could not copy /nt_static from output image"
docker rm "$CONTAINER_ID" >/dev/null || true
exit 1
fi
docker rm "$CONTAINER_ID" >/dev/null
docker rm "$CONTAINER_ID" >/dev/null || true
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
echo "[INFO] Verifying static link..."
if ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 | grep -q "not a dynamic executable\|statically linked"; then
echo "[SUCCESS] Binary is static: $BUILD_DIR/$OUTPUT_NAME"
else
echo "[WARNING] ldd output unexpected:"
ldd "$BUILD_DIR/$OUTPUT_NAME" || true
print_status "Verifying binary with ldd/file"
LDD_OK=false
LDD_OUTPUT=$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)
echo "$LDD_OUTPUT" >&2
if echo "$LDD_OUTPUT" | grep -q "not a dynamic executable\|statically linked"; then
LDD_OK=true
fi
echo "[DONE] $BUILD_DIR/$OUTPUT_NAME"
FILE_OUTPUT=$(file "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)
echo "$FILE_OUTPUT" >&2
if [[ "$LDD_OK" == true ]] || echo "$FILE_OUTPUT" | grep -q "statically linked"; then
print_success "Binary verified as static: $BUILD_DIR/$OUTPUT_NAME"
else
print_warning "Could not conclusively verify static linking"
fi
print_success "Done: $BUILD_DIR/$OUTPUT_NAME"

26
include/nsigner_auth.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef NSIGNER_AUTH_H
#define NSIGNER_AUTH_H
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include <time.h>
typedef struct {
int enabled;
unsigned char privkey[32];
char label[64];
} nt_nsigner_auth_ctx_t;
/*
* Build a TCP auth envelope event (kind 27235) for an n_signer JSON-RPC request.
* Caller owns *out_auth and must cJSON_Delete() it.
* Returns 0 on success.
*/
int nt_nsigner_auth_build(const char *request_id,
const char *method,
const cJSON *params,
const nt_nsigner_auth_ctx_t *auth_ctx,
time_t created_at,
cJSON **out_auth);
#endif /* NSIGNER_AUTH_H */

199
include/nsigner_client.h Normal file
View File

@@ -0,0 +1,199 @@
#ifndef NSIGNER_CLIENT_H
#define NSIGNER_CLIENT_H
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "nsigner_auth.h"
/* Selector for key derivation/role in n_signer requests. */
typedef struct {
int has_nostr_index;
int nostr_index;
char role[64];
} nt_nsigner_selector_t;
static inline nt_nsigner_selector_t nsigner_selector_default(void) {
nt_nsigner_selector_t s;
s.has_nostr_index = 1;
s.nostr_index = 0;
s.role[0] = '\0';
return s;
}
/* Error codes */
#define NT_NSIGNER_OK 0
#define NT_NSIGNER_E_IO -1
#define NT_NSIGNER_E_DENIED -2
#define NT_NSIGNER_E_UNAUTH -3
#define NT_NSIGNER_E_PARSE -4
#define NT_NSIGNER_E_ERROR -5
#define NT_NSIGNER_E_AUTH -6
/*
* Enumerate running nsigner instances by reading /proc/net/unix.
* Returns 0 on success. Caller must free each name and the array.
* Names are returned WITHOUT the leading '@'.
*/
int nsigner_list(char ***names_out, int *count_out);
/*
* Connect to an nsigner abstract namespace socket.
* Returns file descriptor >= 0 on success, -1 on failure.
* The name should NOT include the leading '@'.
*/
int nsigner_connect(const char *name);
/*
* Send a JSON-RPC request and receive the response.
* Uses 4-byte big-endian length-prefixed framing.
* read_timeout_ms: timeout for reading response (use 60000 for prompt-requiring methods).
* Returns NT_NSIGNER_OK on success with response in *response_out (caller must cJSON_Delete).
* On error returns one of the NT_NSIGNER_E_* codes.
*/
int nsigner_rpc(int fd, const cJSON *request, cJSON **response_out, int read_timeout_ms);
/*
* High-level helpers (each opens a connection, sends RPC, closes).
* socket_name: without '@' prefix.
* role: the role selector string (e.g. "main").
*/
int nsigner_get_public_key(const char *socket_name, const nt_nsigner_selector_t *selector, char *out_hex_65);
int nsigner_sign_event(const char *socket_name, const nt_nsigner_selector_t *selector,
const char *unsigned_event_json, char **signed_event_json_out);
int nsigner_nip04_encrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
int nsigner_nip04_decrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
int nsigner_nip44_encrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
int nsigner_nip44_decrypt(const char *socket_name, const nt_nsigner_selector_t *selector,
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
/*
* URL helpers for FIPS/TCP transport.
* URL format: http://host:port (path/query ignored if present).
*/
int nsigner_get_public_key_url(const char *url, const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth, char *out_hex_65);
int nsigner_sign_event_url(const char *url, const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *unsigned_event_json, char **signed_event_json_out);
int nsigner_nip04_encrypt_url(const char *url, const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
int nsigner_nip04_decrypt_url(const char *url, const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
int nsigner_nip44_encrypt_url(const char *url, const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex, const char *plaintext, char **cipher_out);
int nsigner_nip44_decrypt_url(const char *url, const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex, const char *ciphertext, char **plain_out);
/*
* Persistent URL session helpers.
* Open once and reuse the fd for all RPC calls to keep a stable TCP source port.
*/
int nsigner_session_open_url(const char *url);
void nsigner_session_close(int fd);
int nsigner_session_get_public_key(int *fd_inout,
const char *url,
const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
char *out_hex_65);
int nsigner_session_sign_event(int *fd_inout,
const char *url,
const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *unsigned_event_json,
char **signed_event_json_out);
int nsigner_session_nip04_encrypt(int *fd_inout,
const char *url,
const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex,
const char *plaintext,
char **cipher_out);
int nsigner_session_nip04_decrypt(int *fd_inout,
const char *url,
const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex,
const char *ciphertext,
char **plain_out);
int nsigner_session_nip44_encrypt(int *fd_inout,
const char *url,
const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex,
const char *plaintext,
char **cipher_out);
int nsigner_session_nip44_decrypt(int *fd_inout,
const char *url,
const nt_nsigner_selector_t *selector,
const nt_nsigner_auth_ctx_t *auth,
const char *peer_pub_hex,
const char *ciphertext,
char **plain_out);
/*
* qrexec helpers for cross-qube n_signer transport.
* target_qube: destination signer qube (e.g. "nsigner-vault").
* service_name: qrexec service name (e.g. "qubes.NsignerRpc").
*/
int nsigner_qrexec_get_public_key(const char *target_qube,
const char *service_name,
const nt_nsigner_selector_t *selector,
char *out_hex_65);
int nsigner_qrexec_sign_event(const char *target_qube,
const char *service_name,
const nt_nsigner_selector_t *selector,
const char *unsigned_event_json,
char **signed_event_json_out);
int nsigner_qrexec_nip04_encrypt(const char *target_qube,
const char *service_name,
const nt_nsigner_selector_t *selector,
const char *peer_pub_hex,
const char *plaintext,
char **cipher_out);
int nsigner_qrexec_nip04_decrypt(const char *target_qube,
const char *service_name,
const nt_nsigner_selector_t *selector,
const char *peer_pub_hex,
const char *ciphertext,
char **plain_out);
int nsigner_qrexec_nip44_encrypt(const char *target_qube,
const char *service_name,
const nt_nsigner_selector_t *selector,
const char *peer_pub_hex,
const char *plaintext,
char **cipher_out);
int nsigner_qrexec_nip44_decrypt(const char *target_qube,
const char *service_name,
const nt_nsigner_selector_t *selector,
const char *peer_pub_hex,
const char *ciphertext,
char **plain_out);
#endif /* NSIGNER_CLIENT_H */

65
include/signer.h Normal file
View File

@@ -0,0 +1,65 @@
#ifndef SIGNER_H
#define SIGNER_H
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "nsigner_client.h"
#include <time.h>
#include <stdint.h>
typedef enum {
NT_SIGNER_LOCAL = 0,
NT_SIGNER_NSIGNER = 1,
NT_SIGNER_NSIGNER_QREXEC = 2,
NT_SIGNER_NSIGNER_URL = 3
} nt_signer_kind_t;
typedef struct {
nt_signer_kind_t kind;
char socket_name[128];
char qrexec_target[128];
char qrexec_service[128];
char endpoint_url[256];
int url_session_fd;
nt_nsigner_selector_t selector;
nt_nsigner_auth_ctx_t url_auth;
} nt_signer_t;
extern nt_signer_t g_signer;
void signer_init(void);
void signer_shutdown(void);
int signer_init_local(void);
void signer_set_selector(int has_index, int index, const char *role);
int signer_init_nsigner(const char *socket_name, const nt_nsigner_selector_t *selector);
int signer_init_nsigner_qrexec(const char *target_qube, const char *service_name, const nt_nsigner_selector_t *selector);
int signer_init_nsigner_url(const char *endpoint_url, const nt_nsigner_selector_t *selector);
int signer_init_nsigner_url_with_session(const char *endpoint_url,
const nt_nsigner_selector_t *selector,
int session_fd);
int signer_init_nsigner_url_with_session_auth(const char *endpoint_url,
const nt_nsigner_selector_t *selector,
int session_fd,
const nt_nsigner_auth_ctx_t *auth_or_null);
int signer_create_and_sign(int kind,
const char *content,
cJSON *tags,
time_t timestamp,
char **signed_event_json_out);
int signer_nip04_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out);
int signer_nip04_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out);
int signer_nip44_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out);
int signer_nip44_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out);
int signer_nip44_self_encrypt(const char *plaintext, char **cipher_out);
int signer_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext, char **plain_out);
int signer_sign_event_json(const char *unsigned_event_json, char **signed_event_json_out);
/* Returns 0 on success. Only works in NT_SIGNER_LOCAL mode; returns -1 for NT_SIGNER_NSIGNER. */
int signer_get_local_private_key(unsigned char out_priv[32]);
#endif /* SIGNER_H */

View File

@@ -16,8 +16,7 @@ typedef struct {
char *kind10002_json;
char *kind10096_json;
char *kind17375_json;
char **kind30078_events;
int kind30078_count;
char *user_settings_json;
const char *bootstrap_relays[8];
int bootstrap_relay_count;
@@ -39,4 +38,7 @@ void state_parse_relay_list(void);
const char **state_get_read_relays(int *count);
const char **state_get_write_relays(int *count);
int state_nip44_self_encrypt(const char *plaintext, char **cipher_out);
char *state_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext);
#endif /* STATE_H */

View File

@@ -1,18 +1,58 @@
#ifndef TUI_H
#define TUI_H
#define TUI_KEY_RESIZE -2
typedef struct {
const char *breadcrumb;
const char *title_suffix;
} tui_view_t;
/* Initialize terminal input mode (non-canonical, no echo). */
void tui_init(void);
/* Restore terminal settings to their original state. */
void tui_cleanup(void);
/* Install SIGWINCH handler for resize-aware redraw loops. */
void tui_install_resize_handler(void);
/* Consume pending resize flag (returns 1 if a resize was pending). */
int tui_consume_resize_flag(void);
/* Set app title used by anchored frame renderers. */
void tui_set_app_title(const char *title);
/* Clear the screen area by printing terminal-height blank lines. */
void tui_clear_screen(void);
/* Print formatted output and render ^_...^: hotkey highlights. */
void tui_print(const char *fmt, ...);
/* Print full-width horizontal separator using '='. */
void tui_print_hr(void);
/* Print centered text in the current terminal width. */
void tui_print_centered(const char *text);
/* Render anchored top frame (separator/title/separator/breadcrumb + blank lines). */
void tui_render_top_frame(const tui_view_t *view);
/* Column where centered menu block should start. */
int tui_centered_menu_start_col(void);
/* Print one menu line aligned to centered menu start. */
void tui_print_menu_item(const char *fmt, ...);
/* Begin canonical continuous frame and reset line counting. */
void tui_begin_frame(const tui_view_t *view);
/* End frame with anchored prompt near bottom, then read line. */
int tui_end_frame_with_prompt(const char *prompt, char *buf, int bufsize);
/* Render startup splash screen and wait for Enter. */
void tui_show_splash(void);
/* Prompt and read a blocking input line into buf, returns length. */
int tui_get_line(const char *prompt, char *buf, int bufsize);

View File

@@ -18,6 +18,7 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
COMMIT_MESSAGE=""
VERSION_INCREMENT_TYPE="patch" # patch|minor|major
RELEASE_MODE=false
show_usage() {
echo "nt increment and push"
@@ -29,7 +30,21 @@ show_usage() {
echo " -p, --patch Increment patch version (default)"
echo " -m, --minor Increment minor version"
echo " -M, --major Increment major version"
echo " -r, --release After bump+push, build/upload release assets to Gitea"
echo " -h, --help Show help"
echo ""
echo "EXAMPLES:"
echo " $0 \"Fix relay reconnect bug\""
echo " $0 -m \"Add profile editing\""
echo " $0 -r -p \"Release patch build\""
echo " $0 -r -m \"Release minor build\""
echo ""
echo "RELEASE MODE (-r):"
echo " 1) Normal bump + commit + tag + push runs first"
echo " 2) Build x86_64 static binary (required)"
echo " 3) Build arm64 static binary (warning only on failure)"
echo " 4) Create source tarball"
echo " 5) Create/upload Gitea release assets if ~/.gitea_token exists"
}
while [[ $# -gt 0 ]]; do
@@ -46,6 +61,10 @@ while [[ $# -gt 0 ]]; do
VERSION_INCREMENT_TYPE="major"
shift
;;
-r|--release)
RELEASE_MODE=true
shift
;;
-h|--help)
show_usage
exit 0
@@ -71,77 +90,289 @@ if ! git rev-parse --git-dir > /dev/null 2>&1; then
exit 1
fi
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || true)
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.0.0"
print_warning "No version tags found, starting from $LATEST_TAG"
fi
get_and_increment_version() {
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || true)
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.0.0"
print_warning "No version tags found, starting from $LATEST_TAG"
fi
VERSION=${LATEST_TAG#v}
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
else
print_error "Invalid version format in tag: $LATEST_TAG"
exit 1
fi
VERSION=${LATEST_TAG#v}
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
else
print_error "Invalid version format in tag: $LATEST_TAG"
exit 1
fi
case "$VERSION_INCREMENT_TYPE" in
major)
NEW_MAJOR=$((MAJOR + 1))
NEW_MINOR=0
NEW_PATCH=0
;;
minor)
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
;;
*)
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
;;
esac
case "$VERSION_INCREMENT_TYPE" in
major)
NEW_MAJOR=$((MAJOR + 1))
NEW_MINOR=0
NEW_PATCH=0
;;
minor)
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
;;
*)
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
;;
esac
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
}
if [[ ! -f "src/main.c" ]]; then
print_error "src/main.c not found"
exit 1
fi
update_version_macros() {
if [[ ! -f "src/main.c" ]]; then
print_error "src/main.c not found"
exit 1
fi
print_status "Updating version macros in src/main.c"
sed -i "s/^#define NT_VERSION_MAJOR .*/#define NT_VERSION_MAJOR ${NEW_MAJOR}/" src/main.c
sed -i "s/^#define NT_VERSION_MINOR .*/#define NT_VERSION_MINOR ${NEW_MINOR}/" src/main.c
sed -i "s/^#define NT_VERSION_PATCH .*/#define NT_VERSION_PATCH ${NEW_PATCH}/" src/main.c
sed -i "s/^#define NT_VERSION \".*\"/#define NT_VERSION \"${NEW_VERSION}\"/" src/main.c
print_status "Updating version macros in src/main.c"
sed -i "s/^#define NT_VERSION_MAJOR .*/#define NT_VERSION_MAJOR ${NEW_MAJOR}/" src/main.c
sed -i "s/^#define NT_VERSION_MINOR .*/#define NT_VERSION_MINOR ${NEW_MINOR}/" src/main.c
sed -i "s/^#define NT_VERSION_PATCH .*/#define NT_VERSION_PATCH ${NEW_PATCH}/" src/main.c
sed -i "s/^#define NT_VERSION \".*\"/#define NT_VERSION \"${NEW_VERSION}\"/" src/main.c
print_success "Updated version in src/main.c to $NEW_VERSION"
print_success "Updated version in src/main.c to $NEW_VERSION"
}
print_status "Staging changes"
git add src/main.c CMakeLists.txt build.sh increment_and_push.sh README.md plans/planning.md include/*.h src/*.c .test_mnemonic 2>/dev/null || git add .
git_commit_tag_push() {
print_status "Staging changes"
git add src/main.c CMakeLists.txt build.sh increment_and_push.sh README.md plans/planning.md include/*.h src/*.c .test_mnemonic 2>/dev/null || git add .
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
git commit -m "$NEW_VERSION - $COMMIT_MESSAGE"
print_success "Committed changes"
fi
if git diff --staged --quiet; then
print_warning "No changes to commit"
else
git commit -m "$NEW_VERSION - $COMMIT_MESSAGE"
print_success "Committed changes"
fi
if git rev-parse "$NEW_VERSION" >/dev/null 2>&1; then
print_warning "Tag $NEW_VERSION already exists locally; recreating"
git tag -d "$NEW_VERSION" >/dev/null 2>&1 || true
fi
if git rev-parse "$NEW_VERSION" >/dev/null 2>&1; then
print_warning "Tag $NEW_VERSION already exists locally; recreating"
git tag -d "$NEW_VERSION" >/dev/null 2>&1 || true
fi
git tag "$NEW_VERSION"
print_success "Created tag: $NEW_VERSION"
git tag "$NEW_VERSION"
print_success "Created tag: $NEW_VERSION"
print_status "Pushing commit and tag"
git push
git push origin "$NEW_VERSION"
print_status "Pushing commit and tag"
git push
git push origin "$NEW_VERSION"
print_success "Done. Pushed $NEW_VERSION"
print_success "Pushed $NEW_VERSION"
}
build_release_binary() {
print_status "Building release binaries"
if [[ ! -x "./build.sh" ]]; then
print_error "./build.sh not found or not executable"
return 1
fi
if ./build.sh; then
print_success "Built x86_64 static binary"
else
print_error "x86_64 build failed; aborting release mode"
return 1
fi
if ./build.sh --arch arm64; then
print_success "Built arm64 static binary"
else
print_warning "arm64 build failed (continuing release with available assets)"
fi
return 0
}
create_source_tarball() {
local tarball_name="nostr_terminal-${NEW_VERSION#v}.tar.gz"
print_status "Creating source tarball: $tarball_name"
if tar -czf "$tarball_name" \
--exclude='build/*' \
--exclude='.git*' \
--exclude='*.db' \
--exclude='*.db-*' \
--exclude='*.log' \
--exclude='*.tar.gz' \
.; then
print_success "Created source tarball: $tarball_name"
echo "$tarball_name"
return 0
else
print_error "Failed to create source tarball"
return 1
fi
}
create_gitea_release() {
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No $HOME/.gitea_token found. Skipping release creation/upload."
return 0
fi
local token
token=$(tr -d '\n\r' < "$HOME/.gitea_token")
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/nostr_terminal"
print_status "Creating Gitea release: $NEW_VERSION"
local response
response=$(curl -s -X POST "$api_url/releases" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
if echo "$response" | grep -q '"id"'; then
local release_id
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
print_success "Created release $NEW_VERSION (id=$release_id)"
echo "$release_id"
return 0
fi
if echo "$response" | grep -qi "already exists"; then
print_warning "Release $NEW_VERSION already exists; resolving release id"
local existing
existing=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
if echo "$existing" | grep -q '"id"'; then
local release_id
release_id=$(echo "$existing" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
print_status "Using existing release id=$release_id"
echo "$release_id"
return 0
fi
print_error "Could not resolve existing release ID for $NEW_VERSION"
return 1
fi
print_error "Failed to create release"
print_error "Response: $response"
return 1
}
upload_one_asset_with_retry() {
local assets_url="$1"
local token="$2"
local file_path="$3"
local max_attempts=3
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
print_status "Uploading $(basename "$file_path") (attempt $attempt/$max_attempts)"
local response
response=$(curl -s -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$file_path;filename=$(basename "$file_path")")
if echo "$response" | grep -q '"id"'; then
print_success "Uploaded $(basename "$file_path")"
return 0
fi
if [[ $attempt -lt $max_attempts ]]; then
print_warning "Upload attempt failed for $(basename "$file_path"), retrying..."
sleep 2
else
print_error "Failed to upload $(basename "$file_path") after $max_attempts attempts"
print_error "Response: $response"
fi
attempt=$((attempt + 1))
done
return 1
}
upload_release_assets() {
local release_id="$1"
local tarball_path="$2"
if [[ ! -f "$HOME/.gitea_token" ]]; then
print_warning "No $HOME/.gitea_token found. Skipping asset uploads."
return 0
fi
local token
token=$(tr -d '\n\r' < "$HOME/.gitea_token")
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/nostr_terminal"
local assets_url="$api_url/releases/$release_id/assets"
if [[ -f "build/nt_static_x86_64" ]]; then
upload_one_asset_with_retry "$assets_url" "$token" "build/nt_static_x86_64" || true
else
print_warning "Asset missing: build/nt_static_x86_64"
fi
if [[ -f "build/nt_static_arm64" ]]; then
upload_one_asset_with_retry "$assets_url" "$token" "build/nt_static_arm64" || true
else
print_warning "Asset missing: build/nt_static_arm64 (skipping)"
fi
if [[ -n "$tarball_path" && -f "$tarball_path" ]]; then
print_status "Uploading $(basename "$tarball_path")"
local response
response=$(curl -s -X POST "$assets_url" \
-H "Authorization: token $token" \
-F "attachment=@$tarball_path;filename=$(basename "$tarball_path")")
if echo "$response" | grep -q '"id"'; then
print_success "Uploaded $(basename "$tarball_path")"
else
print_warning "Failed to upload $(basename "$tarball_path"): $response"
fi
else
print_warning "Tarball missing, skipping upload"
fi
}
main() {
get_and_increment_version
update_version_macros
git_commit_tag_push
if [[ "$RELEASE_MODE" == true ]]; then
print_status "Release mode enabled"
if ! build_release_binary; then
print_error "Release mode aborted due to required x86_64 build failure"
exit 1
fi
local tarball_path=""
if tarball_path=$(create_source_tarball); then
:
else
print_warning "Proceeding without source tarball"
fi
local release_id=""
if release_id=$(create_gitea_release); then
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
upload_release_assets "$release_id" "$tarball_path"
print_success "Release workflow finished for $NEW_VERSION"
else
print_warning "Release creation/upload skipped (no release id)"
fi
else
print_error "Release creation failed"
exit 1
fi
fi
print_success "Done. Pushed $NEW_VERSION"
}
main

View File

@@ -179,7 +179,10 @@ int db_open(void) {
return -1;
}
snprintf(db_path, sizeof(db_path), "%s/nostr.db", dir_path);
if (snprintf(db_path, sizeof(db_path), "%s/nostr.db", dir_path) >= (int)sizeof(db_path)) {
fprintf(stderr, "db_open: database path too long\n");
return -1;
}
rc = sqlite3_open(db_path, &g_db);
if (rc != SQLITE_OK) {
fprintf(stderr, "db_open: sqlite open failed: %s\n", sqlite3_errmsg(g_db));

View File

@@ -3,6 +3,7 @@
#include "tui.h"
#include "../resources/nostr_core_lib/nostr_core/nostr_common.h"
#include "signer.h"
/*
* Canonical version is the latest git tag.
@@ -10,8 +11,8 @@
*/
#define NT_VERSION_MAJOR 0
#define NT_VERSION_MINOR 0
#define NT_VERSION_PATCH 2
#define NT_VERSION "v0.0.2"
#define NT_VERSION_PATCH 13
#define NT_VERSION "v0.0.13"
#include <stdio.h>
#include <stdlib.h>
@@ -33,10 +34,13 @@ void menu_ai(void);
void menu_ecash(void);
static void menu_show_loaded_events(void) {
int i;
static const tui_view_t view = {
"> Main Menu > Loaded Events",
NULL,
};
char input[8];
tui_clear_screen();
tui_print("LOADED EVENTS\n");
tui_begin_frame(&view);
tui_print("KIND 0 (metadata content):");
tui_print("%s", g_state.kind0_json ? g_state.kind0_json : "(not loaded)");
@@ -58,47 +62,48 @@ static void menu_show_loaded_events(void) {
tui_print("%s", g_state.kind17375_json ? g_state.kind17375_json : "(not loaded)");
tui_print("");
tui_print("KIND 30078 events loaded: %d", g_state.kind30078_count);
for (i = 0; i < g_state.kind30078_count; i++) {
tui_print("[%d] %s", i + 1, g_state.kind30078_events[i] ? g_state.kind30078_events[i] : "(null)");
}
tui_print("USER-SETTINGS (kind 30078, d=user-settings):");
tui_print("%s", g_state.user_settings_json ? g_state.user_settings_json : "(not loaded)");
tui_print("");
tui_print("\nPress Enter to return.");
{
char input[8];
tui_get_line(">", input, (int)sizeof(input));
}
(void)tui_end_frame_with_prompt("Press Enter to return >", input, (int)sizeof(input));
}
void menu_main(void) {
static const tui_view_t main_view = {
"> Main Menu",
NULL,
};
char input[64];
while (1) {
tui_clear_screen();
tui_print("NOSTR TERMINAL %s - %s\n", g_state.version, g_state.user_name);
(void)tui_consume_resize_flag();
tui_begin_frame(&main_view);
tui_print("User: %s", g_state.user_name);
tui_print("");
if (!g_state.logged_in) {
tui_print("^_L^:og in");
tui_print("^_Q^:uit.");
tui_print_menu_item("^_L^:og in");
tui_print_menu_item("^_Q^:uit");
} else {
tui_print("^_W^:rite");
tui_print("^_T^:weet");
tui_print("^_P^:rofile");
tui_print("^_R^:elays");
tui_print("^_F^:ollows");
tui_print("^_K^:ind/event dump");
tui_print("^_N^:otifications");
tui_print("P^_o^:sts");
tui_print("Li^_v^:e feeds");
tui_print("Direct ^_m^:essage");
tui_print("To^_d^:o");
tui_print("D^_i^:ary");
tui_print("^_A^:i");
tui_print("^_E^:cash");
tui_print("^_Q^:uit.");
tui_print_menu_item("^_W^:rite");
tui_print_menu_item("^_T^:weet");
tui_print_menu_item("^_P^:rofile");
tui_print_menu_item("^_R^:elays");
tui_print_menu_item("^_F^:ollows");
tui_print_menu_item("^_K^:ind/event dump");
tui_print_menu_item("^_N^:otifications");
tui_print_menu_item("^_B^:logs/posts");
tui_print_menu_item("^_L^:ive feeds");
tui_print_menu_item("^_M^:essage");
tui_print_menu_item("To^_d^:o");
tui_print_menu_item("^_J^:ournal");
tui_print_menu_item("^_A^:i");
tui_print_menu_item("^_E^:cash");
tui_print_menu_item("^_Q^:uit");
}
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'q' || input[0] == 'Q' || input[0] == 'x' || input[0] == 'X') {
tui_print("\nHasta luego.\n");
@@ -113,10 +118,6 @@ void menu_main(void) {
}
switch (input[0]) {
case 'l':
case 'L':
menu_login();
break;
case 'w':
case 'W':
menu_write();
@@ -145,10 +146,14 @@ void menu_main(void) {
case 'K':
menu_show_loaded_events();
break;
case 'b':
case 'B':
case 'o':
case 'O':
menu_posts();
break;
case 'l':
case 'L':
case 'v':
case 'V':
menu_live();
@@ -161,6 +166,8 @@ void menu_main(void) {
case 'D':
menu_todo();
break;
case 'j':
case 'J':
case 'i':
case 'I':
menu_diary();
@@ -194,9 +201,15 @@ int main(int argc, char *argv[]) {
}
tui_init();
tui_install_resize_handler();
tui_set_window_title("Nostr Terminal");
state_init();
snprintf(g_state.version, sizeof(g_state.version), "%s", NT_VERSION);
{
char app_title[64];
snprintf(app_title, sizeof(app_title), "NOSTR TERMINAL %s", g_state.version);
tui_set_app_title(app_title);
}
if (nostr_init() != 0) {
fprintf(stderr, "Failed to initialize nostr library.\n");
@@ -206,9 +219,11 @@ int main(int argc, char *argv[]) {
return 1;
}
tui_show_splash();
menu_main();
nostr_cleanup();
signer_shutdown();
state_cleanup();
tui_cleanup();
db_close();

View File

@@ -1,16 +1,14 @@
#include "tui.h"
#include "state.h"
#include "net.h"
#include "publish.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
typedef struct {
@@ -80,6 +78,99 @@ static void ai_prefs_free(ai_prefs_t *prefs) {
memset(prefs, 0, sizeof(*prefs));
}
static int ai_ends_with_chat_completions(const char *s) {
static const char *suffix = "/chat/completions";
size_t s_len;
size_t suffix_len;
if (!s) {
return 0;
}
s_len = strlen(s);
suffix_len = strlen(suffix);
if (s_len < suffix_len) {
return 0;
}
return strcmp(s + (s_len - suffix_len), suffix) == 0;
}
static char *ai_join_chat_completions(const char *base_url) {
size_t base_len;
int needs_slash;
char *out;
if (!base_url || base_url[0] == '\0') {
return ai_strdup("https://api.venice.ai/api/v1/chat/completions");
}
if (ai_ends_with_chat_completions(base_url)) {
return ai_strdup(base_url);
}
base_len = strlen(base_url);
needs_slash = (base_len > 0U && base_url[base_len - 1U] != '/');
out = (char *)malloc(base_len + (needs_slash ? 1U : 0U) + strlen("chat/completions") + 1U);
if (!out) {
return NULL;
}
snprintf(out,
base_len + (needs_slash ? 1U : 0U) + strlen("chat/completions") + 1U,
"%s%schat/completions",
base_url,
needs_slash ? "/" : "");
return out;
}
static char *ai_base_url_from_endpoint(const char *endpoint) {
static const char *suffix = "/chat/completions";
size_t endpoint_len;
size_t suffix_len;
char *out;
if (!endpoint || endpoint[0] == '\0') {
return ai_strdup("");
}
endpoint_len = strlen(endpoint);
suffix_len = strlen(suffix);
if (endpoint_len >= suffix_len &&
strcmp(endpoint + (endpoint_len - suffix_len), suffix) == 0) {
out = (char *)malloc(endpoint_len - suffix_len + 1U);
if (!out) {
return NULL;
}
memcpy(out, endpoint, endpoint_len - suffix_len);
out[endpoint_len - suffix_len] = '\0';
return out;
}
return ai_strdup(endpoint);
}
static cJSON *ai_make_d_tag(const char *dval) {
cJSON *tags = cJSON_CreateArray();
cJSON *d_tag;
if (!tags) {
return NULL;
}
d_tag = cJSON_CreateArray();
if (!d_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
cJSON_AddItemToArray(d_tag, cJSON_CreateString(dval ? dval : ""));
cJSON_AddItemToArray(tags, d_tag);
return tags;
}
static size_t nt_http_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) {
nt_http_buffer_t *buf = (nt_http_buffer_t *)userdata;
size_t total = size * nmemb;
@@ -142,156 +233,37 @@ static void nt_configure_curl_tls(CURL *curl) {
}
}
static int ai_encrypt_for_self(const char *plaintext, char **cipher_out) {
unsigned char priv[32];
unsigned char pub[32];
char *out;
size_t out_sz;
if (!plaintext || !cipher_out) {
return -1;
}
*cipher_out = NULL;
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return -1;
}
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
return -1;
}
out_sz = (strlen(plaintext) * 4U) + 256U;
out = (char *)malloc(out_sz);
if (!out) {
return -1;
}
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
free(out);
return -1;
}
*cipher_out = out;
return 0;
}
static char *ai_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
unsigned char priv[32];
unsigned char sender_pub[32];
size_t out_sz;
char *out;
if (!sender_pub_hex || !ciphertext) {
return NULL;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return NULL;
}
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
return NULL;
}
out_sz = strlen(ciphertext) + 4096U;
out = (char *)malloc(out_sz);
if (!out) {
return NULL;
}
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
free(out);
return NULL;
}
return out;
}
static cJSON *ai_make_d_tag(const char *dval) {
cJSON *tags = cJSON_CreateArray();
cJSON *d_tag;
if (!tags) {
return NULL;
}
d_tag = cJSON_CreateArray();
if (!d_tag) {
cJSON_Delete(tags);
return NULL;
}
cJSON_AddItemToArray(d_tag, cJSON_CreateString("d"));
cJSON_AddItemToArray(d_tag, cJSON_CreateString(dval ? dval : ""));
cJSON_AddItemToArray(tags, d_tag);
return tags;
}
static int ai_load_prefs(ai_prefs_t *prefs) {
const char **read_relays;
int relay_count = 0;
char filter[256];
char *event_json = NULL;
cJSON *event;
cJSON *content_item;
cJSON *pubkey_item;
char *plain;
cJSON *root;
cJSON *endpoint;
cJSON *llm;
cJSON *api_key;
cJSON *model;
cJSON *base_url;
cJSON *legacy_endpoint;
char *endpoint;
if (!prefs) {
if (!prefs || !g_state.user_settings_json) {
return -1;
}
read_relays = state_get_read_relays(&relay_count);
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"#d\":[\"prefs\"],\"limit\":1}",
g_state.npub_hex);
if (nt_get_first(filter, read_relays, relay_count, 7000, &event_json) != 0) {
return -1;
}
if (!event_json) {
return 0;
}
event = cJSON_Parse(event_json);
free(event_json);
if (!event) {
return -1;
}
content_item = cJSON_GetObjectItemCaseSensitive(event, "content");
pubkey_item = cJSON_GetObjectItemCaseSensitive(event, "pubkey");
if (!cJSON_IsString(content_item) || !content_item->valuestring ||
!cJSON_IsString(pubkey_item) || !pubkey_item->valuestring) {
cJSON_Delete(event);
return -1;
}
plain = ai_decrypt_from_pub(pubkey_item->valuestring, content_item->valuestring);
cJSON_Delete(event);
if (!plain) {
return -1;
}
root = cJSON_Parse(plain);
free(plain);
root = cJSON_Parse(g_state.user_settings_json);
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return -1;
}
endpoint = cJSON_GetObjectItemCaseSensitive(root, "ai_endpoint");
api_key = cJSON_GetObjectItemCaseSensitive(root, "ai_api_key");
model = cJSON_GetObjectItemCaseSensitive(root, "ai_model");
if (cJSON_IsString(endpoint) && endpoint->valuestring) {
ai_set_string(&prefs->endpoint, endpoint->valuestring);
llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
if (!cJSON_IsObject(llm)) {
cJSON_Delete(root);
return -1;
}
api_key = cJSON_GetObjectItemCaseSensitive(llm, "api_key");
model = cJSON_GetObjectItemCaseSensitive(llm, "model");
base_url = cJSON_GetObjectItemCaseSensitive(llm, "base_url");
legacy_endpoint = cJSON_GetObjectItemCaseSensitive(llm, "endpoint");
if (cJSON_IsString(api_key) && api_key->valuestring) {
ai_set_string(&prefs->api_key, api_key->valuestring);
}
@@ -299,51 +271,106 @@ static int ai_load_prefs(ai_prefs_t *prefs) {
ai_set_string(&prefs->model, model->valuestring);
}
endpoint = NULL;
if (cJSON_IsString(base_url) && base_url->valuestring) {
endpoint = ai_join_chat_completions(base_url->valuestring);
} else if (cJSON_IsString(legacy_endpoint) && legacy_endpoint->valuestring) {
endpoint = ai_strdup(legacy_endpoint->valuestring);
}
if (endpoint) {
ai_set_string(&prefs->endpoint, endpoint);
free(endpoint);
}
cJSON_Delete(root);
return 0;
}
static int ai_save_prefs(const ai_prefs_t *prefs) {
cJSON *root;
char *json;
char *cipher = NULL;
cJSON *llm;
cJSON *tags;
char *new_json;
char *cipher = NULL;
char *base_url = NULL;
int posted;
if (!prefs) {
return -1;
}
root = cJSON_CreateObject();
if (!root) {
root = g_state.user_settings_json
? cJSON_Parse(g_state.user_settings_json)
: cJSON_CreateObject();
if (!root || !cJSON_IsObject(root)) {
cJSON_Delete(root);
return -1;
}
cJSON_AddStringToObject(root, "ai_endpoint", prefs->endpoint ? prefs->endpoint : "");
cJSON_AddStringToObject(root, "ai_api_key", prefs->api_key ? prefs->api_key : "");
cJSON_AddStringToObject(root, "ai_model", prefs->model ? prefs->model : "");
if (!cJSON_GetObjectItemCaseSensitive(root, "v")) {
cJSON_AddNumberToObject(root, "v", 2);
}
cJSON_DeleteItemFromObjectCaseSensitive(root, "updatedAt");
cJSON_AddNumberToObject(root, "updatedAt", (double)time(NULL));
json = cJSON_PrintUnformatted(root);
if (!cJSON_GetObjectItemCaseSensitive(root, "global_zaps")) {
cJSON_AddObjectToObject(root, "global_zaps");
}
if (!cJSON_GetObjectItemCaseSensitive(root, "global_ui")) {
cJSON_AddObjectToObject(root, "global_ui");
}
if (!cJSON_GetObjectItemCaseSensitive(root, "global_relays")) {
cJSON_AddObjectToObject(root, "global_relays");
}
if (!cJSON_GetObjectItemCaseSensitive(root, "global_experimental")) {
cJSON_AddObjectToObject(root, "global_experimental");
}
llm = cJSON_GetObjectItemCaseSensitive(root, "global_llm");
if (!cJSON_IsObject(llm)) {
cJSON_DeleteItemFromObjectCaseSensitive(root, "global_llm");
llm = cJSON_AddObjectToObject(root, "global_llm");
}
if (!llm) {
cJSON_Delete(root);
return -1;
}
base_url = ai_base_url_from_endpoint(prefs->endpoint ? prefs->endpoint : "");
if (!base_url) {
cJSON_Delete(root);
return -1;
}
cJSON_DeleteItemFromObjectCaseSensitive(llm, "api_key");
cJSON_AddStringToObject(llm, "api_key", prefs->api_key ? prefs->api_key : "");
cJSON_DeleteItemFromObjectCaseSensitive(llm, "model");
cJSON_AddStringToObject(llm, "model", prefs->model ? prefs->model : "");
cJSON_DeleteItemFromObjectCaseSensitive(llm, "base_url");
cJSON_AddStringToObject(llm, "base_url", base_url);
new_json = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!json) {
free(base_url);
if (!new_json) {
return -1;
}
if (ai_encrypt_for_self(json, &cipher) != 0) {
free(json);
free(g_state.user_settings_json);
g_state.user_settings_json = new_json;
if (state_nip44_self_encrypt(new_json, &cipher) != 0 || !cipher) {
return -1;
}
tags = ai_make_d_tag("prefs");
tags = ai_make_d_tag("user-settings");
if (!tags) {
free(json);
free(cipher);
return -1;
}
posted = publish_signed_event_with_report("Prefs", 30078, cipher, tags, 8000);
posted = publish_signed_event_with_report("UserSettings", 30078, cipher, tags, 8000);
cJSON_Delete(tags);
free(json);
free(cipher);
return (posted < 0) ? -1 : 0;
@@ -465,6 +492,14 @@ static char *ai_extract_response(const char *resp_json) {
}
static void ai_chat(const ai_prefs_t *prefs) {
static const tui_view_t ai_chat_view = {
"> Main Menu > AI > Chat",
NULL,
};
static const tui_view_t ai_response_view = {
"> Main Menu > AI > Chat > Response",
NULL,
};
char prompt[4096];
cJSON *root;
cJSON *messages;
@@ -473,20 +508,23 @@ static void ai_chat(const ai_prefs_t *prefs) {
char auth[4096];
char *resp_json;
char *answer;
char hold[16];
char hold[512];
if (!prefs) {
return;
}
tui_get_line("prompt >", prompt, (int)sizeof(prompt));
tui_begin_frame(&ai_chat_view);
tui_print("Model: %s", prefs->model ? prefs->model : "");
(void)tui_end_frame_with_prompt("prompt >", prompt, (int)sizeof(prompt));
if (prompt[0] == '\0') {
return;
}
if (!prefs->api_key || prefs->api_key[0] == '\0') {
tui_begin_frame(&ai_chat_view);
tui_print("AI API key is empty. Configure settings first.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -520,23 +558,23 @@ static void ai_chat(const ai_prefs_t *prefs) {
free(body);
if (!resp_json) {
tui_begin_frame(&ai_chat_view);
tui_print("AI request failed.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
answer = ai_extract_response(resp_json);
free(resp_json);
tui_clear_screen();
tui_print("AI RESPONSE\n");
tui_begin_frame(&ai_response_view);
if (answer) {
tui_print("%s", answer);
} else {
tui_print("Failed to parse AI response.");
}
free(answer);
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
static void ai_choose_model(ai_prefs_t *prefs) {
@@ -555,72 +593,84 @@ static void ai_choose_model(ai_prefs_t *prefs) {
}
static void ai_settings(ai_prefs_t *prefs) {
static const tui_view_t ai_settings_view = {
"> Main Menu > AI > Settings",
NULL,
};
char endpoint[512];
char key[1024];
char model[256];
char hold[16];
char hold[512];
if (!prefs) {
return;
}
tui_begin_frame(&ai_settings_view);
tui_print("Leave empty to keep current value.");
tui_print("Current endpoint: %s", prefs->endpoint ? prefs->endpoint : "");
tui_get_line("new endpoint >", endpoint, (int)sizeof(endpoint));
(void)tui_end_frame_with_prompt("new endpoint >", endpoint, (int)sizeof(endpoint));
if (endpoint[0] != '\0') {
ai_set_string(&prefs->endpoint, endpoint);
}
tui_begin_frame(&ai_settings_view);
tui_print("Current API key: %s", (prefs->api_key && prefs->api_key[0] != '\0') ? "(set)" : "(empty)");
tui_get_line("new api key >", key, (int)sizeof(key));
(void)tui_end_frame_with_prompt("new api key >", key, (int)sizeof(key));
if (key[0] != '\0') {
ai_set_string(&prefs->api_key, key);
}
tui_begin_frame(&ai_settings_view);
tui_print("Current model: %s", prefs->model ? prefs->model : "");
tui_get_line("new model >", model, (int)sizeof(model));
(void)tui_end_frame_with_prompt("new model >", model, (int)sizeof(model));
if (model[0] != '\0') {
ai_set_string(&prefs->model, model);
}
tui_begin_frame(&ai_settings_view);
if (ai_save_prefs(prefs) != 0) {
tui_print("Failed to save prefs.");
} else {
tui_print("Prefs saved.");
}
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
void menu_ai(void) {
static const tui_view_t ai_view = {
"> Main Menu > AI",
NULL,
};
ai_prefs_t prefs;
char input[16];
char input[512];
ai_prefs_init(&prefs);
(void)ai_load_prefs(&prefs);
while (1) {
tui_clear_screen();
tui_print("AI\n");
tui_begin_frame(&ai_view);
tui_print("Model: %s", prefs.model ? prefs.model : "");
tui_print("Endpoint: %s", prefs.endpoint ? prefs.endpoint : "");
tui_print("1 - Chat");
tui_print("2 - Choose model");
tui_print("3 - Settings");
tui_print("x - Back");
tui_print_menu_item("^_C^:hat");
tui_print_menu_item("^_M^:odel");
tui_print_menu_item("^_S^:ettings");
tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
if (input[0] == '1') {
if (input[0] == 'c' || input[0] == 'C') {
ai_chat(&prefs);
} else if (input[0] == '2') {
} else if (input[0] == 'm' || input[0] == 'M') {
ai_choose_model(&prefs);
} else if (input[0] == '3') {
} else if (input[0] == 's' || input[0] == 'S') {
ai_settings(&prefs);
}
}

View File

@@ -3,6 +3,7 @@
#include "net.h"
#include "publish.h"
#include "editor.h"
#include "signer.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
@@ -49,63 +50,13 @@ static void diary_free_entries(diary_entry_t *entries, int count) {
}
static int diary_encrypt_for_self(const char *plaintext, char **cipher_out) {
unsigned char priv[32];
unsigned char pub[32];
char *out;
size_t out_sz;
if (!plaintext || !cipher_out) {
return -1;
}
*cipher_out = NULL;
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return -1;
}
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
return -1;
}
out_sz = (strlen(plaintext) * 4U) + 256U;
out = (char *)malloc(out_sz);
if (!out) {
return -1;
}
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
free(out);
return -1;
}
*cipher_out = out;
return 0;
return signer_nip44_self_encrypt(plaintext, cipher_out);
}
static char *diary_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
unsigned char priv[32];
unsigned char sender_pub[32];
size_t out_sz;
char *out;
char *out = NULL;
if (!sender_pub_hex || !ciphertext) {
return NULL;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return NULL;
}
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
return NULL;
}
out_sz = strlen(ciphertext) + 4096U;
out = (char *)malloc(out_sz);
if (!out) {
return NULL;
}
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
free(out);
if (signer_nip44_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
return NULL;
}
@@ -299,6 +250,14 @@ static int diary_cmp_desc_date(const void *a, const void *b) {
}
static void diary_browse_past(void) {
static const tui_view_t diary_past_view = {
"> Main Menu > Diary > Past Entries",
NULL,
};
static const tui_view_t diary_entry_view = {
"> Main Menu > Diary > Past Entries > View",
NULL,
};
const char **read_relays;
int relay_count = 0;
char filter[256];
@@ -316,8 +275,9 @@ static void diary_browse_past(void) {
g_state.npub_hex);
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
tui_begin_frame(&diary_past_view);
tui_print("Failed to query diary entries.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return;
}
@@ -375,17 +335,17 @@ static void diary_browse_past(void) {
}
while (1) {
tui_clear_screen();
tui_print("PAST DIARY ENTRIES\n");
tui_begin_frame(&diary_past_view);
for (i = 0; i < entries_count; i++) {
tui_print("%2d - %s", i + 1, entries[i].date_d ? entries[i].date_d : "(no d-tag)");
}
if (entries_count == 0) {
tui_print("No diary entries found.");
}
tui_print("\nEnter number to view, x to back.");
tui_print("");
tui_print("Enter number to view, x to back.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
break;
}
@@ -395,10 +355,11 @@ static void diary_browse_past(void) {
if (idx < 0 || idx >= entries_count) {
continue;
}
tui_clear_screen();
tui_print("DIARY %s\n", entries[idx].date_d ? entries[idx].date_d : "");
tui_begin_frame(&diary_entry_view);
tui_print("Date: %s", entries[idx].date_d ? entries[idx].date_d : "");
tui_print("");
tui_print("%s", entries[idx].content ? entries[idx].content : "");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
}
}
@@ -406,39 +367,45 @@ static void diary_browse_past(void) {
}
void menu_diary(void) {
static const tui_view_t diary_view = {
"> Main Menu > Diary",
NULL,
};
char today_d[16];
char *existing = NULL;
char *edited = NULL;
char input[16];
if (diary_get_today_d(today_d) != 0) {
tui_begin_frame(&diary_view);
tui_print("Failed to compute today's date.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return;
}
existing = diary_load_entry_plain(today_d);
while (1) {
tui_clear_screen();
tui_print("DIARY\n");
tui_begin_frame(&diary_view);
tui_print("Today: %s", today_d);
tui_print("Existing entry: %s", existing ? "yes" : "no");
tui_print("1 - Edit today's entry");
tui_print("2 - Browse past entries");
tui_print("x - Back");
tui_print_menu_item("^_E^:dit today's entry");
tui_print_menu_item("^_B^:rowse past entries");
tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
if (input[0] == 'q' || input[0] == 'Q' ||
input[0] == 'x' || input[0] == 'X') {
break;
}
if (input[0] == '1') {
if (input[0] == 'e' || input[0] == 'E') {
edited = editor_launch(existing ? existing : "");
if (!edited) {
tui_begin_frame(&diary_view);
tui_print("Edit canceled.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
@@ -447,8 +414,10 @@ void menu_diary(void) {
existing = diary_strdup(edited);
}
free(edited);
tui_get_line(">", input, (int)sizeof(input));
} else if (input[0] == '2') {
tui_begin_frame(&diary_view);
tui_print("Diary save attempted.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
} else if (input[0] == 'b' || input[0] == 'B') {
diary_browse_past();
}
}

View File

@@ -1,6 +1,7 @@
#include "tui.h"
#include "state.h"
#include "net.h"
#include "signer.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
@@ -132,6 +133,10 @@ static int dm_parse_recipient_hex(const char *input, char out_hex[65]) {
}
static void menu_dm_send(void) {
static const tui_view_t dm_send_view = {
"> Main Menu > DM > Send",
NULL,
};
char who[256];
char msg[4096];
char recip_hex[65] = {0};
@@ -146,28 +151,28 @@ static void menu_dm_send(void) {
int any_ok = 0;
char hold[16];
tui_clear_screen();
tui_print("SEND DM\n");
tui_get_line("recipient (npub or hex) >", who, (int)sizeof(who));
tui_begin_frame(&dm_send_view);
(void)tui_end_frame_with_prompt("recipient (npub or hex) >", who, (int)sizeof(who));
if (who[0] == '\0') {
return;
}
if (dm_parse_recipient_hex(who, recip_hex) != 0) {
tui_begin_frame(&dm_send_view);
tui_print("Invalid recipient. Use npub or 64-char hex pubkey.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
tui_get_line("message >", msg, (int)sizeof(msg));
(void)tui_end_frame_with_prompt("message >", msg, (int)sizeof(msg));
if (msg[0] == '\0') {
return;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
tui_print("Invalid private key in state.");
tui_get_line(">", hold, (int)sizeof(hold));
if (signer_get_local_private_key(priv) != 0) {
tui_begin_frame(&dm_send_view);
tui_print("Private key unavailable for DM send.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -180,20 +185,23 @@ static void menu_dm_send(void) {
NULL,
g_state.npub_hex);
if (!rumor) {
tui_begin_frame(&dm_send_view);
tui_print("Failed to build NIP-17 DM rumor.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
wrap_count = nostr_nip17_send_dm(rumor, recips, 1, priv, gift_wraps, 8, 0);
cJSON_Delete(rumor);
if (wrap_count <= 0) {
tui_begin_frame(&dm_send_view);
tui_print("Failed to create NIP-17 gift wrap event(s).");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
write_relays = state_get_write_relays(&relay_count);
tui_begin_frame(&dm_send_view);
tui_print("Publishing %d wrapped DM event(s) to %d relay(s)...", wrap_count, relay_count);
for (i = 0; i < wrap_count; i++) {
@@ -224,37 +232,13 @@ static void menu_dm_send(void) {
tui_print("DM publish failed.");
}
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
static char *dm_try_decrypt_kind4(const char *sender_pub_hex, const char *ciphertext) {
unsigned char priv[32];
unsigned char sender_pub[32];
size_t out_sz;
char *out;
if (!sender_pub_hex || !ciphertext) {
return NULL;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return NULL;
}
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
return NULL;
}
out_sz = strlen(ciphertext) + 1024U;
out = (char *)malloc(out_sz);
if (!out) {
return NULL;
}
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
free(out);
return NULL;
}
char *out = NULL;
if (!sender_pub_hex || !ciphertext) return NULL;
if (signer_nip04_decrypt(sender_pub_hex, ciphertext, &out) != 0) return NULL;
return out;
}
@@ -344,7 +328,7 @@ static void dm_collect_nip17_kind1059(dm_item_t **items, int *items_count) {
return;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
if (signer_get_local_private_key(priv) != 0) {
return;
}
@@ -420,13 +404,16 @@ static void dm_collect_nip17_kind1059(dm_item_t **items, int *items_count) {
}
static void menu_dm_read_inbox(void) {
static const tui_view_t dm_inbox_view = {
"> Main Menu > DM > Inbox",
NULL,
};
dm_item_t *items = NULL;
int items_count = 0;
int i;
char hold[16];
tui_clear_screen();
tui_print("READ INBOX\n");
tui_begin_frame(&dm_inbox_view);
tui_print("Querying kind 1059 + kind 4...");
dm_collect_nip17_kind1059(&items, &items_count);
@@ -436,8 +423,7 @@ static void menu_dm_read_inbox(void) {
qsort(items, (size_t)items_count, sizeof(dm_item_t), dm_cmp_desc_created);
}
tui_clear_screen();
tui_print("INBOX\n");
tui_begin_frame(&dm_inbox_view);
for (i = 0; i < items_count; i++) {
char sender_short[32];
@@ -450,28 +436,33 @@ static void menu_dm_read_inbox(void) {
}
dm_free_items(items, items_count);
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to exit >", hold, (int)sizeof(hold));
}
void menu_dm(void) {
static const tui_view_t dm_view = {
"> Main Menu > DM",
NULL,
};
char input[16];
while (1) {
tui_clear_screen();
tui_print("DIRECT MESSAGE\n");
tui_print("1 - Send DM (NIP-17)");
tui_print("2 - Read inbox (kind 1059 + kind 4)");
tui_print("x - Back");
tui_begin_frame(&dm_view);
tui_print_menu_item("^_S^:end DM (NIP-17)");
tui_print_menu_item("^_R^:ead inbox (kind 1059 + kind 4)");
tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
if (input[0] == '1') {
if (input[0] == 's' || input[0] == 'S') {
menu_dm_send();
} else if (input[0] == '2') {
} else if (input[0] == 'r' || input[0] == 'R') {
menu_dm_read_inbox();
}
}

View File

@@ -406,12 +406,30 @@ static uint64_t ecash_wallet_total(const ecash_wallet_t *w) {
return total;
}
static void ecash_print_mint_row_responsive(int term_width, int index, const char *mint_url, uint64_t mint_total) {
if (term_width < 90) {
tui_print("%2d. %s", index, mint_url ? mint_url : "");
tui_print(" %llu sats", (unsigned long long)mint_total);
return;
}
tui_print("%2d. %-48s %12llu sats",
index,
mint_url ? mint_url : "",
(unsigned long long)mint_total);
}
static void ecash_show_balance(const ecash_wallet_t *w) {
static const tui_view_t ecash_balance_view = {
"> Main Menu > Ecash > Balance",
NULL,
};
int i;
int term_width = 0;
char hold[16];
tui_clear_screen();
tui_print("ECASH BALANCE\n");
tui_begin_frame(&ecash_balance_view);
tui_get_terminal_size(&term_width, NULL);
tui_print("Total: %llu sats", (unsigned long long)ecash_wallet_total(w));
tui_print("");
@@ -425,30 +443,36 @@ static void ecash_show_balance(const ecash_wallet_t *w) {
}
}
tui_print("%2d. %s", i + 1, w->mints[i]);
tui_print(" balance: %llu sats", (unsigned long long)mint_total);
ecash_print_mint_row_responsive(term_width, i + 1, w->mints[i], mint_total);
}
if (w->mint_count == 0) {
tui_print("No mints configured.");
}
tui_print("\nProof count: %d", w->proof_count);
tui_get_line(">", hold, (int)sizeof(hold));
tui_print("");
tui_print("Proof count: %d", w->proof_count);
(void)tui_end_frame_with_prompt("Press Enter to exit >", hold, (int)sizeof(hold));
}
static void ecash_add_mint_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_add_mint_view = {
"> Main Menu > Ecash > Add Mint",
NULL,
};
char mint[512];
char hold[16];
tui_get_line("mint url >", mint, (int)sizeof(mint));
tui_begin_frame(&ecash_add_mint_view);
(void)tui_end_frame_with_prompt("mint url >", mint, (int)sizeof(mint));
if (mint[0] == '\0') {
return;
}
tui_begin_frame(&ecash_add_mint_view);
if (ecash_add_mint(w, mint) != 0) {
tui_print("Failed to add mint.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -457,30 +481,48 @@ static void ecash_add_mint_menu(ecash_wallet_t *w) {
} else {
tui_print("Mint added.");
}
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
static void ecash_remove_mint_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_remove_mint_view = {
"> Main Menu > Ecash > Remove Mint",
NULL,
};
char input[64];
char yn[8];
int idx;
int i;
char hold[16];
if (!w || w->mint_count <= 0) {
tui_begin_frame(&ecash_remove_mint_view);
tui_print("No mints to remove.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
tui_begin_frame(&ecash_remove_mint_view);
for (i = 0; i < w->mint_count; i++) {
tui_print("%2d. %s", i + 1, w->mints[i]);
}
tui_get_line("remove number >", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("remove number >", input, (int)sizeof(input));
idx = atoi(input) - 1;
if (idx < 0 || idx >= w->mint_count) {
tui_begin_frame(&ecash_remove_mint_view);
tui_print("Invalid mint number.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
tui_begin_frame(&ecash_remove_mint_view);
tui_print("Remove mint: %s", w->mints[idx] ? w->mints[idx] : "");
(void)tui_end_frame_with_prompt("Confirm remove [y/n] >", yn, (int)sizeof(yn));
if (!(yn[0] == 'y' || yn[0] == 'Y')) {
tui_begin_frame(&ecash_remove_mint_view);
tui_print("Remove canceled.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -490,15 +532,20 @@ static void ecash_remove_mint_menu(ecash_wallet_t *w) {
}
w->mint_count--;
tui_begin_frame(&ecash_remove_mint_view);
if (ecash_publish_wallet(w) != 0) {
tui_print("Failed to publish wallet metadata.");
} else {
tui_print("Mint removed.");
}
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
static void ecash_receive_token_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_import_view = {
"> Main Menu > Ecash > Import Token",
NULL,
};
char token[8192];
cashu_decoded_token_t decoded;
int i;
@@ -509,21 +556,24 @@ static void ecash_receive_token_menu(ecash_wallet_t *w) {
}
memset(&decoded, 0, sizeof(decoded));
tui_get_line("paste cashu token >", token, (int)sizeof(token));
tui_begin_frame(&ecash_import_view);
(void)tui_end_frame_with_prompt("paste cashu token >", token, (int)sizeof(token));
if (token[0] == '\0') {
return;
}
if (cashu_decode_token(token, &decoded) != 0) {
tui_begin_frame(&ecash_import_view);
tui_print("Failed to decode token.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
if (!decoded.mint_url || decoded.proof_count <= 0 || !decoded.proofs) {
cashu_free_decoded_token(&decoded);
tui_begin_frame(&ecash_import_view);
tui_print("Token did not contain usable proofs.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -534,12 +584,13 @@ static void ecash_receive_token_menu(ecash_wallet_t *w) {
cashu_free_decoded_token(&decoded);
tui_begin_frame(&ecash_import_view);
if (ecash_publish_wallet(w) != 0) {
tui_print("Token imported locally, but publish failed.");
} else {
tui_print("Token received and wallet updated.");
}
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
static void ecash_remove_proofs_by_global_indices(ecash_wallet_t *w, const int *indices, int nidx) {
@@ -582,6 +633,14 @@ static void ecash_remove_proofs_by_global_indices(ecash_wallet_t *w, const int *
}
static void ecash_send_token_menu(ecash_wallet_t *w) {
static const tui_view_t ecash_send_view = {
"> Main Menu > Ecash > Send Token",
NULL,
};
static const tui_view_t ecash_send_result_view = {
"> Main Menu > Ecash > Send Token > Result",
NULL,
};
char input[128];
int mint_idx;
uint64_t target_amount;
@@ -602,28 +661,34 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
}
if (w->mint_count <= 0) {
tui_begin_frame(&ecash_send_view);
tui_print("No mints configured.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
tui_begin_frame(&ecash_send_view);
for (i = 0; i < w->mint_count; i++) {
tui_print("%2d. %s", i + 1, w->mints[i]);
}
tui_get_line("mint number >", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("mint number >", input, (int)sizeof(input));
mint_idx = atoi(input) - 1;
if (mint_idx < 0 || mint_idx >= w->mint_count) {
tui_begin_frame(&ecash_send_view);
tui_print("Invalid mint number.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
tui_get_line("amount (sats) >", input, (int)sizeof(input));
tui_begin_frame(&ecash_send_view);
tui_print("Mint: %s", w->mints[mint_idx]);
(void)tui_end_frame_with_prompt("amount (sats) >", input, (int)sizeof(input));
target_amount = (uint64_t)strtoull(input, NULL, 10);
if (target_amount == 0) {
tui_begin_frame(&ecash_send_view);
tui_print("Invalid amount.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -636,8 +701,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (!next_proofs) {
free(mint_proofs);
free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -647,8 +713,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (!next_idx) {
free(mint_proofs);
free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -662,8 +729,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (mint_proof_count <= 0) {
free(mint_proofs);
free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("No proofs available for selected mint.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -671,8 +739,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
if (!selected_local) {
free(mint_proofs);
free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -686,8 +755,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(selected_local);
free(mint_proofs);
free(mint_global_indices);
tui_begin_frame(&ecash_send_view);
tui_print("Could not select proofs for that amount.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -703,8 +773,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(mint_global_indices);
free(selected_global);
cashu_free_decoded_token(&token);
tui_begin_frame(&ecash_send_view);
tui_print("Memory error.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -719,8 +790,9 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(mint_global_indices);
free(selected_global);
cashu_free_decoded_token(&token);
tui_begin_frame(&ecash_send_view);
tui_print("Failed to assemble token proofs.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
@@ -734,23 +806,23 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
free(mint_global_indices);
free(selected_global);
cashu_free_decoded_token(&token);
tui_begin_frame(&ecash_send_view);
tui_print("Failed to encode token.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
return;
}
ecash_remove_proofs_by_global_indices(w, selected_global, selected_count);
(void)ecash_publish_wallet(w);
tui_clear_screen();
tui_print("SEND TOKEN\n");
tui_begin_frame(&ecash_send_result_view);
tui_print("Requested: %llu sats", (unsigned long long)target_amount);
tui_print("Selected: %llu sats", (unsigned long long)selected_total);
tui_print("Token:");
tui_print("%s", encoded);
tui_print("");
tui_print("Note: Mint HTTP swap/melt flows are not yet implemented in this MVP.");
tui_get_line(">", hold, (int)sizeof(hold));
(void)tui_end_frame_with_prompt("Press Enter to exit >", hold, (int)sizeof(hold));
free(encoded);
free(selected_local);
@@ -761,6 +833,10 @@ static void ecash_send_token_menu(ecash_wallet_t *w) {
}
void menu_ecash(void) {
static const tui_view_t ecash_view = {
"> Main Menu > Ecash",
NULL,
};
ecash_wallet_t wallet;
char input[16];
@@ -769,32 +845,32 @@ void menu_ecash(void) {
}
while (1) {
tui_clear_screen();
tui_print("ECASH WALLET\n");
tui_begin_frame(&ecash_view);
tui_print("Mints: %d", wallet.mint_count);
tui_print("Proofs: %d", wallet.proof_count);
tui_print("1 - Balance");
tui_print("2 - Add mint");
tui_print("3 - Remove mint");
tui_print("4 - Receive token");
tui_print("5 - Send token");
tui_print("x - Back");
tui_print_menu_item("^_B^:alance");
tui_print_menu_item("^_A^:dd mint");
tui_print_menu_item("^_R^:emove mint");
tui_print_menu_item("^_I^:mport token");
tui_print_menu_item("^_S^:end token");
tui_print_menu_item("E^_x^:it");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
if (input[0] == '1') {
if (input[0] == 'b' || input[0] == 'B') {
ecash_show_balance(&wallet);
} else if (input[0] == '2') {
} else if (input[0] == 'a' || input[0] == 'A') {
ecash_add_mint_menu(&wallet);
} else if (input[0] == '3') {
} else if (input[0] == 'r' || input[0] == 'R') {
ecash_remove_mint_menu(&wallet);
} else if (input[0] == '4') {
} else if (input[0] == 'i' || input[0] == 'I') {
ecash_receive_token_menu(&wallet);
} else if (input[0] == '5') {
} else if (input[0] == 's' || input[0] == 'S') {
ecash_send_token_menu(&wallet);
}
}

View File

@@ -30,7 +30,23 @@ static int follows_publish_kind3(cJSON *event_obj) {
8000);
}
static void follows_print_row_responsive(int term_width, int index, const char *npub, const char *name) {
if (term_width < 90) {
tui_print("[%2d] %s", index, npub ? npub : "");
if (name && name[0] != '\0') {
tui_print(" %s", name);
}
return;
}
tui_print("[%2d] %s %s", index, npub ? npub : "", name ? name : "");
}
void menu_follows(void) {
static const tui_view_t follows_view = {
"> Main Menu > Follows",
NULL,
};
cJSON *working = NULL;
cJSON *tags = NULL;
char input[256];
@@ -53,10 +69,10 @@ void menu_follows(void) {
while (1) {
int i;
int n;
int term_width = 0;
tui_clear_screen();
tui_print("FOLLOWS");
tui_print("\n\n");
tui_begin_frame(&follows_view);
tui_get_terminal_size(&term_width, NULL);
n = cJSON_GetArraySize(tags);
for (i = 0; i < n; i++) {
@@ -81,20 +97,20 @@ void menu_follows(void) {
}
}
tui_print("[%2d] %s %s",
i + 1,
npub,
(cJSON_IsString(t3) && t3->valuestring) ? t3->valuestring : "");
follows_print_row_responsive(term_width,
i + 1,
npub,
(cJSON_IsString(t3) && t3->valuestring) ? t3->valuestring : "");
}
tui_print("\n\n");
tui_print("P^_r^:ofile");
tui_print("^_A^:dd follow");
tui_print("^_D^:elete follow");
tui_print("^_P^:ost changes and exit.");
tui_print("E^_x^:it without saving");
tui_print("");
tui_print_menu_item("^_R^:efresh profile");
tui_print_menu_item("^_A^:dd follow");
tui_print_menu_item("^_D^:elete follow");
tui_print_menu_item("^_P^:ost changes and exit.");
tui_print_menu_item("E^_x^:it without saving");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'a' || input[0] == 'A') {
char who[256];
@@ -107,7 +123,8 @@ void menu_follows(void) {
cJSON *meta;
cJSON *name;
tui_get_line("npub to add >", who, (int)sizeof(who));
tui_begin_frame(&follows_view);
(void)tui_end_frame_with_prompt("npub to add >", who, (int)sizeof(who));
if (who[0] == '\0') {
continue;
}
@@ -115,22 +132,26 @@ void menu_follows(void) {
if (strncmp(who, "npub", 4) == 0) {
unsigned char pub[32];
if (nostr_decode_npub(who, pub) != 0) {
tui_begin_frame(&follows_view);
tui_print("Invalid npub.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
nostr_bytes_to_hex(pub, 32, follow_hex);
} else if (strlen(who) == 64) {
unsigned char pub[32];
if (nostr_hex_to_bytes(who, pub, 32) != 0) {
tui_begin_frame(&follows_view);
tui_print("Invalid hex pubkey.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
snprintf(follow_hex, sizeof(follow_hex), "%s", who);
strncpy(follow_hex, who, sizeof(follow_hex) - 1U);
follow_hex[sizeof(follow_hex) - 1U] = '\0';
} else {
tui_begin_frame(&follows_view);
tui_print("Enter npub or 64-char hex pubkey.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
@@ -140,8 +161,9 @@ void menu_follows(void) {
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
7000,
&follow_kind0) != 0 || !follow_kind0) {
tui_begin_frame(&follows_view);
tui_print("Can't find user. Try adding more relays.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
@@ -158,11 +180,12 @@ void menu_follows(void) {
}
}
tui_get_line((cJSON_IsString(name) && name->valuestring)
? "Add follow [y/n] >"
: "Add this follow [y/n] >",
yn,
(int)sizeof(yn));
tui_begin_frame(&follows_view);
(void)tui_end_frame_with_prompt((cJSON_IsString(name) && name->valuestring)
? "Add follow [y/n] >"
: "Add this follow [y/n] >",
yn,
(int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON *tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("p"));
@@ -180,13 +203,14 @@ void menu_follows(void) {
char yn[32];
int idx;
tui_get_line("# to delete >", numbuf, (int)sizeof(numbuf));
tui_begin_frame(&follows_view);
(void)tui_end_frame_with_prompt("# to delete >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue;
}
tui_get_line("Delete follow [y/n] >", yn, (int)sizeof(yn));
(void)tui_end_frame_with_prompt("Delete follow [y/n] >", yn, (int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON_DeleteItemFromArray(tags, idx);
}
@@ -201,7 +225,8 @@ void menu_follows(void) {
cJSON *content;
cJSON *meta;
tui_get_line("#>", numbuf, (int)sizeof(numbuf));
tui_begin_frame(&follows_view);
(void)tui_end_frame_with_prompt("# >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue;
@@ -219,8 +244,9 @@ void menu_follows(void) {
g_state.read_relay_count > 0 ? g_state.read_relay_count : g_state.bootstrap_relay_count,
7000,
&follow_kind0) != 0 || !follow_kind0) {
tui_begin_frame(&follows_view);
tui_print("Unable to fetch profile.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
@@ -233,7 +259,8 @@ void menu_follows(void) {
}
}
tui_print("PROFILE");
tui_begin_frame(&follows_view);
tui_print("Profile");
if (meta) {
cJSON *it = NULL;
cJSON_ArrayForEach(it, meta) {
@@ -245,7 +272,7 @@ void menu_follows(void) {
tui_print("No metadata content.");
}
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
cJSON_Delete(meta);
cJSON_Delete(ev);
free(follow_kind0);
@@ -256,12 +283,17 @@ void menu_follows(void) {
free(g_state.kind3_json);
g_state.kind3_json = new_json;
}
tui_begin_frame(&follows_view);
if (posted < 0) {
tui_print("Follows publish failed.");
} else {
tui_print("Follows published.");
}
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
break;
} else if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
} else if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
}

View File

@@ -60,7 +60,7 @@ static void live_preview_text(const char *text, int max_len, char *buf, size_t b
buf[0] = '\0';
if (!text || text[0] == '\0' || max_len <= 0) {
snprintf(buf, buf_size, "");
buf[0] = '\0';
return;
}
@@ -218,6 +218,10 @@ static void live_free_authors(char **authors, int count) {
}
static void menu_live_run_filter(const char *title, const char *filter_json) {
static const tui_view_t live_feed_view = {
"> Main Menu > Live Feeds > Stream",
NULL,
};
const char **read_relays;
int relay_count = 0;
live_feed_ctx_t ctx;
@@ -225,8 +229,8 @@ static void menu_live_run_filter(const char *title, const char *filter_json) {
char input[8];
read_relays = state_get_read_relays(&relay_count);
tui_clear_screen();
tui_print("%s\n", title);
tui_begin_frame(&live_feed_view);
tui_print("%s", title ? title : "Live Feed");
tui_print("Press any key to stop...");
memset(&ctx, 0, sizeof(ctx));
@@ -238,33 +242,38 @@ static void menu_live_run_filter(const char *title, const char *filter_json) {
live_event_print_cb,
&ctx);
tui_print("");
tui_begin_frame(&live_feed_view);
if (events_received < 0) {
tui_print("Feed stopped (failed to connect to all relays).");
} else {
tui_print("Feed stopped. %d events received.", events_received);
}
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
}
void menu_live(void) {
static const tui_view_t live_menu_view = {
"> Main Menu > Live Feeds",
NULL,
};
char input[16];
while (1) {
tui_clear_screen();
tui_print("LIVE FEEDS\n");
tui_print("1 - Follows feed");
tui_print("2 - Mentions");
tui_print("3 - Firehose");
tui_print("x - Back");
tui_begin_frame(&live_menu_view);
tui_print_menu_item("^_F^:ollows feed");
tui_print_menu_item("^_M^:entions");
tui_print_menu_item("^_G^:lobal firehose");
tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
if (input[0] == '1') {
if (input[0] == 'f' || input[0] == 'F') {
char **authors = NULL;
int authors_count = 0;
cJSON *filter_arr = NULL;
@@ -275,8 +284,9 @@ void menu_live(void) {
int i;
if (live_extract_follow_authors(&authors, &authors_count) != 0 || authors_count <= 0) {
tui_begin_frame(&live_menu_view);
tui_print("No follows found in kind3 list.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
live_free_authors(authors, authors_count);
continue;
}
@@ -291,8 +301,9 @@ void menu_live(void) {
cJSON_Delete(authors_json);
cJSON_Delete(kinds_json);
live_free_authors(authors, authors_count);
tui_begin_frame(&live_menu_view);
tui_print("Failed to build follows filter.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
@@ -312,14 +323,14 @@ void menu_live(void) {
cJSON_Delete(filter_arr);
live_free_authors(authors, authors_count);
} else if (input[0] == '2') {
} else if (input[0] == 'm' || input[0] == 'M') {
char filter[512];
snprintf(filter,
sizeof(filter),
"[{\"kinds\":[1],\"#p\":[\"%s\"],\"limit\":500}]",
g_state.npub_hex);
menu_live_run_filter("MENTIONS", filter);
} else if (input[0] == '3') {
} else if (input[0] == 'g' || input[0] == 'G') {
char filter[128];
snprintf(filter, sizeof(filter), "[{\"kinds\":[1],\"limit\":500}]");
menu_live_run_filter("FIREHOSE", filter);

View File

@@ -1,8 +1,11 @@
#include "tui.h"
#include "state.h"
#include "net.h"
#include "db.h"
#include "publish.h"
#include "signer.h"
#include "nsigner_client.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip006.h"
@@ -14,6 +17,7 @@
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/random.h>
#define NT_DEV_SEED_PHRASE "cube dirt movie learn depth axis ball view aunt electric finish release"
@@ -90,6 +94,10 @@ static int menu_publish_signed_event(int kind, const char *content, cJSON *tags)
}
static void menu_add_new_user(void) {
static const tui_view_t login_new_profile_view = {
"> Login > New Account > Profile Setup",
NULL,
};
char buf[512];
char username[128];
cJSON *meta = cJSON_CreateObject();
@@ -99,28 +107,35 @@ static void menu_add_new_user(void) {
username[0] = '\0';
do {
tui_get_line("Enter a username for this account (required) >", username, (int)sizeof(username));
tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter a username for this account (required) >", username, (int)sizeof(username));
} while (username[0] == '\0');
cJSON_AddStringToObject(meta, "name", username);
cJSON_AddStringToObject(meta, "displayname", username);
tui_get_line("Enter \"about\" info: >", buf, (int)sizeof(buf));
tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter \"about\" info: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "about", buf);
tui_get_line("Enter profile picture url: >", buf, (int)sizeof(buf));
tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter profile picture url: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "picture", buf);
tui_get_line("Enter banner picture url: >", buf, (int)sizeof(buf));
tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter banner picture url: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "banner", buf);
tui_get_line("Enter website url: >", buf, (int)sizeof(buf));
tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter website url: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "website", buf);
tui_get_line("Enter lud16: >", buf, (int)sizeof(buf));
tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter lud16: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "lud16", buf);
tui_get_line("Enter nip05: >", buf, (int)sizeof(buf));
tui_begin_frame(&login_new_profile_view);
(void)tui_end_frame_with_prompt("Enter nip05: >", buf, (int)sizeof(buf));
cJSON_AddStringToObject(meta, "nip05", buf);
free(g_state.kind0_json);
@@ -149,6 +164,34 @@ static void menu_add_new_user(void) {
}
void menu_login(void) {
static const tui_view_t login_view = {
"> Login",
NULL,
};
static const tui_view_t login_privkey_view = {
"> Login > Private Key",
NULL,
};
static const tui_view_t login_mnemonic_view = {
"> Login > Mnemonic",
NULL,
};
static const tui_view_t login_signer_local_view = {
"> Login > Signer Local",
NULL,
};
static const tui_view_t login_signer_url_view = {
"> Login > URL Signer",
NULL,
};
static const tui_view_t login_signer_qrexec_view = {
"> Login > Qrexec Signer",
NULL,
};
static const tui_view_t login_new_account_view = {
"> Login > New Account",
NULL,
};
char input[512];
char pending_test_mnemonic[512] = {0};
int pending_test_mnemonic_ready = 0;
@@ -157,43 +200,341 @@ void menu_login(void) {
unsigned char priv[32];
unsigned char pub[32];
tui_clear_screen();
tui_print("LOGIN");
(void)tui_consume_resize_flag();
tui_begin_frame(&login_view);
tui_print_menu_item("^_E^:nter test/dev seed fallback");
tui_print_menu_item("^_P^:rivate key (nsec or 64-hex)");
tui_print_menu_item("^_M^:nemonic (12 words)");
tui_print_menu_item("^_S^:igner local (same qube)");
tui_print_menu_item("^_U^:RL signer (FIPS/web address)");
tui_print_menu_item("^_N^:ew account");
tui_print_menu_item("^_Q^:uit");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (strcmp(input, "q") == 0 || strcmp(input, "x") == 0) {
tui_print("\nHasta luego.\n");
signer_shutdown();
exit(0);
}
if (input[0] == 'e' || input[0] == 'E') {
input[0] = '\0';
} else if (input[0] == 'p' || input[0] == 'P') {
tui_begin_frame(&login_privkey_view);
(void)tui_end_frame_with_prompt("Private key (nsec or 64-hex) >", input, (int)sizeof(input));
if (input[0] == '\0') {
continue;
}
} else if (input[0] == 'm' || input[0] == 'M') {
tui_begin_frame(&login_mnemonic_view);
(void)tui_end_frame_with_prompt("Seed phrase (12 words) >", input, (int)sizeof(input));
if (input[0] == '\0') {
continue;
}
}
if (strcmp(input, "s") == 0 || strcmp(input, "S") == 0) {
char **names = NULL;
int name_count = 0;
char pubkey_hex[65] = {0};
int selected = 0;
nt_nsigner_selector_t selector = nsigner_selector_default();
char idxbuf[32];
int nostr_index = 0;
tui_begin_frame(&login_signer_local_view);
tui_print("Searching for running n_signer instances...");
if (nsigner_list(&names, &name_count) != 0 || name_count == 0) {
tui_print("No running n_signer found. Start `nsigner` and try again.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
if (name_count == 1) {
tui_print("Found: @%s", names[0]);
selected = 0;
} else {
int idx;
char pickbuf[16];
tui_print("Found %d n_signer instances:", name_count);
for (idx = 0; idx < name_count; idx++) {
tui_print(" [%d] @%s", idx, names[idx]);
}
(void)tui_end_frame_with_prompt("Select (0) >", pickbuf, (int)sizeof(pickbuf));
selected = (pickbuf[0] != '\0') ? atoi(pickbuf) : 0;
if (selected < 0 || selected >= name_count) {
selected = 0;
}
}
tui_begin_frame(&login_signer_local_view);
(void)tui_end_frame_with_prompt("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') {
nostr_index = atoi(idxbuf);
if (nostr_index < 0) {
nostr_index = 0;
}
}
selector.has_nostr_index = 1;
selector.nostr_index = nostr_index;
selector.role[0] = '\0';
tui_begin_frame(&login_signer_local_view);
tui_print("Waiting for n_signer approval...");
if (nsigner_get_public_key(names[selected], &selector, pubkey_hex) != 0) {
int i;
tui_print("Failed to get public key from n_signer (denied or error).");
for (i = 0; i < name_count; i++) {
free(names[i]);
}
free(names);
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
signer_init_nsigner(names[selected], &selector);
snprintf(g_state.npub_hex, sizeof(g_state.npub_hex), "%s", pubkey_hex);
{
unsigned char pub_bytes[32];
nostr_hex_to_bytes(pubkey_hex, pub_bytes, 32);
nostr_key_to_bech32(pub_bytes, "npub", g_state.npub_bech32);
}
g_state.nsec_hex[0] = '\0';
g_state.nsec_bech32[0] = '\0';
g_state.seed_phrase[0] = '\0';
g_state.logged_in = 1;
tui_print("Signed in via n_signer (@%s)", names[selected]);
tui_print("npub: %s", g_state.npub_bech32);
{
int i;
for (i = 0; i < name_count; i++) {
free(names[i]);
}
free(names);
}
(void)state_load_user_info();
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
tui_print("No profile found on relays. Setting up new user...");
menu_add_new_user();
(void)state_load_user_info();
}
return;
}
if (strcmp(input, "u") == 0 || strcmp(input, "U") == 0) {
char pubkey_hex[65] = {0};
char endpoint_url[256];
const char *saved_endpoint = db_get_local("nsigner_url_endpoint");
nt_nsigner_selector_t selector = nsigner_selector_default();
char idxbuf[32];
int nostr_index = 0;
int session_fd = -1;
nt_nsigner_auth_ctx_t auth = {0};
snprintf(endpoint_url, sizeof(endpoint_url), "%s", (saved_endpoint && saved_endpoint[0] != '\0') ? saved_endpoint : "");
tui_begin_frame(&login_signer_url_view);
(void)tui_end_frame_with_prompt("Signer URL (http://<npub>.fips:8080) >", endpoint_url, (int)sizeof(endpoint_url));
if (endpoint_url[0] == '\0') {
if (saved_endpoint && saved_endpoint[0] != '\0') {
snprintf(endpoint_url, sizeof(endpoint_url), "%s", saved_endpoint);
} else {
tui_begin_frame(&login_signer_url_view);
tui_print("No URL provided.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
}
tui_begin_frame(&login_signer_url_view);
(void)tui_end_frame_with_prompt("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') {
nostr_index = atoi(idxbuf);
if (nostr_index < 0) {
nostr_index = 0;
}
}
selector.has_nostr_index = 1;
selector.nostr_index = nostr_index;
selector.role[0] = '\0';
tui_begin_frame(&login_signer_url_view);
tui_print("Connecting to n_signer URL: %s", endpoint_url);
tui_print("Waiting for n_signer approval...");
auth.enabled = 1;
if (getrandom(auth.privkey, sizeof(auth.privkey), 0) != (ssize_t)sizeof(auth.privkey)) {
tui_print("Failed to generate URL auth key.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
snprintf(auth.label, sizeof(auth.label), "%s", "nostr_terminal");
session_fd = nsigner_session_open_url(endpoint_url);
if (session_fd < 0) {
tui_print("Failed to open persistent connection to n_signer URL.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
if (nsigner_session_get_public_key(&session_fd, endpoint_url, &selector, &auth, pubkey_hex) != 0) {
nsigner_session_close(session_fd);
tui_print("Failed to get public key from n_signer URL (denied or error).");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
if (signer_init_nsigner_url_with_session_auth(endpoint_url, &selector, session_fd, &auth) != 0) {
nsigner_session_close(session_fd);
tui_print("Failed to initialize URL signer mode.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
(void)db_set_local("nsigner_url_endpoint", endpoint_url);
snprintf(g_state.npub_hex, sizeof(g_state.npub_hex), "%s", pubkey_hex);
{
unsigned char pub_bytes[32];
nostr_hex_to_bytes(pubkey_hex, pub_bytes, 32);
nostr_key_to_bech32(pub_bytes, "npub", g_state.npub_bech32);
}
g_state.nsec_hex[0] = '\0';
g_state.nsec_bech32[0] = '\0';
g_state.seed_phrase[0] = '\0';
g_state.logged_in = 1;
tui_print("Signed in via n_signer URL (%s)", endpoint_url);
tui_print("npub: %s", g_state.npub_bech32);
(void)state_load_user_info();
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
tui_print("No profile found on relays. Setting up new user...");
menu_add_new_user();
(void)state_load_user_info();
}
return;
}
if (strcmp(input, "S") == 0) {
char pubkey_hex[65] = {0};
char target_qube[128];
char service_name[128];
const char *saved_target = db_get_local("nsigner_qrexec_target");
nt_nsigner_selector_t selector = nsigner_selector_default();
char idxbuf[32];
int nostr_index = 0;
snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault");
snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc");
tui_begin_frame(&login_signer_qrexec_view);
(void)tui_end_frame_with_prompt("Target signer qube (nsigner-vault) >", target_qube, (int)sizeof(target_qube));
if (target_qube[0] == '\0') {
snprintf(target_qube, sizeof(target_qube), "%s", (saved_target && saved_target[0] != '\0') ? saved_target : "nsigner-vault");
}
tui_begin_frame(&login_signer_qrexec_view);
(void)tui_end_frame_with_prompt("Qrexec service (qubes.NsignerRpc) >", service_name, (int)sizeof(service_name));
if (service_name[0] == '\0') {
snprintf(service_name, sizeof(service_name), "%s", "qubes.NsignerRpc");
}
tui_begin_frame(&login_signer_qrexec_view);
(void)tui_end_frame_with_prompt("Seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') {
nostr_index = atoi(idxbuf);
if (nostr_index < 0) {
nostr_index = 0;
}
}
selector.has_nostr_index = 1;
selector.nostr_index = nostr_index;
selector.role[0] = '\0';
tui_begin_frame(&login_signer_qrexec_view);
tui_print("Calling qrexec service %s in %s...", service_name, target_qube);
tui_print("Waiting for n_signer approval...");
if (nsigner_qrexec_get_public_key(target_qube, service_name, &selector, pubkey_hex) != 0) {
tui_print("Failed to get public key via qrexec (denied or error).");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
if (signer_init_nsigner_qrexec(target_qube, service_name, &selector) != 0) {
tui_print("Failed to initialize qrexec signer mode.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
(void)db_set_local("nsigner_qrexec_target", target_qube);
snprintf(g_state.npub_hex, sizeof(g_state.npub_hex), "%s", pubkey_hex);
{
unsigned char pub_bytes[32];
nostr_hex_to_bytes(pubkey_hex, pub_bytes, 32);
nostr_key_to_bech32(pub_bytes, "npub", g_state.npub_bech32);
}
g_state.nsec_hex[0] = '\0';
g_state.nsec_bech32[0] = '\0';
g_state.seed_phrase[0] = '\0';
g_state.logged_in = 1;
tui_print("Signed in via qrexec n_signer (%s:%s)", target_qube, service_name);
tui_print("npub: %s", g_state.npub_bech32);
(void)state_load_user_info();
if (g_state.kind0_json && strcmp(g_state.kind0_json, "{}") == 0) {
tui_print("No profile found on relays. Setting up new user...");
menu_add_new_user();
(void)state_load_user_info();
}
return;
}
if (strcmp(input, "n") == 0) {
char mnemonic[256];
char idxbuf[32];
int account = 0;
if (nostr_generate_mnemonic_and_keys(mnemonic, sizeof(mnemonic), 0, priv, pub) != 0) {
tui_begin_frame(&login_new_account_view);
tui_print("Failed to generate new seed phrase.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
tui_get_line("Enter seed phrase index (0):", idxbuf, (int)sizeof(idxbuf));
tui_begin_frame(&login_new_account_view);
(void)tui_end_frame_with_prompt("Enter seed phrase index (0):", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') {
account = atoi(idxbuf);
if (account < 0) {
account = 0;
}
if (nostr_derive_keys_from_mnemonic(mnemonic, account, priv, pub) != 0) {
tui_begin_frame(&login_new_account_view);
tui_print("Failed to derive keys from generated mnemonic.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
}
if (menu_set_keys_from_private_bytes(priv, mnemonic) != 0) {
tui_begin_frame(&login_new_account_view);
tui_print("Could not initialize keys.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
@@ -243,8 +584,9 @@ void menu_login(void) {
(void)state_load_user_info();
return;
}
tui_begin_frame(&login_privkey_view);
tui_print("Invalid nsecHex. Try again.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
}
@@ -255,8 +597,9 @@ void menu_login(void) {
(void)state_load_user_info();
return;
}
tui_begin_frame(&login_privkey_view);
tui_print("Invalid nsec. Try again.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
@@ -277,12 +620,14 @@ void menu_login(void) {
int account = 0;
if (nostr_bip39_mnemonic_validate(input) != 0) {
tui_begin_frame(&login_mnemonic_view);
tui_print("Invalid seed phrase. Try again.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
tui_get_line("Enter seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
tui_begin_frame(&login_mnemonic_view);
(void)tui_end_frame_with_prompt("Enter seed phrase index (0) >", idxbuf, (int)sizeof(idxbuf));
if (idxbuf[0] != '\0') {
account = atoi(idxbuf);
if (account < 0) {
@@ -292,17 +637,23 @@ void menu_login(void) {
if (nostr_derive_keys_from_mnemonic(input, account, priv, pub) != 0 ||
menu_set_keys_from_private_bytes(priv, input) != 0) {
tui_begin_frame(&login_mnemonic_view);
tui_print("Failed to derive keys from seed phrase.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
continue;
}
tui_begin_frame(&login_mnemonic_view);
tui_print("Using seed phrase: %s", g_state.seed_phrase);
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
(void)state_load_user_info();
return;
}
}
tui_print("Invalid login input. Enter 12 words, nsec, 64-hex, [n], or [q].");
tui_get_line(">", input, (int)sizeof(input));
tui_begin_frame(&login_view);
tui_print("Invalid login input. Choose a menu command or enter valid key/mnemonic data.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
}
}

View File

@@ -169,6 +169,10 @@ static int notif_cmp_desc_created(const void *a, const void *b) {
}
void menu_notifications(void) {
static const tui_view_t notifications_view = {
"> Main Menu > Notifications",
NULL,
};
const char **read_relays;
int relay_count = 0;
time_t now;
@@ -194,13 +198,12 @@ void menu_notifications(void) {
g_state.npub_hex,
since);
tui_clear_screen();
tui_print("NOTIFICATIONS\n");
tui_begin_frame(&notifications_view);
tui_print("Querying %d read relay(s)...", relay_count);
if (nt_query_sync(filter, read_relays, relay_count, 7000, &events, &event_count) != 0) {
tui_print("Failed to query notifications (all relays failed).");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return;
}
@@ -271,8 +274,7 @@ void menu_notifications(void) {
qsort(items, (size_t)items_count, sizeof(notif_item_t), notif_cmp_desc_created);
}
tui_clear_screen();
tui_print("NOTIFICATIONS\n");
tui_begin_frame(&notifications_view);
tui_print("%d reactions, %d replies in last 3 days", reactions, replies);
tui_print("");
@@ -305,5 +307,5 @@ void menu_notifications(void) {
notif_free_items(items, items_count);
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
}

View File

@@ -3,6 +3,7 @@
#include "net.h"
#include "publish.h"
#include "editor.h"
#include "signer.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
@@ -105,30 +106,9 @@ static char *posts_find_tag_value(cJSON *tags, const char *tag_name) {
}
static char *posts_try_decrypt_nip04(const char *sender_pub_hex, const char *ciphertext) {
unsigned char priv[32];
unsigned char sender_pub[32];
size_t out_sz;
char *out;
char *out = NULL;
if (!sender_pub_hex || !ciphertext) {
return NULL;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return NULL;
}
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
return NULL;
}
out_sz = strlen(ciphertext) + 1024U;
out = (char *)malloc(out_sz);
if (!out) {
return NULL;
}
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
free(out);
if (signer_nip04_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
return NULL;
}
@@ -136,37 +116,7 @@ static char *posts_try_decrypt_nip04(const char *sender_pub_hex, const char *cip
}
static int posts_encrypt_nip04_for_self(const char *plaintext, char **cipher_out) {
unsigned char priv[32];
unsigned char pub[32];
size_t out_sz;
char *out;
if (!plaintext || !cipher_out) {
return -1;
}
*cipher_out = NULL;
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return -1;
}
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
return -1;
}
out_sz = (strlen(plaintext) * 4U) + 256U;
out = (char *)malloc(out_sz);
if (!out) {
return -1;
}
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
free(out);
return -1;
}
*cipher_out = out;
return 0;
return signer_nip04_encrypt(g_state.npub_hex, plaintext, cipher_out);
}
static int posts_push_item(post_item_t **items, int *count, post_item_t *src) {
@@ -212,6 +162,26 @@ static void posts_format_date(long long created_at, char *buf, size_t buf_sz) {
}
}
static void posts_print_list_item_responsive(int index,
int kind,
const char *kind_label,
const char *label,
const char *date_buf,
int term_width) {
if (term_width < 80) {
tui_print("[%2d] %s", index, label ? label : "(untitled)");
tui_print(" %s %s", kind_label ? kind_label : "?", date_buf ? date_buf : "");
return;
}
tui_print("[%2d] (%d/%s) %s %s",
index,
kind,
kind_label ? kind_label : "?",
label ? label : "(untitled)",
date_buf ? date_buf : "");
}
static int posts_build_list(const char *filter_json, post_item_t **items_out, int *count_out) {
const char **read_relays;
int relay_count = 0;
@@ -329,6 +299,10 @@ static const char *posts_kind_label(int kind) {
}
static void posts_view_item(const post_item_t *it) {
static const tui_view_t post_view = {
"> Main Menu > Posts > View",
NULL,
};
char date_buf[64];
char input[16];
char *decrypted = NULL;
@@ -343,8 +317,7 @@ static void posts_view_item(const post_item_t *it) {
decrypted = posts_try_decrypt_nip04(it->pubkey, it->content);
}
tui_clear_screen();
tui_print("POST VIEW\n");
tui_begin_frame(&post_view);
tui_print("id: %s", it->id);
tui_print("kind: %d (%s)", it->kind, posts_kind_label(it->kind));
tui_print("date: %s", date_buf);
@@ -362,19 +335,35 @@ static void posts_view_item(const post_item_t *it) {
}
free(decrypted);
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
}
static void posts_delete_item(const post_item_t *it) {
static const tui_view_t post_delete = {
"> Main Menu > Posts > Delete",
NULL,
};
cJSON *tags;
cJSON *e_tag;
char input[16];
char yn[8];
int posted;
if (!it || !it->id) {
return;
}
tui_begin_frame(&post_delete);
tui_print("Delete this post from relays?");
tui_print("id: %.24s...", it->id);
(void)tui_end_frame_with_prompt("Confirm delete [y/n] >", yn, (int)sizeof(yn));
if (!(yn[0] == 'y' || yn[0] == 'Y')) {
tui_begin_frame(&post_delete);
tui_print("Delete canceled.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return;
}
tags = cJSON_CreateArray();
if (!tags) {
return;
@@ -393,16 +382,21 @@ static void posts_delete_item(const post_item_t *it) {
posted = publish_signed_event_with_report("Delete", 5, "", tags, 8000);
cJSON_Delete(tags);
tui_begin_frame(&post_delete);
if (posted < 0) {
tui_print("Delete publish failed.");
} else {
tui_print("Deletion published.");
}
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
}
static void posts_edit_item(const post_item_t *it) {
static const tui_view_t post_edit = {
"> Main Menu > Posts > Edit",
NULL,
};
char *initial = NULL;
char *edited = NULL;
char *out_content = NULL;
@@ -424,8 +418,9 @@ static void posts_edit_item(const post_item_t *it) {
}
if (!initial) {
tui_begin_frame(&post_edit);
tui_print("Failed to prepare editor content.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return;
}
@@ -433,23 +428,24 @@ static void posts_edit_item(const post_item_t *it) {
free(initial);
if (!edited) {
tui_begin_frame(&post_edit);
tui_print("Edit canceled.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return;
}
if (it->kind == 30024 || it->kind == 30078) {
if (posts_encrypt_nip04_for_self(edited, &out_content) != 0) {
tui_begin_frame(&post_edit);
tui_print("Failed to encrypt updated content.");
free(edited);
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return;
}
} else {
out_content = posts_strdup(edited);
if (!out_content) {
free(edited);
tui_get_line(">", input, (int)sizeof(input));
return;
}
}
@@ -458,8 +454,9 @@ static void posts_edit_item(const post_item_t *it) {
if (!tags_copy) {
free(edited);
free(out_content);
tui_begin_frame(&post_edit);
tui_print("Failed to duplicate tags.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
return;
}
@@ -468,33 +465,39 @@ static void posts_edit_item(const post_item_t *it) {
free(edited);
free(out_content);
tui_begin_frame(&post_edit);
if (posted < 0) {
tui_print("Post update failed.");
} else {
tui_print("Post updated.");
}
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
}
static void posts_kind_menu(const char *title, const char *filter_json) {
static const tui_view_t posts_kind_view = {
"> Main Menu > Posts > List",
NULL,
};
post_item_t *items = NULL;
int items_count = 0;
char input[32];
tui_clear_screen();
tui_print("%s\n", title);
if (posts_build_list(filter_json, &items, &items_count) != 0) {
tui_begin_frame(&posts_kind_view);
tui_print("%s", title ? title : "Posts");
tui_print("Failed to query posts (all relays failed).");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return;
}
while (1) {
int i;
int term_width = 0;
tui_clear_screen();
tui_print("%s\n", title);
tui_begin_frame(&posts_kind_view);
tui_print("%s", title ? title : "Posts");
tui_get_terminal_size(&term_width, NULL);
for (i = 0; i < items_count; i++) {
char date_buf[64];
const char *label;
@@ -504,12 +507,12 @@ static void posts_kind_menu(const char *title, const char *filter_json) {
? items[i].title
: ((items[i].dtag && items[i].dtag[0] != '\0') ? items[i].dtag : "(untitled)");
tui_print("[%2d] (%d/%s) %s %s",
i + 1,
items[i].kind,
posts_kind_label(items[i].kind),
label,
date_buf);
posts_print_list_item_responsive(i + 1,
items[i].kind,
posts_kind_label(items[i].kind),
label,
date_buf,
term_width);
}
if (items_count == 0) {
@@ -517,10 +520,15 @@ static void posts_kind_menu(const char *title, const char *filter_json) {
}
tui_print("");
tui_print("Commands: v <n> (view), e <n> (edit), d <n> (delete), x (back)");
tui_get_line(">", input, (int)sizeof(input));
tui_print_menu_item("^_V^:iew <n>");
tui_print_menu_item("^_E^:dit <n>");
tui_print_menu_item("^_D^:elete <n>");
tui_print_menu_item("^_X^:Exit");
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
@@ -547,43 +555,48 @@ static void posts_kind_menu(const char *title, const char *filter_json) {
}
void menu_posts(void) {
static const tui_view_t posts_view = {
"> Main Menu > Posts",
NULL,
};
char input[32];
char filter[512];
while (1) {
tui_clear_screen();
tui_print("POSTS\n");
tui_print("1 - Private blog (kind 30024)");
tui_print("2 - Public blog (kind 30023)");
tui_print("3 - Encrypted data (kind 30078)");
tui_print("4 - All posts");
tui_print("x - Back");
tui_begin_frame(&posts_view);
tui_print_menu_item("^_S^:ecret blog (kind 30024)");
tui_print_menu_item("^_P^:ublic blog (kind 30023)");
tui_print_menu_item("^_E^:ncrypted data (kind 30078)");
tui_print_menu_item("^_A^:ll posts");
tui_print_menu_item("^_X^:Exit");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
if (input[0] == 'b' || input[0] == 'B' ||
input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
if (input[0] == '1') {
if (input[0] == 's' || input[0] == 'S') {
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[30024],\"limit\":200}",
g_state.npub_hex);
posts_kind_menu("PRIVATE BLOG", filter);
} else if (input[0] == '2') {
} else if (input[0] == 'p' || input[0] == 'P') {
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[30023],\"limit\":200}",
g_state.npub_hex);
posts_kind_menu("PUBLIC BLOG", filter);
} else if (input[0] == '3') {
} else if (input[0] == 'e' || input[0] == 'E') {
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"limit\":200}",
g_state.npub_hex);
posts_kind_menu("ENCRYPTED DATA", filter);
} else if (input[0] == '4') {
} else if (input[0] == 'a' || input[0] == 'A') {
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[30023,30024,30078],\"limit\":300}",

View File

@@ -1,6 +1,7 @@
#include "tui.h"
#include "state.h"
#include "net.h"
#include "signer.h"
#include "publish.h"
@@ -15,6 +16,10 @@ static int profile_publish_kind0(const char *content_json) {
}
void menu_profile(void) {
static const tui_view_t profile_view = {
"> Main Menu > Profile",
NULL,
};
char menu_sel[32];
char edit_buf[512];
@@ -23,11 +28,36 @@ void menu_profile(void) {
const char *fields[] = {"name", "about", "picture", "banner", "website", "lud16", "nip05"};
int i;
tui_clear_screen();
tui_print("PROFILE\n");
tui_begin_frame(&profile_view);
tui_print("nsecHex: %s", g_state.nsec_hex);
tui_print("nsec: %s", g_state.nsec_bech32);
if (g_signer.kind == NT_SIGNER_NSIGNER) {
tui_print("Signer: n_signer (@%s)", g_signer.socket_name);
if (g_signer.selector.has_nostr_index) {
tui_print("Signer selector: nostr_index=%d", g_signer.selector.nostr_index);
} else {
tui_print("Signer selector: role=%s", g_signer.selector.role[0] ? g_signer.selector.role : "main");
}
} else if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
tui_print("Signer: n_signer qrexec");
tui_print("Signer target: %s", g_signer.qrexec_target);
tui_print("Signer service: %s", g_signer.qrexec_service);
if (g_signer.selector.has_nostr_index) {
tui_print("Signer selector: nostr_index=%d", g_signer.selector.nostr_index);
} else {
tui_print("Signer selector: role=%s", g_signer.selector.role[0] ? g_signer.selector.role : "main");
}
} else if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
tui_print("Signer: n_signer URL");
tui_print("Signer endpoint: %s", g_signer.endpoint_url);
if (g_signer.selector.has_nostr_index) {
tui_print("Signer selector: nostr_index=%d", g_signer.selector.nostr_index);
} else {
tui_print("Signer selector: role=%s", g_signer.selector.role[0] ? g_signer.selector.role : "main");
}
} else {
tui_print("nsecHex: %s", g_state.nsec_hex);
tui_print("nsec: %s", g_state.nsec_bech32);
}
tui_print("npubHex: %s", g_state.npub_hex);
tui_print("npub: %s", g_state.npub_bech32);
tui_print("");
@@ -42,12 +72,12 @@ void menu_profile(void) {
tui_print("%-18s %s", fields[i], (cJSON_IsString(v) && v->valuestring) ? v->valuestring : "");
}
tui_print("\n");
tui_print("^_M^:odify account");
tui_print("^_P^:ost changes and exit.");
tui_print("E^_x^:it without saving");
tui_print("");
tui_print_menu_item("^_M^:odify account");
tui_print_menu_item("^_P^:ost changes and exit.");
tui_print_menu_item("E^_x^:it without saving");
tui_get_line(">", menu_sel, (int)sizeof(menu_sel));
(void)tui_end_frame_with_prompt(">", menu_sel, (int)sizeof(menu_sel));
if (menu_sel[0] == 'm' || menu_sel[0] == 'M') {
for (i = 0; i < (int)(sizeof(fields) / sizeof(fields[0])); i++) {

View File

@@ -95,7 +95,21 @@ static void relays_print_nip11_summary(const char *nip11_json) {
cJSON_Delete(doc);
}
static void relays_print_row_responsive(int term_width, int index, const char *url, const char *rw) {
if (term_width < 90) {
tui_print("[%2d] %s", index, url ? url : "");
tui_print(" %s", rw ? rw : "");
return;
}
tui_print("[%2d] %-30s %s", index, url ? url : "", rw ? rw : "");
}
void menu_relays(void) {
static const tui_view_t relays_view = {
"> Main Menu > Relays",
NULL,
};
cJSON *working = NULL;
cJSON *tags = NULL;
char input[512];
@@ -118,9 +132,10 @@ void menu_relays(void) {
while (1) {
int i;
int n;
int term_width = 0;
tui_clear_screen();
tui_print("RELAYS\n");
tui_begin_frame(&relays_view);
tui_get_terminal_size(&term_width, NULL);
n = cJSON_GetArraySize(tags);
for (i = 0; i < n; i++) {
@@ -138,17 +153,20 @@ void menu_relays(void) {
rw = t2->valuestring;
}
tui_print("[%2d] %-30s %s", i + 1, (cJSON_IsString(t1) && t1->valuestring) ? t1->valuestring : "", rw);
relays_print_row_responsive(term_width,
i + 1,
(cJSON_IsString(t1) && t1->valuestring) ? t1->valuestring : "",
rw);
}
tui_print("\n\n");
tui_print("^_A^:dd relay");
tui_print("^_D^:elete relay");
tui_print("^_M^:odify relay");
tui_print("^_P^:ost changes and exit.");
tui_print("E^_x^:it without saving");
tui_print("");
tui_print_menu_item("^_A^:dd relay");
tui_print_menu_item("^_D^:elete relay");
tui_print_menu_item("^_M^:odify relay");
tui_print_menu_item("^_P^:ost changes and exit.");
tui_print_menu_item("E^_x^:it without saving");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'a' || input[0] == 'A') {
char relay_url[256];
@@ -157,12 +175,14 @@ void menu_relays(void) {
char write_yn[32];
char *nip11 = NULL;
tui_print("\n\nADD RELAY\n");
tui_get_line("Relay URL >", relay_url, (int)sizeof(relay_url));
tui_begin_frame(&relays_view);
tui_print("Add relay");
(void)tui_end_frame_with_prompt("Relay URL >", relay_url, (int)sizeof(relay_url));
if (relay_url[0] == '\0') {
continue;
}
tui_begin_frame(&relays_view);
tui_print("Gathering relay data ...");
if (nt_fetch_nip11(relay_url, &nip11) == 0 && nip11) {
relays_print_nip11_summary(nip11);
@@ -171,14 +191,14 @@ void menu_relays(void) {
tui_print("NIP-11 fetch failed.");
}
tui_get_line("Add relay [y/n] >", yn, (int)sizeof(yn));
(void)tui_end_frame_with_prompt("Add relay [y/n] >", yn, (int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON *tag = cJSON_CreateArray();
cJSON_AddItemToArray(tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(tag, cJSON_CreateString(relay_url));
tui_get_line("Read from relay [y/n] >", read_yn, (int)sizeof(read_yn));
tui_get_line("Write to relay [y/n] >", write_yn, (int)sizeof(write_yn));
(void)tui_end_frame_with_prompt("Read from relay [y/n] >", read_yn, (int)sizeof(read_yn));
(void)tui_end_frame_with_prompt("Write to relay [y/n] >", write_yn, (int)sizeof(write_yn));
if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) {
cJSON_AddItemToArray(tag, cJSON_CreateString("write"));
@@ -194,7 +214,8 @@ void menu_relays(void) {
cJSON *tag;
cJSON *url;
tui_get_line("relay to delete >", numbuf, (int)sizeof(numbuf));
tui_begin_frame(&relays_view);
(void)tui_end_frame_with_prompt("relay to delete >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue;
@@ -202,12 +223,16 @@ void menu_relays(void) {
tag = cJSON_GetArrayItem(tags, idx);
url = cJSON_GetArrayItem(tag, 1);
tui_get_line("Delete selected relay [y/n] >", yn, (int)sizeof(yn));
(void)tui_end_frame_with_prompt("Delete selected relay [y/n] >", yn, (int)sizeof(yn));
if (yn[0] == 'y' || yn[0] == 'Y') {
cJSON_DeleteItemFromArray(tags, idx);
tui_begin_frame(&relays_view);
if (cJSON_IsString(url) && url->valuestring) {
tui_print("Deleted %s", url->valuestring);
} else {
tui_print("Deleted relay.");
}
(void)tui_end_frame_with_prompt("Press Enter to continue >", input, (int)sizeof(input));
}
} else if (input[0] == 'm' || input[0] == 'M') {
char numbuf[32];
@@ -218,7 +243,8 @@ void menu_relays(void) {
cJSON *url;
cJSON *new_tag;
tui_get_line("# relay to modify >", numbuf, (int)sizeof(numbuf));
tui_begin_frame(&relays_view);
(void)tui_end_frame_with_prompt("# relay to modify >", numbuf, (int)sizeof(numbuf));
idx = atoi(numbuf) - 1;
if (idx < 0 || idx >= cJSON_GetArraySize(tags)) {
continue;
@@ -234,8 +260,8 @@ void menu_relays(void) {
cJSON_AddItemToArray(new_tag, cJSON_CreateString("r"));
cJSON_AddItemToArray(new_tag, cJSON_CreateString(url->valuestring));
tui_get_line("Read from relay [y/n]?", read_yn, (int)sizeof(read_yn));
tui_get_line("Write to relay [y/n]?", write_yn, (int)sizeof(write_yn));
(void)tui_end_frame_with_prompt("Read from relay [y/n]?", read_yn, (int)sizeof(read_yn));
(void)tui_end_frame_with_prompt("Write to relay [y/n]?", write_yn, (int)sizeof(write_yn));
if ((read_yn[0] == 'n' || read_yn[0] == 'N') && (write_yn[0] == 'y' || write_yn[0] == 'Y')) {
cJSON_AddItemToArray(new_tag, cJSON_CreateString("write"));
@@ -247,17 +273,23 @@ void menu_relays(void) {
} else if (input[0] == 'p' || input[0] == 'P') {
int posted = relays_publish_kind10002(working);
char *new_json = cJSON_PrintUnformatted(working);
if (new_json) {
free(g_state.kind10002_json);
g_state.kind10002_json = new_json;
state_parse_relay_list();
}
tui_begin_frame(&relays_view);
if (posted < 0) {
tui_print("Relay list publish failed.");
} else {
tui_print("Relay list published.");
}
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
break;
} else if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
} else if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
}

View File

@@ -2,6 +2,7 @@
#include "state.h"
#include "net.h"
#include "publish.h"
#include "signer.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
@@ -86,64 +87,13 @@ static int todo_parse_index_1based(const char *input, int count) {
}
static int todo_encrypt_for_self(const char *plaintext, char **cipher_out) {
unsigned char priv[32];
unsigned char pub[32];
size_t out_sz;
char *out;
if (!plaintext || !cipher_out) {
return -1;
}
*cipher_out = NULL;
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return -1;
}
if (nostr_hex_to_bytes(g_state.npub_hex, pub, 32) != 0) {
return -1;
}
out_sz = (strlen(plaintext) * 4U) + 256U;
out = (char *)malloc(out_sz);
if (!out) {
return -1;
}
if (nostr_nip04_encrypt(priv, pub, plaintext, out, out_sz) != 0) {
free(out);
return -1;
}
*cipher_out = out;
return 0;
return signer_nip44_self_encrypt(plaintext, cipher_out);
}
static char *todo_decrypt_from_pub(const char *sender_pub_hex, const char *ciphertext) {
unsigned char priv[32];
unsigned char sender_pub[32];
size_t out_sz;
char *out;
char *out = NULL;
if (!sender_pub_hex || !ciphertext) {
return NULL;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
return NULL;
}
if (nostr_hex_to_bytes(sender_pub_hex, sender_pub, 32) != 0) {
return NULL;
}
out_sz = strlen(ciphertext) + 2048U;
out = (char *)malloc(out_sz);
if (!out) {
return NULL;
}
if (nostr_nip04_decrypt(priv, sender_pub, ciphertext, out, out_sz) != 0) {
free(out);
if (signer_nip44_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
return NULL;
}
@@ -351,6 +301,7 @@ static void todo_toggle_item(todo_item_t *items, int count) {
static void todo_delete_item(todo_item_t *items, int *count) {
char input[64];
char yn[8];
int idx;
int i;
@@ -365,6 +316,12 @@ static void todo_delete_item(todo_item_t *items, int *count) {
return;
}
tui_get_line("Delete selected item [y/n] >", yn, (int)sizeof(yn));
if (!(yn[0] == 'y' || yn[0] == 'Y')) {
tui_print("Delete canceled.");
return;
}
free(items[idx].text);
for (i = idx; i < (*count - 1); i++) {
items[i] = items[i + 1];
@@ -439,29 +396,40 @@ static int todo_publish(const todo_item_t *items, int count) {
}
void menu_todo(void) {
static const tui_view_t todo_view = {
"> Main Menu > Todo",
NULL,
};
todo_item_t *items = NULL;
int count = 0;
char input[32];
if (todo_load_remote(&items, &count) != 0) {
tui_print("Failed to load remote todo list; starting empty.");
items = NULL;
count = 0;
}
while (1) {
tui_clear_screen();
todo_show_list(items, count);
tui_print("a - Add item");
tui_print("c - Complete/toggle item");
tui_print("d - Delete item");
tui_print("r - Reorder (swap two)");
tui_print("p - Post/save changes");
tui_print("x - Exit without saving");
tui_begin_frame(&todo_view);
if (count == 0) {
tui_print("TODO LIST");
tui_print("(empty)");
tui_print("");
} else {
todo_show_list(items, count);
}
tui_get_line(">", input, (int)sizeof(input));
tui_print_menu_item("^_A^:dd item");
tui_print_menu_item("^_C^:omplete/toggle item");
tui_print_menu_item("^_D^:elete item");
tui_print_menu_item("^_R^:eorder (swap two)");
tui_print_menu_item("^_P^:ost/save changes");
tui_print_menu_item("E^_x^:it without saving");
if (input[0] == 'x' || input[0] == 'X' || input[0] == 'q' || input[0] == 'Q') {
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 'x' || input[0] == 'X' ||
input[0] == 'q' || input[0] == 'Q') {
break;
}
@@ -476,7 +444,9 @@ void menu_todo(void) {
} else if (input[0] == 'p' || input[0] == 'P') {
char hold[8];
(void)todo_publish(items, count);
tui_get_line(">", hold, (int)sizeof(hold));
tui_begin_frame(&todo_view);
tui_print("Todo publish attempted.");
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
}

View File

@@ -8,20 +8,28 @@
#include <string.h>
void menu_tweet(void) {
static const tui_view_t tweet_view = {
"> Main Menu > Tweet",
NULL,
};
char text[4096];
int posted;
tui_clear_screen();
tui_print("TWEET\n");
tui_get_line("tweet >", text, (int)sizeof(text));
tui_begin_frame(&tweet_view);
tui_print("Compose a short post.");
tui_print("");
(void)tui_end_frame_with_prompt("tweet >", text, (int)sizeof(text));
if (text[0] == '\0') {
return;
}
posted = publish_signed_event_with_report("Tweet", 1, text, NULL, 8000);
tui_begin_frame(&tweet_view);
if (posted < 0) {
tui_print("Tweet publish failed.");
} else {
tui_print("Tweet published.");
}
tui_get_line(">", text, (int)sizeof(text));
(void)tui_end_frame_with_prompt("Press Enter to exit >", text, (int)sizeof(text));
}

View File

@@ -39,35 +39,40 @@ static cJSON *write_make_blog_tags(const char *d_tag_value, const char *title_op
}
void menu_write(void) {
static const tui_view_t write_view = {
"> Main Menu > Write",
NULL,
};
char *content;
char input[64];
tui_clear_screen();
tui_print("WRITE\n");
tui_begin_frame(&write_view);
tui_print("Launching external editor...");
content = editor_launch(NULL);
if (!content) {
tui_begin_frame(&write_view);
tui_print("No content created.");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return;
}
tui_clear_screen();
tui_print("WRITE RESULT\n");
tui_begin_frame(&write_view);
tui_print("Content length: %d bytes", (int)strlen(content));
tui_print("\n");
tui_print("^_T^:weet it");
tui_print("^_B^:log post (kind 30023)");
tui_print("^_D^:iary entry (kind 30024)");
tui_print("^_X^: discard");
tui_print("");
tui_print_menu_item("^_T^:weet it");
tui_print_menu_item("^_B^:log post (kind 30023)");
tui_print_menu_item("^_D^:iary entry (kind 30024)");
tui_print_menu_item("^_X^:Exit without saving");
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt(">", input, (int)sizeof(input));
if (input[0] == 't' || input[0] == 'T') {
int posted = publish_signed_event_with_report("Tweet", 1, content, NULL, 8000);
tui_begin_frame(&write_view);
if (posted < 0) {
tui_print("Tweet publish failed.");
} else {
tui_print("Tweet published.");
}
} else if (input[0] == 'b' || input[0] == 'B') {
char title[256];
@@ -76,11 +81,14 @@ void menu_write(void) {
cJSON *tags;
int posted;
tui_get_line("title >", title, (int)sizeof(title));
tui_get_line("slug (d tag) [empty uses title] >", slug, (int)sizeof(slug));
tui_begin_frame(&write_view);
(void)tui_end_frame_with_prompt("title >", title, (int)sizeof(title));
(void)tui_end_frame_with_prompt("slug (d tag) [empty uses title] >", slug, (int)sizeof(slug));
d_val = (slug[0] != '\0') ? slug : title;
tags = write_make_blog_tags(d_val, title);
tui_begin_frame(&write_view);
if (!tags) {
tui_print("Failed to build blog tags.");
} else {
@@ -88,6 +96,8 @@ void menu_write(void) {
cJSON_Delete(tags);
if (posted < 0) {
tui_print("Blog publish failed.");
} else {
tui_print("Blog post published.");
}
}
} else if (input[0] == 'd' || input[0] == 'D') {
@@ -98,17 +108,18 @@ void menu_write(void) {
int posted;
memset(&tmv, 0, sizeof(tmv));
tui_begin_frame(&write_view);
if (!localtime_r(&now, &tmv)) {
tui_print("Failed to get local date.");
free(content);
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return;
}
if (strftime(date_d, sizeof(date_d), "%Y%m%d", &tmv) == 0) {
tui_print("Failed to format diary date.");
free(content);
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
return;
}
@@ -120,12 +131,15 @@ void menu_write(void) {
cJSON_Delete(tags);
if (posted < 0) {
tui_print("Diary publish failed.");
} else {
tui_print("Diary entry published.");
}
}
} else {
tui_begin_frame(&write_view);
tui_print("Discarded.");
}
free(content);
tui_get_line(">", input, (int)sizeof(input));
(void)tui_end_frame_with_prompt("Press Enter to exit >", input, (int)sizeof(input));
}

118
src/net.c
View File

@@ -2,11 +2,13 @@
#include "db.h"
#include "state.h"
#include "signer.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip042.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include "../resources/nostr_core_lib/nostr_websocket/nostr_websocket_tls.h"
#include "../resources/nostr_core_lib/nostr_core/nostr_core.h"
#include <curl/curl.h>
#include <errno.h>
@@ -18,7 +20,7 @@
#include <unistd.h>
#define NT_SUB_ID "sub1"
#define NT_WS_BUFFER_SIZE 65536
#define NT_WS_BUFFER_SIZE 262144
static long long nt_now_ms(void) {
struct timeval tv;
@@ -244,7 +246,7 @@ static int nt_send_nip42_auth(nostr_ws_client_t *client,
*auth_event_id_out = NULL;
if (nostr_hex_to_bytes(g_state.nsec_hex, private_key, sizeof(private_key)) != 0) {
if (signer_get_local_private_key(private_key) != 0) {
return -1;
}
@@ -528,7 +530,7 @@ void nt_publish_results_free(nt_publish_result_t *results, int result_count) {
}
/* Query one relay and append deduplicated EVENT payloads until EOSE/timeout. */
static int nt_query_one_relay(const char *relay_url,
static __attribute__((unused)) int nt_query_one_relay(const char *relay_url,
cJSON *filter,
int timeout_ms,
int verbose,
@@ -684,13 +686,12 @@ int nt_query_sync_verbose(const char *filter_json,
char ***events_out,
int *event_count) {
cJSON *filter = NULL;
nostr_relay_pool_t *pool = NULL;
cJSON **pool_events = NULL;
int pool_event_count = 0;
char **events = NULL;
char **seen_ids = NULL;
int events_n = 0;
int events_cap = 0;
int seen_n = 0;
int seen_cap = 0;
int success_relays = 0;
int i;
if (!events_out || !event_count || !filter_json || !relay_urls || relay_count <= 0) {
@@ -707,59 +708,62 @@ int nt_query_sync_verbose(const char *filter_json,
return -1;
}
for (i = 0; i < relay_count; i++) {
const char *relay = relay_urls[i];
int rc;
const char *reason = "unknown";
if (!relay || relay[0] == '\0') {
continue;
}
if (verbose && phase_label) {
fprintf(stderr, "%s: attempting relay %d/%d %s\n", phase_label, i + 1, relay_count, relay);
}
rc = nt_query_one_relay(relay,
filter,
timeout_ms,
verbose,
phase_label,
&events,
&events_n,
&events_cap,
&seen_ids,
&seen_n,
&seen_cap,
&reason);
if (rc < 0) {
if (verbose && phase_label) {
fprintf(stderr, "%s: failed %s (%s)\n", phase_label, relay, reason ? reason : "error");
}
nt_free_string_array(events, events_n);
nt_free_string_array(seen_ids, seen_n);
cJSON_Delete(filter);
return -1;
}
if (rc > 0) {
success_relays++;
if (verbose && phase_label) {
fprintf(stderr, "%s: connected %s (%s)\n", phase_label, relay, reason ? reason : "ok");
}
} else if (verbose && phase_label) {
fprintf(stderr, "%s: failed %s (%s)\n", phase_label, relay, reason ? reason : "failed");
}
}
nt_free_string_array(seen_ids, seen_n);
cJSON_Delete(filter);
if (success_relays == 0) {
nt_free_string_array(events, events_n);
pool = nostr_relay_pool_create(NULL);
if (!pool) {
cJSON_Delete(filter);
fprintf(stderr, "query: failed to create relay pool\n");
return -1;
}
if (verbose && phase_label) {
fprintf(stderr,
"%s: querying %d relays via nostr_core relay pool\n",
phase_label,
relay_count);
}
pool_events = nostr_relay_pool_query_sync(pool,
relay_urls,
relay_count,
filter,
&pool_event_count,
timeout_ms);
cJSON_Delete(filter);
filter = NULL;
if (!pool_events || pool_event_count <= 0) {
nostr_relay_pool_destroy(pool);
return -1;
}
for (i = 0; i < pool_event_count; i++) {
char *event_str = cJSON_PrintUnformatted(pool_events[i]);
if (!event_str || nt_push_string(&events, &events_n, &events_cap, event_str) != 0) {
free(event_str);
nt_free_string_array(events, events_n);
for (i = 0; i < pool_event_count; i++) {
cJSON_Delete(pool_events[i]);
}
free(pool_events);
nostr_relay_pool_destroy(pool);
return -1;
}
}
for (i = 0; i < pool_event_count; i++) {
cJSON_Delete(pool_events[i]);
}
free(pool_events);
if (verbose && phase_label) {
fprintf(stderr,
"%s: relay pool collected %d unique events\n",
phase_label,
events_n);
}
nostr_relay_pool_destroy(pool);
*events_out = events;
*event_count = events_n;
return 0;

109
src/nsigner_auth.c Normal file
View File

@@ -0,0 +1,109 @@
#include "nsigner_auth.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define NT_NSIGNER_AUTH_EVENT_KIND 27235
static cJSON *nt_nsigner_auth_build_tag_pair(const char *name, const char *value) {
cJSON *pair;
if (!name || !value) {
return NULL;
}
pair = cJSON_CreateArray();
if (!pair) {
return NULL;
}
cJSON_AddItemToArray(pair, cJSON_CreateString(name));
cJSON_AddItemToArray(pair, cJSON_CreateString(value));
return pair;
}
static int nt_nsigner_auth_compute_params_hash_hex(const cJSON *params, char out_hex_65[65]) {
char *params_json = NULL;
unsigned char hash[32];
int rc = -1;
if (!out_hex_65) {
return -1;
}
if (params) {
params_json = cJSON_PrintUnformatted((cJSON *)params);
} else {
params_json = strdup("null");
}
if (!params_json) {
return -1;
}
if (nostr_sha256((const unsigned char *)params_json, strlen(params_json), hash) != 0) {
goto cleanup;
}
nostr_bytes_to_hex(hash, sizeof(hash), out_hex_65);
out_hex_65[64] = '\0';
rc = 0;
cleanup:
free(params_json);
return rc;
}
int nt_nsigner_auth_build(const char *request_id,
const char *method,
const cJSON *params,
const nt_nsigner_auth_ctx_t *auth_ctx,
time_t created_at,
cJSON **out_auth) {
char body_hash_hex[65];
cJSON *tags = NULL;
cJSON *auth_event = NULL;
if (!request_id || request_id[0] == '\0' ||
!method || method[0] == '\0' ||
!auth_ctx || !auth_ctx->enabled ||
!out_auth) {
return -1;
}
*out_auth = NULL;
if (nt_nsigner_auth_compute_params_hash_hex(params, body_hash_hex) != 0) {
return -1;
}
tags = cJSON_CreateArray();
if (!tags) {
return -1;
}
cJSON_AddItemToArray(tags, nt_nsigner_auth_build_tag_pair("nsigner_rpc", request_id));
cJSON_AddItemToArray(tags, nt_nsigner_auth_build_tag_pair("nsigner_method", method));
cJSON_AddItemToArray(tags, nt_nsigner_auth_build_tag_pair("nsigner_body_hash", body_hash_hex));
if (created_at <= 0) {
created_at = time(NULL);
}
auth_event = nostr_create_and_sign_event(NT_NSIGNER_AUTH_EVENT_KIND,
auth_ctx->label,
tags,
auth_ctx->privkey,
created_at);
if (!auth_event) {
cJSON_Delete(tags);
return -1;
}
*out_auth = auth_event;
return 0;
}

1337
src/nsigner_client.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,7 @@
#include "net.h"
#include "state.h"
#include "tui.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include "signer.h"
#include <stdio.h>
#include <stdlib.h>
@@ -16,9 +14,6 @@ int publish_signed_event_with_report(const char *label,
const char *content,
cJSON *tags,
int timeout_ms) {
unsigned char priv[32];
cJSON *tags_copy = NULL;
cJSON *event = NULL;
char *event_json = NULL;
const char **write_relays;
int relay_count = 0;
@@ -31,31 +26,8 @@ int publish_signed_event_with_report(const char *label,
label = "Event";
}
if (nostr_hex_to_bytes(g_state.nsec_hex, priv, 32) != 0) {
tui_print("%s: invalid private key in state.", label);
return -1;
}
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
if (!tags_copy) {
tui_print("%s: failed to prepare tags.", label);
return -1;
}
event = nostr_create_and_sign_event(kind,
content ? content : "",
tags_copy,
priv,
time(NULL));
cJSON_Delete(tags_copy);
if (!event) {
tui_print("%s: failed to create signed event.", label);
return -1;
}
event_json = cJSON_PrintUnformatted(event);
cJSON_Delete(event);
if (!event_json) {
if (signer_create_and_sign(kind, content ? content : "", tags, time(NULL), &event_json) != 0 ||
!event_json) {
tui_print("%s: failed to serialize event.", label);
return -1;
}

722
src/signer.c Normal file
View File

@@ -0,0 +1,722 @@
#include "signer.h"
#include "state.h"
#include "nsigner_client.h"
#include "../resources/nostr_core_lib/nostr_core/nip001.h"
#include "../resources/nostr_core_lib/nostr_core/nip004.h"
#include "../resources/nostr_core_lib/nostr_core/nip044.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
nt_signer_t g_signer = {
.kind = NT_SIGNER_LOCAL,
.socket_name = {0},
.endpoint_url = {0},
.url_session_fd = -1,
.selector = {
.has_nostr_index = 1,
.nostr_index = 0,
.role = {0}
},
.url_auth = {
.enabled = 0,
.privkey = {0},
.label = {0}
}
};
static int signer_local_private_key(unsigned char out_priv[32]) {
if (!out_priv) {
return -1;
}
if (nostr_hex_to_bytes(g_state.nsec_hex, out_priv, 32) != 0) {
return -1;
}
return 0;
}
void signer_set_selector(int has_index, int index, const char *role) {
g_signer.selector.has_nostr_index = has_index ? 1 : 0;
g_signer.selector.nostr_index = (index < 0) ? 0 : index;
if (role && role[0] != '\0') {
snprintf(g_signer.selector.role, sizeof(g_signer.selector.role), "%s", role);
} else {
g_signer.selector.role[0] = '\0';
}
}
void signer_init(void) {
nt_nsigner_selector_t sel = nsigner_selector_default();
if (g_signer.url_session_fd >= 0) {
nsigner_session_close(g_signer.url_session_fd);
g_signer.url_session_fd = -1;
}
g_signer.kind = NT_SIGNER_LOCAL;
g_signer.socket_name[0] = '\0';
g_signer.qrexec_target[0] = '\0';
g_signer.qrexec_service[0] = '\0';
g_signer.endpoint_url[0] = '\0';
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
g_signer.url_auth.enabled = 0;
memset(g_signer.url_auth.privkey, 0, sizeof(g_signer.url_auth.privkey));
g_signer.url_auth.label[0] = '\0';
}
void signer_shutdown(void) {
memset(g_signer.url_auth.privkey, 0, sizeof(g_signer.url_auth.privkey));
signer_init();
}
int signer_init_local(void) {
signer_init();
return 0;
}
int signer_init_nsigner(const char *socket_name, const nt_nsigner_selector_t *selector) {
nt_nsigner_selector_t sel = selector ? *selector : nsigner_selector_default();
if (!socket_name || socket_name[0] == '\0') {
return -1;
}
g_signer.kind = NT_SIGNER_NSIGNER;
snprintf(g_signer.socket_name, sizeof(g_signer.socket_name), "%s", socket_name);
g_signer.qrexec_target[0] = '\0';
g_signer.qrexec_service[0] = '\0';
g_signer.endpoint_url[0] = '\0';
if (g_signer.url_session_fd >= 0) {
nsigner_session_close(g_signer.url_session_fd);
g_signer.url_session_fd = -1;
}
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
return 0;
}
int signer_init_nsigner_qrexec(const char *target_qube, const char *service_name, const nt_nsigner_selector_t *selector) {
nt_nsigner_selector_t sel = selector ? *selector : nsigner_selector_default();
if (!target_qube || target_qube[0] == '\0' || !service_name || service_name[0] == '\0') {
return -1;
}
g_signer.kind = NT_SIGNER_NSIGNER_QREXEC;
g_signer.socket_name[0] = '\0';
snprintf(g_signer.qrexec_target, sizeof(g_signer.qrexec_target), "%s", target_qube);
snprintf(g_signer.qrexec_service, sizeof(g_signer.qrexec_service), "%s", service_name);
g_signer.endpoint_url[0] = '\0';
if (g_signer.url_session_fd >= 0) {
nsigner_session_close(g_signer.url_session_fd);
g_signer.url_session_fd = -1;
}
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
return 0;
}
int signer_init_nsigner_url_with_session_auth(const char *endpoint_url,
const nt_nsigner_selector_t *selector,
int session_fd,
const nt_nsigner_auth_ctx_t *auth_or_null) {
nt_nsigner_selector_t sel = selector ? *selector : nsigner_selector_default();
if (!endpoint_url || endpoint_url[0] == '\0') {
if (session_fd >= 0) {
nsigner_session_close(session_fd);
}
return -1;
}
g_signer.kind = NT_SIGNER_NSIGNER_URL;
g_signer.socket_name[0] = '\0';
g_signer.qrexec_target[0] = '\0';
g_signer.qrexec_service[0] = '\0';
snprintf(g_signer.endpoint_url, sizeof(g_signer.endpoint_url), "%s", endpoint_url);
if (g_signer.url_session_fd >= 0 && g_signer.url_session_fd != session_fd) {
nsigner_session_close(g_signer.url_session_fd);
}
g_signer.url_session_fd = session_fd;
if (g_signer.url_session_fd < 0) {
g_signer.url_session_fd = nsigner_session_open_url(g_signer.endpoint_url);
}
signer_set_selector(sel.has_nostr_index, sel.nostr_index, sel.role);
g_signer.url_auth.enabled = 0;
memset(g_signer.url_auth.privkey, 0, sizeof(g_signer.url_auth.privkey));
g_signer.url_auth.label[0] = '\0';
if (auth_or_null && auth_or_null->enabled) {
g_signer.url_auth.enabled = 1;
memcpy(g_signer.url_auth.privkey, auth_or_null->privkey, sizeof(g_signer.url_auth.privkey));
snprintf(g_signer.url_auth.label, sizeof(g_signer.url_auth.label), "%s", auth_or_null->label);
}
return 0;
}
int signer_init_nsigner_url_with_session(const char *endpoint_url,
const nt_nsigner_selector_t *selector,
int session_fd) {
return signer_init_nsigner_url_with_session_auth(endpoint_url, selector, session_fd, NULL);
}
int signer_init_nsigner_url(const char *endpoint_url, const nt_nsigner_selector_t *selector) {
return signer_init_nsigner_url_with_session_auth(endpoint_url, selector, -1, NULL);
}
int signer_get_local_private_key(unsigned char out_priv[32]) {
if (!out_priv) {
return -1;
}
if (g_signer.kind != NT_SIGNER_LOCAL) {
return -1;
}
return signer_local_private_key(out_priv);
}
int signer_create_and_sign(int kind,
const char *content,
cJSON *tags,
time_t timestamp,
char **signed_event_json_out) {
unsigned char priv[32];
cJSON *tags_copy;
cJSON *event;
if (!signed_event_json_out) {
return -1;
}
*signed_event_json_out = NULL;
if (g_signer.kind == NT_SIGNER_NSIGNER) {
cJSON *unsigned_event = NULL;
cJSON *tags_copy = NULL;
char *unsigned_json = NULL;
int rc = -1;
unsigned_event = cJSON_CreateObject();
if (!unsigned_event) {
return -1;
}
if (!cJSON_AddNumberToObject(unsigned_event, "kind", kind) ||
!cJSON_AddStringToObject(unsigned_event, "content", content ? content : "")) {
cJSON_Delete(unsigned_event);
return -1;
}
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
if (!tags_copy) {
cJSON_Delete(unsigned_event);
return -1;
}
if (!cJSON_AddItemToObject(unsigned_event, "tags", tags_copy)) {
cJSON_Delete(tags_copy);
cJSON_Delete(unsigned_event);
return -1;
}
if (!cJSON_AddNumberToObject(unsigned_event, "created_at", (double)timestamp) ||
!cJSON_AddStringToObject(unsigned_event, "pubkey", g_state.npub_hex)) {
cJSON_Delete(unsigned_event);
return -1;
}
unsigned_json = cJSON_PrintUnformatted(unsigned_event);
if (!unsigned_json) {
cJSON_Delete(unsigned_event);
return -1;
}
rc = nsigner_sign_event(g_signer.socket_name,
&g_signer.selector,
unsigned_json,
signed_event_json_out);
free(unsigned_json);
cJSON_Delete(unsigned_event);
return rc;
}
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
cJSON *unsigned_event = NULL;
cJSON *tags_copy = NULL;
char *unsigned_json = NULL;
int rc = -1;
unsigned_event = cJSON_CreateObject();
if (!unsigned_event) {
return -1;
}
if (!cJSON_AddNumberToObject(unsigned_event, "kind", kind) ||
!cJSON_AddStringToObject(unsigned_event, "content", content ? content : "")) {
cJSON_Delete(unsigned_event);
return -1;
}
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
if (!tags_copy) {
cJSON_Delete(unsigned_event);
return -1;
}
if (!cJSON_AddItemToObject(unsigned_event, "tags", tags_copy)) {
cJSON_Delete(tags_copy);
cJSON_Delete(unsigned_event);
return -1;
}
if (!cJSON_AddNumberToObject(unsigned_event, "created_at", (double)timestamp) ||
!cJSON_AddStringToObject(unsigned_event, "pubkey", g_state.npub_hex)) {
cJSON_Delete(unsigned_event);
return -1;
}
unsigned_json = cJSON_PrintUnformatted(unsigned_event);
if (!unsigned_json) {
cJSON_Delete(unsigned_event);
return -1;
}
rc = nsigner_qrexec_sign_event(g_signer.qrexec_target,
g_signer.qrexec_service,
&g_signer.selector,
unsigned_json,
signed_event_json_out);
free(unsigned_json);
cJSON_Delete(unsigned_event);
return rc;
}
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
cJSON *unsigned_event = NULL;
cJSON *tags_copy = NULL;
char *unsigned_json = NULL;
int rc = -1;
unsigned_event = cJSON_CreateObject();
if (!unsigned_event) {
return -1;
}
if (!cJSON_AddNumberToObject(unsigned_event, "kind", kind) ||
!cJSON_AddStringToObject(unsigned_event, "content", content ? content : "")) {
cJSON_Delete(unsigned_event);
return -1;
}
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
if (!tags_copy) {
cJSON_Delete(unsigned_event);
return -1;
}
if (!cJSON_AddItemToObject(unsigned_event, "tags", tags_copy)) {
cJSON_Delete(tags_copy);
cJSON_Delete(unsigned_event);
return -1;
}
if (!cJSON_AddNumberToObject(unsigned_event, "created_at", (double)timestamp) ||
!cJSON_AddStringToObject(unsigned_event, "pubkey", g_state.npub_hex)) {
cJSON_Delete(unsigned_event);
return -1;
}
unsigned_json = cJSON_PrintUnformatted(unsigned_event);
if (!unsigned_json) {
cJSON_Delete(unsigned_event);
return -1;
}
rc = nsigner_session_sign_event(&g_signer.url_session_fd,
g_signer.endpoint_url,
&g_signer.selector,
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
unsigned_json,
signed_event_json_out);
free(unsigned_json);
cJSON_Delete(unsigned_event);
return rc;
}
if (g_signer.kind != NT_SIGNER_LOCAL) {
return -1;
}
if (signer_local_private_key(priv) != 0) {
return -1;
}
tags_copy = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
if (!tags_copy) {
return -1;
}
event = nostr_create_and_sign_event(kind,
content ? content : "",
tags_copy,
priv,
timestamp);
cJSON_Delete(tags_copy);
if (!event) {
return -1;
}
*signed_event_json_out = cJSON_PrintUnformatted(event);
cJSON_Delete(event);
return *signed_event_json_out ? 0 : -1;
}
int signer_sign_event_json(const char *unsigned_event_json, char **signed_event_json_out) {
unsigned char priv[32];
cJSON *unsigned_event;
cJSON *kind_item;
cJSON *content_item;
cJSON *tags_item;
cJSON *created_at_item;
cJSON *tags_copy;
cJSON *signed_event;
int kind;
const char *content;
time_t created_at;
if (!unsigned_event_json || !signed_event_json_out) {
return -1;
}
*signed_event_json_out = NULL;
if (g_signer.kind == NT_SIGNER_NSIGNER) {
return nsigner_sign_event(g_signer.socket_name,
&g_signer.selector,
unsigned_event_json,
signed_event_json_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
return nsigner_qrexec_sign_event(g_signer.qrexec_target,
g_signer.qrexec_service,
&g_signer.selector,
unsigned_event_json,
signed_event_json_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
return nsigner_session_sign_event(&g_signer.url_session_fd,
g_signer.endpoint_url,
&g_signer.selector,
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
unsigned_event_json,
signed_event_json_out);
}
if (g_signer.kind != NT_SIGNER_LOCAL) {
return -1;
}
if (signer_local_private_key(priv) != 0) {
return -1;
}
unsigned_event = cJSON_Parse(unsigned_event_json);
if (!unsigned_event || !cJSON_IsObject(unsigned_event)) {
cJSON_Delete(unsigned_event);
return -1;
}
kind_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "kind");
content_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "content");
tags_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "tags");
created_at_item = cJSON_GetObjectItemCaseSensitive(unsigned_event, "created_at");
if (!cJSON_IsNumber(kind_item) || !cJSON_IsArray(tags_item) || !cJSON_IsNumber(created_at_item)) {
cJSON_Delete(unsigned_event);
return -1;
}
kind = (int)kind_item->valuedouble;
content = (cJSON_IsString(content_item) && content_item->valuestring) ? content_item->valuestring : "";
created_at = (time_t)created_at_item->valuedouble;
tags_copy = cJSON_Duplicate(tags_item, 1);
cJSON_Delete(unsigned_event);
if (!tags_copy) {
return -1;
}
signed_event = nostr_create_and_sign_event(kind, content, tags_copy, priv, created_at);
cJSON_Delete(tags_copy);
if (!signed_event) {
return -1;
}
*signed_event_json_out = cJSON_PrintUnformatted(signed_event);
cJSON_Delete(signed_event);
return *signed_event_json_out ? 0 : -1;
}
int signer_nip04_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out) {
unsigned char priv[32];
unsigned char peer_pub[32];
size_t out_sz;
if (!peer_pub_hex || !plaintext || !cipher_out) {
return -1;
}
*cipher_out = NULL;
if (g_signer.kind == NT_SIGNER_NSIGNER) {
return nsigner_nip04_encrypt(g_signer.socket_name,
&g_signer.selector,
peer_pub_hex,
plaintext,
cipher_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
return nsigner_qrexec_nip04_encrypt(g_signer.qrexec_target,
g_signer.qrexec_service,
&g_signer.selector,
peer_pub_hex,
plaintext,
cipher_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
return nsigner_session_nip04_encrypt(&g_signer.url_session_fd,
g_signer.endpoint_url,
&g_signer.selector,
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
peer_pub_hex,
plaintext,
cipher_out);
}
if (g_signer.kind != NT_SIGNER_LOCAL) {
return -1;
}
if (signer_local_private_key(priv) != 0 ||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
return -1;
}
out_sz = (strlen(plaintext) * 2U) + 1024U;
*cipher_out = (char *)malloc(out_sz);
if (!*cipher_out) {
return -1;
}
if (nostr_nip04_encrypt(priv, peer_pub, plaintext, *cipher_out, out_sz) != 0) {
free(*cipher_out);
*cipher_out = NULL;
return -1;
}
return 0;
}
int signer_nip04_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out) {
unsigned char priv[32];
unsigned char peer_pub[32];
size_t out_sz;
if (!peer_pub_hex || !ciphertext || !plain_out) {
return -1;
}
*plain_out = NULL;
if (g_signer.kind == NT_SIGNER_NSIGNER) {
return nsigner_nip04_decrypt(g_signer.socket_name,
&g_signer.selector,
peer_pub_hex,
ciphertext,
plain_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
return nsigner_qrexec_nip04_decrypt(g_signer.qrexec_target,
g_signer.qrexec_service,
&g_signer.selector,
peer_pub_hex,
ciphertext,
plain_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
return nsigner_session_nip04_decrypt(&g_signer.url_session_fd,
g_signer.endpoint_url,
&g_signer.selector,
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
peer_pub_hex,
ciphertext,
plain_out);
}
if (g_signer.kind != NT_SIGNER_LOCAL) {
return -1;
}
if (signer_local_private_key(priv) != 0 ||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
return -1;
}
out_sz = strlen(ciphertext) + 1024U;
*plain_out = (char *)malloc(out_sz);
if (!*plain_out) {
return -1;
}
if (nostr_nip04_decrypt(priv, peer_pub, ciphertext, *plain_out, out_sz) != 0) {
free(*plain_out);
*plain_out = NULL;
return -1;
}
return 0;
}
int signer_nip44_encrypt(const char *peer_pub_hex, const char *plaintext, char **cipher_out) {
unsigned char priv[32];
unsigned char peer_pub[32];
size_t out_sz;
if (!peer_pub_hex || !plaintext || !cipher_out) {
return -1;
}
*cipher_out = NULL;
if (g_signer.kind == NT_SIGNER_NSIGNER) {
return nsigner_nip44_encrypt(g_signer.socket_name,
&g_signer.selector,
peer_pub_hex,
plaintext,
cipher_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
return nsigner_qrexec_nip44_encrypt(g_signer.qrexec_target,
g_signer.qrexec_service,
&g_signer.selector,
peer_pub_hex,
plaintext,
cipher_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
return nsigner_session_nip44_encrypt(&g_signer.url_session_fd,
g_signer.endpoint_url,
&g_signer.selector,
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
peer_pub_hex,
plaintext,
cipher_out);
}
if (g_signer.kind != NT_SIGNER_LOCAL) {
return -1;
}
if (signer_local_private_key(priv) != 0 ||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
return -1;
}
out_sz = (strlen(plaintext) * 2U) + 2048U;
*cipher_out = (char *)malloc(out_sz);
if (!*cipher_out) {
return -1;
}
if (nostr_nip44_encrypt(priv, peer_pub, plaintext, *cipher_out, out_sz) != 0) {
free(*cipher_out);
*cipher_out = NULL;
return -1;
}
return 0;
}
int signer_nip44_decrypt(const char *peer_pub_hex, const char *ciphertext, char **plain_out) {
unsigned char priv[32];
unsigned char peer_pub[32];
size_t out_sz;
if (!peer_pub_hex || !ciphertext || !plain_out) {
return -1;
}
*plain_out = NULL;
if (g_signer.kind == NT_SIGNER_NSIGNER) {
return nsigner_nip44_decrypt(g_signer.socket_name,
&g_signer.selector,
peer_pub_hex,
ciphertext,
plain_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_QREXEC) {
return nsigner_qrexec_nip44_decrypt(g_signer.qrexec_target,
g_signer.qrexec_service,
&g_signer.selector,
peer_pub_hex,
ciphertext,
plain_out);
}
if (g_signer.kind == NT_SIGNER_NSIGNER_URL) {
return nsigner_session_nip44_decrypt(&g_signer.url_session_fd,
g_signer.endpoint_url,
&g_signer.selector,
g_signer.url_auth.enabled ? &g_signer.url_auth : NULL,
peer_pub_hex,
ciphertext,
plain_out);
}
if (g_signer.kind != NT_SIGNER_LOCAL) {
return -1;
}
if (signer_local_private_key(priv) != 0 ||
nostr_hex_to_bytes(peer_pub_hex, peer_pub, 32) != 0) {
return -1;
}
out_sz = strlen(ciphertext) + 4096U;
*plain_out = (char *)malloc(out_sz);
if (!*plain_out) {
return -1;
}
if (nostr_nip44_decrypt(priv, peer_pub, ciphertext, *plain_out, out_sz) != 0) {
free(*plain_out);
*plain_out = NULL;
return -1;
}
return 0;
}
int signer_nip44_self_encrypt(const char *plaintext, char **cipher_out) {
if (!plaintext || !cipher_out) {
return -1;
}
return signer_nip44_encrypt(g_state.npub_hex, plaintext, cipher_out);
}
int signer_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext, char **plain_out) {
if (!sender_pub_hex || !ciphertext || !plain_out) {
return -1;
}
return signer_nip44_decrypt(sender_pub_hex, ciphertext, plain_out);
}

View File

@@ -3,11 +3,17 @@
#include "db.h"
#include "net.h"
#include "publish.h"
#include "signer.h"
#include "../resources/nostr_core_lib/cjson/cJSON.h"
#include "../resources/nostr_core_lib/nostr_core/nip044.h"
#include "../resources/nostr_core_lib/nostr_core/utils.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
nt_state_t g_state;
@@ -134,9 +140,8 @@ void state_cleanup(void) {
free(g_state.kind17375_json);
g_state.kind17375_json = NULL;
state_free_string_array(g_state.kind30078_events, g_state.kind30078_count);
g_state.kind30078_events = NULL;
g_state.kind30078_count = 0;
free(g_state.user_settings_json);
g_state.user_settings_json = NULL;
state_free_string_array(g_state.read_relays, g_state.read_relay_count);
g_state.read_relays = NULL;
@@ -279,29 +284,18 @@ static int state_pick_latest_json(char **slot, long long *slot_created, const ch
return 0;
}
static int state_push_event_copy(char ***arr, int *count, const char *event_json) {
char **next;
char *copy;
int state_nip44_self_encrypt(const char *plaintext, char **cipher_out) {
return signer_nip44_self_encrypt(plaintext, cipher_out);
}
if (!arr || !count || !event_json) {
return -1;
char *state_nip44_self_decrypt(const char *sender_pub_hex, const char *ciphertext) {
char *out = NULL;
if (signer_nip44_self_decrypt(sender_pub_hex, ciphertext, &out) != 0) {
return NULL;
}
copy = state_strdup_local(event_json);
if (!copy) {
return -1;
}
next = (char **)realloc(*arr, (size_t)(*count + 1) * sizeof(char *));
if (!next) {
free(copy);
return -1;
}
*arr = next;
(*arr)[*count] = copy;
(*count)++;
return 0;
return out;
}
int state_load_user_info(void) {
@@ -314,6 +308,7 @@ int state_load_user_info(void) {
char **events = NULL;
int event_count = 0;
int i;
char *settings_event_json = NULL;
char *kind0_event = NULL;
char *kind3_event = NULL;
@@ -339,9 +334,8 @@ int state_load_user_info(void) {
g_state.kind10096_json = NULL;
free(g_state.kind17375_json);
g_state.kind17375_json = NULL;
state_free_string_array(g_state.kind30078_events, g_state.kind30078_count);
g_state.kind30078_events = NULL;
g_state.kind30078_count = 0;
free(g_state.user_settings_json);
g_state.user_settings_json = NULL;
fprintf(stderr, "Attempting to fetch kind 10002 relays from bootstrap relays\n");
snprintf(filter,
@@ -363,10 +357,10 @@ int state_load_user_info(void) {
state_parse_relay_list();
relays = state_get_read_relays(&relay_count);
fprintf(stderr, "Attempting to fetch kinds [0,3,10096,17375,30078] from user relays\n");
fprintf(stderr, "Attempting to fetch kinds [0,3,10096,17375] from user relays\n");
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[0,3,10096,17375,30078],\"limit\":200}",
"{\"authors\":[\"%s\"],\"kinds\":[0,3,10096,17375],\"limit\":200}",
g_state.npub_hex);
if (nt_query_sync_verbose(filter,
@@ -401,8 +395,6 @@ int state_load_user_info(void) {
(void)state_pick_latest_json(&kind10096_event, &kind10096_created, events[i]);
} else if (kind == 17375) {
(void)state_pick_latest_json(&kind17375_event, &kind17375_created, events[i]);
} else if (kind == 30078) {
(void)state_push_event_copy(&g_state.kind30078_events, &g_state.kind30078_count, events[i]);
}
cJSON_Delete(ev);
@@ -432,14 +424,43 @@ int state_load_user_info(void) {
free(kind10096_event);
free(kind17375_event);
fprintf(stderr, "Attempting to fetch user-settings (kind 30078, d=user-settings)\n");
snprintf(filter,
sizeof(filter),
"{\"authors\":[\"%s\"],\"kinds\":[30078],\"#d\":[\"user-settings\"],\"limit\":1}",
g_state.npub_hex);
if (nt_get_first_verbose(filter,
relays,
relay_count,
6000,
"Phase 3 (user-settings)",
1,
&settings_event_json) == 0 && settings_event_json) {
cJSON *ev = cJSON_Parse(settings_event_json);
if (ev) {
cJSON *content_item = cJSON_GetObjectItemCaseSensitive(ev, "content");
cJSON *pubkey_item = cJSON_GetObjectItemCaseSensitive(ev, "pubkey");
if (cJSON_IsString(content_item) && cJSON_IsString(pubkey_item)) {
char *decrypted = state_nip44_self_decrypt(pubkey_item->valuestring,
content_item->valuestring);
if (decrypted) {
g_state.user_settings_json = decrypted;
}
}
cJSON_Delete(ev);
}
free(settings_event_json);
}
fprintf(stderr,
"Load summary: 10002=%s 0=%s 3=%s 10096=%s 17375=%s 30078=%d\n",
"Load summary: 10002=%s 0=%s 3=%s 10096=%s 17375=%s user_settings=%s\n",
g_state.kind10002_json ? "ok" : "missing",
(g_state.kind0_json && strcmp(g_state.kind0_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind3_json && strstr(g_state.kind3_json, "\"tags\"") != NULL) ? "ok" : "missing",
(g_state.kind10096_json && strcmp(g_state.kind10096_json, "{}") != 0) ? "ok" : "missing",
(g_state.kind17375_json && strcmp(g_state.kind17375_json, "{}") != 0) ? "ok" : "missing",
g_state.kind30078_count);
g_state.user_settings_json ? "ok" : "missing");
snprintf(g_state.user_name, sizeof(g_state.user_name), "%s", "not logged in");
if (g_state.kind0_json) {

306
src/tui.c
View File

@@ -1,6 +1,8 @@
#include "tui.h"
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
@@ -13,6 +15,36 @@ static struct termios g_original_termios;
static bool g_termios_saved = false;
static bool g_raw_enabled = false;
static volatile sig_atomic_t g_resize_pending = 0;
static char g_app_title[256] = "NOSTR TERMINAL";
static int g_last_menu_start_col = 0;
static int g_frame_line_count = 0;
static bool g_frame_active = false;
static int tui_visible_len_no_markup(const char *s) {
int len = 0;
if (!s) {
return 0;
}
for (size_t i = 0; s[i] != '\0';) {
if (s[i] == '^' && s[i + 1] == '_') {
i += 2;
continue;
}
if (s[i] == '^' && s[i + 1] == ':') {
i += 2;
continue;
}
len++;
i++;
}
return len;
}
/* Write a string while converting ^_...^: segments to underline. */
static void tui_write_with_hotkey_markup(const char *s) {
bool in_hotkey = false;
@@ -29,6 +61,11 @@ static void tui_write_with_hotkey_markup(const char *s) {
continue;
}
if (in_hotkey && s[i] == '^' && s[i + 1] == '^') {
i += 2;
continue;
}
if (in_hotkey && s[i] == '^' && s[i + 1] == ':') {
fputs("\x1b[0m", stdout); /* reset */
in_hotkey = false;
@@ -45,6 +82,86 @@ static void tui_write_with_hotkey_markup(const char *s) {
}
}
static int tui_count_newlines(const char *s) {
int count = 0;
if (!s) {
return 0;
}
for (size_t i = 0; s[i] != '\0'; i++) {
if (s[i] == '\n') {
count++;
}
}
return count;
}
static void tui_track_lines(int lines) {
if (g_frame_active && lines > 0) {
g_frame_line_count += lines;
}
}
static void tui_print_repeat_char(char ch, int width) {
if (width <= 0) {
fputc('\n', stdout);
tui_track_lines(1);
return;
}
for (int i = 0; i < width; i++) {
fputc(ch, stdout);
}
fputc('\n', stdout);
tui_track_lines(1);
}
static void tui_print_centered_text(const char *text, int width) {
int len = 0;
if (!text) {
fputc('\n', stdout);
tui_track_lines(1);
return;
}
len = tui_visible_len_no_markup(text);
if (width <= 0) {
fputc('\n', stdout);
tui_track_lines(1);
return;
}
if (len >= width) {
tui_write_with_hotkey_markup(text);
fputc('\n', stdout);
tui_track_lines(1);
return;
}
{
int left = (width - len) / 2;
int right = width - len - left;
for (int i = 0; i < left; i++) {
fputc(' ', stdout);
}
tui_write_with_hotkey_markup(text);
for (int i = 0; i < right; i++) {
fputc(' ', stdout);
}
fputc('\n', stdout);
tui_track_lines(1);
}
}
static void tui_handle_sigwinch(int signum) {
(void)signum;
g_resize_pending = 1;
}
void tui_init(void) {
struct termios raw;
@@ -81,6 +198,24 @@ void tui_cleanup(void) {
}
}
void tui_install_resize_handler(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = tui_handle_sigwinch;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
(void)sigaction(SIGWINCH, &sa, NULL);
}
int tui_consume_resize_flag(void) {
if (g_resize_pending) {
g_resize_pending = 0;
return 1;
}
return 0;
}
void tui_get_terminal_size(int *width, int *height) {
struct winsize ws;
int w = 80;
@@ -103,6 +238,14 @@ void tui_get_terminal_size(int *width, int *height) {
}
}
void tui_set_app_title(const char *title) {
if (!title || title[0] == '\0') {
return;
}
snprintf(g_app_title, sizeof(g_app_title), "%s", title);
}
void tui_clear_screen(void) {
int width = 0;
int height = 0;
@@ -126,6 +269,152 @@ void tui_print(const char *fmt, ...) {
tui_write_with_hotkey_markup(buffer);
fputc('\n', stdout);
fflush(stdout);
tui_track_lines(tui_count_newlines(buffer) + 1);
}
void tui_print_hr(void) {
int width = 0;
int height = 0;
(void)height;
tui_get_terminal_size(&width, &height);
tui_print_repeat_char('=', width);
fflush(stdout);
}
void tui_print_centered(const char *text) {
int width = 0;
int height = 0;
(void)height;
tui_get_terminal_size(&width, &height);
tui_print_centered_text(text ? text : "", width);
fflush(stdout);
}
int tui_centered_menu_start_col(void) {
int width = 0;
int height = 0;
int title_len = 0;
(void)height;
tui_get_terminal_size(&width, &height);
title_len = tui_visible_len_no_markup(g_app_title);
if (width <= title_len) {
g_last_menu_start_col = 0;
return 0;
}
g_last_menu_start_col = (width - title_len) / 2;
if (g_last_menu_start_col < 0) {
g_last_menu_start_col = 0;
}
return g_last_menu_start_col;
}
void tui_render_top_frame(const tui_view_t *view) {
int width = 0;
int height = 0;
(void)height;
tui_get_terminal_size(&width, &height);
tui_print_repeat_char('=', width);
tui_print_centered_text(g_app_title, width);
tui_print_repeat_char('=', width);
if (view && view->breadcrumb && view->breadcrumb[0] != '\0') {
tui_write_with_hotkey_markup(view->breadcrumb);
} else {
tui_write_with_hotkey_markup("> Main Menu");
}
fputc('\n', stdout);
tui_track_lines(1);
fputc('\n', stdout);
fputc('\n', stdout);
tui_track_lines(2);
fflush(stdout);
tui_centered_menu_start_col();
}
void tui_print_menu_item(const char *fmt, ...) {
char buffer[8192];
va_list ap;
va_start(ap, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);
for (int i = 0; i < g_last_menu_start_col; i++) {
fputc(' ', stdout);
}
tui_write_with_hotkey_markup(buffer);
fputc('\n', stdout);
fflush(stdout);
tui_track_lines(tui_count_newlines(buffer) + 1);
}
void tui_begin_frame(const tui_view_t *view) {
g_frame_active = true;
g_frame_line_count = 0;
tui_clear_screen();
tui_render_top_frame(view);
}
int tui_end_frame_with_prompt(const char *prompt, char *buf, int bufsize) {
int width = 0;
int height = 0;
int filler = 0;
tui_get_terminal_size(&width, &height);
filler = (height - 1) - g_frame_line_count;
if (filler < 0) {
filler = 0;
}
for (int i = 0; i < filler; i++) {
fputc('\n', stdout);
}
if (filler > 0) {
fprintf(stdout, "\033[%dA\r", filler);
}
for (int i = 0; i < g_last_menu_start_col; i++) {
fputc(' ', stdout);
}
fflush(stdout);
g_frame_active = false;
g_frame_line_count = 0;
return tui_get_line(prompt ? prompt : ">", buf, bufsize);
}
void tui_show_splash(void) {
static const tui_view_t splash_view = {
"> Splash",
NULL,
};
char hold[8];
tui_begin_frame(&splash_view);
tui_print("Welcome to Nostr Terminal.");
tui_print("");
tui_print("Continuous TUI mode is enabled.");
tui_print("Frame redraw preserves scrollback by using newline refresh.");
tui_print("");
(void)tui_end_frame_with_prompt("Press Enter to continue >", hold, (int)sizeof(hold));
}
int tui_get_line(const char *prompt, char *buf, int bufsize) {
@@ -162,6 +451,9 @@ int tui_get_line(const char *prompt, char *buf, int bufsize) {
unsigned char ch = 0;
ssize_t n = read(STDIN_FILENO, &ch, 1);
if (n <= 0) {
if (n < 0 && errno == EINTR) {
continue;
}
break;
}
@@ -203,11 +495,19 @@ int tui_get_key(void) {
tui_init();
}
if (read(STDIN_FILENO, &ch, 1) != 1) {
while (1) {
ssize_t n = read(STDIN_FILENO, &ch, 1);
if (n == 1) {
return (int)ch;
}
if (n < 0 && errno == EINTR) {
if (g_resize_pending) {
return TUI_KEY_RESIZE;
}
continue;
}
return -1;
}
return (int)ch;
}
void tui_set_window_title(const char *title) {