v0.0.3 - Add loglevel=1 to live boot args to suppress TTY console noise

This commit is contained in:
Laan Tungir
2026-05-07 10:20:50 -04:00
parent adebb27c62
commit 0f74671529
59985 changed files with 8508144 additions and 4402 deletions

Submodule includes/c-relay deleted from 2d2ca79dfd

Binary file not shown.

1
includes/didactyl Submodule

Submodule includes/didactyl added at b6ca2bf3bf

1
includes/n_signer Submodule

Submodule includes/n_signer added at 7ffba2b678

Binary file not shown.

1
includes/nostr-id-tui/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

View File

@@ -0,0 +1,48 @@
cmake_minimum_required(VERSION 3.16)
project(nostr-id-tui C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
find_package(Curses REQUIRED)
option(NOSTR_TUI_FORCE_STATIC "Link nostr-id-tui as fully static binary" OFF)
set(NOSTR_CORE_LIB_DIR "${CMAKE_SOURCE_DIR}/../../includes/nostr_core_lib" CACHE PATH "Path to nostr_core_lib directory")
set(NOSTR_CORE_STATIC_LIB "${NOSTR_CORE_LIB_DIR}/libnostr_core_x64.a" CACHE FILEPATH "Path to libnostr_core static archive")
if(NOT EXISTS "${NOSTR_CORE_STATIC_LIB}")
message(FATAL_ERROR "Missing ${NOSTR_CORE_STATIC_LIB}. Build it first with: cd includes/nostr_core_lib && ./build.sh x64")
endif()
add_executable(nostr-id-tui
src/main.c
src/services_conf.c
src/systemd_client.c
)
target_include_directories(nostr-id-tui PRIVATE
include
${NOSTR_CORE_LIB_DIR}
${NOSTR_CORE_LIB_DIR}/nostr_core
)
target_compile_options(nostr-id-tui PRIVATE -Wall -Wextra -Wpedantic)
target_link_libraries(nostr-id-tui PRIVATE
${NOSTR_CORE_STATIC_LIB}
${CURSES_LIBRARIES}
secp256k1
ssl
crypto
curl
z
pthread
m
dl
)
if(NOSTR_TUI_FORCE_STATIC)
target_link_options(nostr-id-tui PRIVATE -static)
endif()

View File

@@ -0,0 +1,26 @@
ARG BASE_IMAGE=n-os-tr/alpine-musl-base:3.19
FROM ${BASE_IMAGE} AS builder
RUN apk add --no-cache \
bash \
ncurses-dev \
ncurses-static
WORKDIR /work
COPY includes/nostr_core_lib /work/includes/nostr_core_lib
COPY includes/nostr-id-tui /work/includes/nostr-id-tui
RUN cd /work/includes/nostr_core_lib && bash ./build.sh x64
RUN cmake -S /work/includes/nostr-id-tui -B /work/includes/nostr-id-tui/build-cmake \
-DCMAKE_BUILD_TYPE=Release \
-DNOSTR_CORE_LIB_DIR=/work/includes/nostr_core_lib \
-DNOSTR_CORE_STATIC_LIB=/work/includes/nostr_core_lib/libnostr_core_x64.a \
-DNOSTR_TUI_FORCE_STATIC=ON && \
cmake --build /work/includes/nostr-id-tui/build-cmake --target nostr-id-tui -j"$(nproc)" && \
mkdir -p /out && \
cp /work/includes/nostr-id-tui/build-cmake/nostr-id-tui /out/nostr-id-tui
FROM scratch AS output
COPY --from=builder /out/nostr-id-tui /nostr-id-tui

View File

@@ -0,0 +1,53 @@
# nostr-id-tui
Boot-time terminal identity UI for n-OS-tr.
Current local implementation provides a functional ncurses flow:
- Main menu
- Enter existing mnemonic -> validate -> derive account 0 `npub`
- Generate new mnemonic -> derive account 0 `npub` -> show random confirmation positions
## Local host dev loop (fast iteration)
Use the wrapper script to auto-bootstrap `nostr_core_lib` (if missing), rebuild, and run in your current terminal:
```bash
./stack/nostr-id-tui/dev.sh
```
What it does:
1. If `includes/nostr_core_lib/libnostr_core_x64.a` is missing, builds it with `./build.sh x64`.
2. Configures `stack/nostr-id-tui/build-host` with CMake.
3. Rebuilds `nostr-id-tui` for host.
4. Runs it with `TERM=${TERM:-xterm-256color}`.
Manual equivalent (if preferred):
```bash
cmake -S stack/nostr-id-tui -B stack/nostr-id-tui/build-host
cmake --build stack/nostr-id-tui/build-host -j
TERM=xterm-256color ./stack/nostr-id-tui/build-host/nostr-id-tui
```
## Static Docker build (musl, reproducible)
This path now builds `nostr_core_lib` inside Alpine and links `nostr-id-tui` fully static against musl.
```bash
./stack/nostr-id-tui/build_static.sh
```
Output:
- `stack/nostr-id-tui/build/nostr-id-tui_static_x86_64`
## Notes
- Uses upstream helpers from `nostr_core_lib`:
- `nostr_secure_memory`
- `nostr_bip39_ui`
- `nostr_generate_mnemonic_and_keys`
- `nostr_derive_keys_from_mnemonic`
- Identity-agent IPC and systemd boot wiring are intentionally out of scope for this step.

View File

@@ -0,0 +1,85 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
HOST_ARCH="$(uname -m)"
REQUESTED_ARCH="${ARCH:-}"
if [ -n "$REQUESTED_ARCH" ]; then
case "$REQUESTED_ARCH" in
arm64|aarch64)
PLATFORM="linux/arm64"
OUTPUT_ARCH="arm64"
;;
amd64|x86_64)
PLATFORM="linux/amd64"
OUTPUT_ARCH="x86_64"
;;
*)
echo "ERROR: Unsupported ARCH='$REQUESTED_ARCH' (supported: arm64, amd64)"
exit 1
;;
esac
else
case "$HOST_ARCH" in
x86_64)
PLATFORM="linux/amd64"
OUTPUT_ARCH="x86_64"
;;
aarch64|arm64)
PLATFORM="linux/arm64"
OUTPUT_ARCH="arm64"
;;
*)
echo "WARNING: Unknown host architecture '$HOST_ARCH', defaulting to linux/amd64"
PLATFORM="linux/amd64"
OUTPUT_ARCH="$HOST_ARCH"
;;
esac
fi
OUTPUT_NAME="nostr-id-tui_static_${OUTPUT_ARCH}"
BASE_IMAGE_TAG="n-os-tr/alpine-musl-base:3.19"
APP_IMAGE_TAG="n-os-tr/nostr-id-tui-musl-builder:latest"
mkdir -p "$BUILD_DIR"
echo "=========================================="
echo "n-OS-tr nostr-id-tui static build"
echo "=========================================="
echo "Repo root: $REPO_ROOT"
echo "Platform: $PLATFORM"
echo "Output: $BUILD_DIR/$OUTPUT_NAME"
echo ""
echo "[1/3] Building shared base image ($BASE_IMAGE_TAG)"
docker build \
--platform "$PLATFORM" \
-f "$REPO_ROOT/stack/build-common/Dockerfile.alpine-musl" \
-t "$BASE_IMAGE_TAG" \
"$REPO_ROOT/stack/build-common"
echo "[2/3] Building nostr-id-tui musl image ($APP_IMAGE_TAG)"
docker build \
--platform "$PLATFORM" \
-f "$SCRIPT_DIR/Dockerfile.alpine-musl" \
-t "$APP_IMAGE_TAG" \
"$REPO_ROOT"
echo "[3/3] Extracting binary"
CONTAINER_ID="$(docker create "$APP_IMAGE_TAG" /nostr-id-tui)"
trap 'docker rm -f "$CONTAINER_ID" >/dev/null 2>&1 || true' EXIT
docker cp "$CONTAINER_ID:/nostr-id-tui" "$BUILD_DIR/$OUTPUT_NAME"
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
file "$BUILD_DIR/$OUTPUT_NAME"
if LDD_OUT="$(ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 || true)"; then
echo "$LDD_OUT"
fi
echo "Built: $BUILD_DIR/$OUTPUT_NAME"
echo "Size: $(stat -c '%s bytes' "$BUILD_DIR/$OUTPUT_NAME")"

26
includes/nostr-id-tui/dev.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
NOSTR_CORE_DIR="$REPO_ROOT/includes/nostr_core_lib"
NOSTR_CORE_LIB="$NOSTR_CORE_DIR/libnostr_core_x64.a"
BUILD_DIR="$SCRIPT_DIR/build-host"
BINARY="$BUILD_DIR/nostr-id-tui"
if [[ ! -f "$NOSTR_CORE_LIB" ]]; then
echo "[dev.sh] missing $NOSTR_CORE_LIB; building nostr_core_lib first"
(
cd "$NOSTR_CORE_DIR"
./build.sh x64
)
fi
echo "[dev.sh] configuring host build"
cmake -S "$SCRIPT_DIR" -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Debug
echo "[dev.sh] building host binary"
cmake --build "$BUILD_DIR" -j"$(nproc)"
echo "[dev.sh] launching TUI"
exec env TERM="${TERM:-xterm-256color}" "$BINARY"

View File

@@ -0,0 +1,32 @@
#ifndef N_OS_TR_TUI_H
#define N_OS_TR_TUI_H
#include <stddef.h>
#define N_OS_TR_TUI_VERSION "0.2.0"
#define NOSTR_TUI_MAX_MNEMONIC 256
#define NOSTR_TUI_MAX_NPUB 128
#define N_OS_TR_TUI_MAX_SERVICES 16
#define N_OS_TR_TUI_OK 0
#define N_OS_TR_TUI_ERR 1
typedef struct {
char key;
char unit[64];
char label[64];
int default_on;
char deps[128];
int desired_on;
} service_entry_t;
int services_conf_load(const char* path, service_entry_t* out, size_t max, size_t* out_count);
int services_conf_load_fallback(service_entry_t* out, size_t max, size_t* out_count);
int systemd_is_simulation_mode(void);
int systemd_unit_is_active(const char* unit, int* is_active);
int systemd_unit_start(const char* unit, char* err_out, size_t err_out_len);
int systemd_unit_stop(const char* unit, char* err_out, size_t err_out_len);
#endif /* N_OS_TR_TUI_H */

View File

@@ -0,0 +1,758 @@
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <ncurses.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/un.h>
#include <unistd.h>
#include "n_os_tr_tui.h"
#include "nostr_core/nostr_core.h"
typedef enum {
SCREEN_WAIT_SIGNER = 0,
SCREEN_SERVICES = 1,
SCREEN_EXIT = 2
} screen_t;
typedef struct {
int identity_loaded;
char signer_pubkey_hex[65];
char identity_display[NOSTR_TUI_MAX_NPUB];
service_entry_t services[N_OS_TR_TUI_MAX_SERVICES];
size_t service_count;
} app_state_t;
#define BANNER_HEIGHT 9
#define BANNER_IN_MS 3000LL
#define BANNER_HOLD_MS 3000LL
#define BANNER_OUT_MS 3000LL
#define BANNER_PAUSE_MS 3000LL
#define BANNER_CYCLE_MS (BANNER_IN_MS + BANNER_HOLD_MS + BANNER_OUT_MS + BANNER_PAUSE_MS)
#define SIGNER_POLL_INTERVAL_MS 500
#define SIGNER_IO_TIMEOUT_MS 1000
static const char* BANNER_ART[BANNER_HEIGHT] = {
"____________________________/\\\\\\\\\\__________/\\\\\\\\\\\\\\\\\\\\\\______________________________________________",
"___________________________/\\\\\\///\\\\\\______/\\\\\\/////////\\\\\\___________________________________________",
"__________________________/\\\\\\/__\\///\\\\\\___\\//\\\\\\______\\///_________________/\\\\\\______________________",
"_/\\\\/\\\\\\\\\\\\_______________/\\\\\\______\\//\\\\\\___\\////\\\\\\_____________________/\\\\\\\\\\\\\\\\\\\\__/\\\\/\\\\\\\\\\\\\\___",
"_\\/\\\\\\////\\\\\\_____________\\/\\\\\\_______\\/\\\\\\______\\////\\\\\\_________________\\////\\\\\\////__\\/\\\\\\/////\\\\\\_",
"__\\/\\\\\\__\\//\\\\\\____________\\//\\\\\\______/\\\\\\__________\\////\\\\\\_________________\\/\\\\\\______\\/\\\\\\___\\///_",
"___\\/\\\\\\___\\/\\\\\\_____________\\///\\\\\\__/\\\\\\_____/\\\\\\______\\//\\\\\\________________\\/\\\\\\_/\\\\__\\/\\\\\\_______",
"____\\/\\\\\\___\\/\\\\\\__/\\\\\\\\\\\\\\____\\///\\\\\\\\\\/_____\\///\\\\\\\\\\\\\\\\\\/____/\\\\\\\\\\\\\\____\\//\\\\\\\\\\\\___\\/\\\\\\______",
"_____\\///____\\///__\\////////_______\\/////_________\\///////////_____\\////////______\\/////____\\///______"
};
static int ui_row(int y) {
return y + BANNER_HEIGHT + 1;
}
static long long monotonic_ms(void) {
struct timeval tv;
if (gettimeofday(&tv, NULL) != 0) {
return 0;
}
return ((long long)tv.tv_sec * 1000LL) + ((long long)tv.tv_usec / 1000LL);
}
static int banner_art_width(void) {
int i;
int max_width = 0;
for (i = 0; i < BANNER_HEIGHT; ++i) {
int w = (int)strlen(BANNER_ART[i]);
if (w > max_width) {
max_width = w;
}
}
return max_width;
}
static void render_banner_frame(void) {
int row;
int col;
int screen_w = COLS;
int art_w = banner_art_width();
int center_x = (screen_w - art_w) / 2;
long long t = monotonic_ms() % BANNER_CYCLE_MS;
int show_art = 1;
int art_x = center_x;
for (row = 0; row < BANNER_HEIGHT; ++row) {
move(row, 0);
for (col = 0; col < screen_w; ++col) {
addch('_');
}
}
if (t < BANNER_IN_MS) {
int delta = center_x + art_w;
art_x = -art_w + (int)((delta * t) / BANNER_IN_MS);
} else if (t < (BANNER_IN_MS + BANNER_HOLD_MS)) {
art_x = center_x;
} else if (t < (BANNER_IN_MS + BANNER_HOLD_MS + BANNER_OUT_MS)) {
long long out_t = t - (BANNER_IN_MS + BANNER_HOLD_MS);
int delta = (screen_w + 1) - center_x;
art_x = center_x + (int)((delta * out_t) / BANNER_OUT_MS);
} else {
show_art = 0;
}
if (!show_art) {
return;
}
for (row = 0; row < BANNER_HEIGHT; ++row) {
const char* line = BANNER_ART[row];
int len = (int)strlen(line);
for (col = 0; col < len; ++col) {
int x = art_x + col;
char ch = line[col];
if (x < 0 || x >= screen_w) {
continue;
}
mvaddch(row, x, ch);
}
}
}
static void draw_centered_header(const char* title) {
int width = COLS;
int title_len = (int)strlen(title);
int min_required = title_len + 4;
int left_pad;
int right_pad;
int i;
int r;
for (r = ui_row(1); r < LINES; ++r) {
move(r, 0);
clrtoeol();
}
move(ui_row(0), 0);
for (i = 0; i < COLS; ++i) {
addch(' ');
}
if (width < min_required) {
mvprintw(ui_row(0), 0, "=== %s ===", title);
return;
}
left_pad = (width - title_len - 2) / 2;
right_pad = width - title_len - 2 - left_pad;
move(ui_row(0), 0);
for (i = 0; i < left_pad; ++i) {
addch('=');
}
addch(' ');
addstr(title);
addch(' ');
for (i = 0; i < right_pad; ++i) {
addch('=');
}
}
static void wait_key(void) {
timeout(60);
mvprintw(LINES - 2, 2, "Press any key to continue...");
refresh();
while (getch() == ERR) {
render_banner_frame();
mvprintw(LINES - 2, 2, "Press any key to continue...");
refresh();
}
timeout(-1);
}
static int is_hex_64(const char* s) {
int i;
if (!s) {
return 0;
}
for (i = 0; i < 64; ++i) {
if (!isxdigit((unsigned char)s[i])) {
return 0;
}
}
return s[64] == '\0';
}
static int write_all(int fd, const unsigned char* buf, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = send(fd, buf + off, len - off, 0);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return 0;
}
if (n == 0) {
return 0;
}
off += (size_t)n;
}
return 1;
}
static int read_all(int fd, unsigned char* buf, size_t len) {
size_t off = 0;
while (off < len) {
ssize_t n = recv(fd, buf + off, len - off, 0);
if (n < 0) {
if (errno == EINTR) {
continue;
}
return 0;
}
if (n == 0) {
return 0;
}
off += (size_t)n;
}
return 1;
}
static int extract_result_hex_pubkey(const char* json, char* out_pubkey) {
const char* p;
const char* q;
char candidate[65];
size_t i;
if (!json || !out_pubkey) {
return 0;
}
p = strstr(json, "\"result\"");
if (!p) {
return 0;
}
p = strchr(p, ':');
if (!p) {
return 0;
}
p = strchr(p, '"');
if (!p) {
return 0;
}
p++;
q = p;
while (*q && *q != '"') {
q++;
}
if ((size_t)(q - p) != 64) {
return 0;
}
for (i = 0; i < 64; ++i) {
candidate[i] = p[i];
}
candidate[64] = '\0';
if (!is_hex_64(candidate)) {
return 0;
}
strcpy(out_pubkey, candidate);
return 1;
}
/* Returns 1 if signer responded with a valid 64-char hex pubkey, else 0. */
static int poll_nsigner(char* out_pubkey) {
static const char req_json[] =
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"get_public_key\",\"params\":{\"nostr_index\":0}}";
int fd = -1;
struct sockaddr_un addr;
struct timeval tv;
uint32_t req_len_be;
uint32_t resp_len_be;
uint32_t resp_len;
unsigned char header[4];
unsigned char* resp_buf = NULL;
size_t req_len;
int ok = 0;
if (!out_pubkey) {
return 0;
}
out_pubkey[0] = '\0';
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
return 0;
}
tv.tv_sec = SIGNER_IO_TIMEOUT_MS / 1000;
tv.tv_usec = (SIGNER_IO_TIMEOUT_MS % 1000) * 1000;
(void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
(void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';
(void)snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, "%s", "n_os_tr");
if (connect(fd, (struct sockaddr*)&addr, sizeof(sa_family_t) + 1 + strlen("n_os_tr")) != 0) {
close(fd);
return 0;
}
req_len = strlen(req_json);
req_len_be = htonl((uint32_t)req_len);
memcpy(header, &req_len_be, sizeof(header));
if (!write_all(fd, header, sizeof(header)) || !write_all(fd, (const unsigned char*)req_json, req_len)) {
close(fd);
return 0;
}
if (!read_all(fd, header, sizeof(header))) {
close(fd);
return 0;
}
memcpy(&resp_len_be, header, sizeof(resp_len_be));
resp_len = ntohl(resp_len_be);
if (resp_len == 0 || resp_len > 1024 * 1024) {
close(fd);
return 0;
}
resp_buf = (unsigned char*)calloc((size_t)resp_len + 1U, 1U);
if (!resp_buf) {
close(fd);
return 0;
}
if (!read_all(fd, resp_buf, resp_len)) {
free(resp_buf);
close(fd);
return 0;
}
resp_buf[resp_len] = '\0';
ok = extract_result_hex_pubkey((const char*)resp_buf, out_pubkey);
free(resp_buf);
close(fd);
return ok;
}
static void set_identity_display(app_state_t* app) {
if (!app) {
return;
}
if (app->signer_pubkey_hex[0] == '\0') {
snprintf(app->identity_display, sizeof(app->identity_display), "(unknown)");
return;
}
snprintf(app->identity_display,
sizeof(app->identity_display),
"%.16s...%.8s",
app->signer_pubkey_hex,
app->signer_pubkey_hex + 56);
}
static void load_services_if_needed(app_state_t* app) {
const char* path;
if (!app || app->service_count > 0) {
return;
}
path = getenv("NOSTR_TUI_SERVICES_CONF");
if (!path || path[0] == '\0') {
path = "/etc/n-os-tr/services.conf";
}
if (services_conf_load(path, app->services, N_OS_TR_TUI_MAX_SERVICES, &app->service_count) != N_OS_TR_TUI_OK) {
(void)services_conf_load_fallback(app->services, N_OS_TR_TUI_MAX_SERVICES, &app->service_count);
}
}
static void refresh_service_states(app_state_t* app, int* active_states) {
size_t i;
int active = 0;
for (i = 0; i < app->service_count; ++i) {
if (systemd_unit_is_active(app->services[i].unit, &active) != N_OS_TR_TUI_OK) {
active = 0;
}
active_states[i] = active;
}
}
static void apply_dependency_rules(app_state_t* app, size_t idx) {
size_t i;
if (!app || idx >= app->service_count) {
return;
}
if (strcmp(app->services[idx].deps, "-") == 0 || app->services[idx].deps[0] == '\0') {
return;
}
if (!app->services[idx].desired_on) {
return;
}
for (i = 0; i < app->service_count; ++i) {
if (strcmp(app->services[i].unit, app->services[idx].deps) == 0) {
app->services[i].desired_on = 1;
}
}
}
static int apply_services(app_state_t* app) {
int y = ui_row(9);
size_t i;
int active_states[N_OS_TR_TUI_MAX_SERVICES];
char err[256] = {0};
int had_error = 0;
refresh_service_states(app, active_states);
mvprintw(ui_row(7), 2, "Applying selection...");
for (i = 0; i < app->service_count; ++i) {
int rc;
const char* action = NULL;
if (app->services[i].desired_on && !active_states[i]) {
action = "START";
rc = systemd_unit_start(app->services[i].unit, err, sizeof(err));
} else if (!app->services[i].desired_on && active_states[i]) {
action = "STOP ";
rc = systemd_unit_stop(app->services[i].unit, err, sizeof(err));
} else {
action = "SKIP ";
rc = N_OS_TR_TUI_OK;
}
if (rc == N_OS_TR_TUI_OK) {
mvprintw(y++, 4, "[ OK ] %s %s", action, app->services[i].unit);
} else {
mvprintw(y++, 4, "[FAIL] %s %s", action, app->services[i].unit);
mvprintw(y++, 6, "%s", err[0] ? err : "systemctl error");
had_error = 1;
}
if (y >= LINES - 4) {
break;
}
}
if (had_error) {
mvprintw(LINES - 4, 2, "One or more unit operations failed.");
return N_OS_TR_TUI_ERR;
}
mvprintw(LINES - 4, 2, "Service selection applied.");
return N_OS_TR_TUI_OK;
}
static void render_underlined_hotkey_service_row(int y, const service_entry_t* svc, int active_state) {
int x = 3;
const char* label;
const char* key_pos;
size_t prefix_len;
if (!svc) {
return;
}
label = svc->label;
key_pos = strchr(label, (int)tolower((unsigned char)svc->key));
if (!key_pos) {
key_pos = strchr(label, (int)toupper((unsigned char)svc->key));
}
if (!key_pos) {
mvprintw(y,
x,
"%-16s [ %s ] [ %s ] %s",
label,
svc->desired_on ? "on " : "off",
active_state ? "on " : "off",
svc->unit);
return;
}
prefix_len = (size_t)(key_pos - label);
move(y, x);
addnstr(label, (int)prefix_len);
x += (int)prefix_len;
move(y, x);
attron(A_UNDERLINE);
addch(*key_pos);
attroff(A_UNDERLINE);
x += 1;
move(y, x);
addstr(key_pos + 1);
mvprintw(y,
22,
"[ %s ] [ %s ] %s",
svc->desired_on ? "on " : "off",
active_state ? "on " : "off",
svc->unit);
}
static void render_services_table(app_state_t* app, const int* active_states) {
size_t i;
int y = ui_row(6);
mvprintw(ui_row(4), 2, "Service Desired Active Unit");
mvprintw(ui_row(5), 2, "----------------- ------- ------ ------------------------------");
for (i = 0; i < app->service_count && y < LINES - 8; ++i) {
render_underlined_hotkey_service_row(y++, &app->services[i], active_states[i]);
}
}
static screen_t screen_wait_signer(app_state_t* app) {
long long next_poll = 0;
int last_cols = COLS;
int last_lines = LINES;
int spinner = 0;
if (!app) {
return SCREEN_EXIT;
}
nodelay(stdscr, TRUE);
keypad(stdscr, TRUE);
while (1) {
long long now = monotonic_ms();
if (now >= next_poll) {
char pubkey_hex[65] = {0};
if (poll_nsigner(pubkey_hex)) {
snprintf(app->signer_pubkey_hex, sizeof(app->signer_pubkey_hex), "%s", pubkey_hex);
set_identity_display(app);
app->identity_loaded = 1;
nodelay(stdscr, FALSE);
return SCREEN_SERVICES;
}
next_poll = now + SIGNER_POLL_INTERVAL_MS;
spinner = (spinner + 1) % 4;
}
if (COLS != last_cols || LINES != last_lines) {
last_cols = COLS;
last_lines = LINES;
}
draw_centered_header("n_OS_tr");
mvprintw(ui_row(3), 2, "Waiting for signer... %c", "|/-\\"[spinner]);
mvprintw(ui_row(4), 2, "Polling @n_os_tr for get_public_key(nostr_index=0) every 500ms");
mvprintw(LINES - 2, 2, "Press Q to quit");
render_banner_frame();
refresh();
{
long long wait_ms = next_poll - monotonic_ms();
struct timeval tv;
fd_set rfds;
int sel;
if (wait_ms < 0) {
wait_ms = 0;
}
if (wait_ms > SIGNER_POLL_INTERVAL_MS) {
wait_ms = SIGNER_POLL_INTERVAL_MS;
}
tv.tv_sec = (time_t)(wait_ms / 1000);
tv.tv_usec = (suseconds_t)((wait_ms % 1000) * 1000);
FD_ZERO(&rfds);
FD_SET(STDIN_FILENO, &rfds);
sel = select(STDIN_FILENO + 1, &rfds, NULL, NULL, &tv);
if (sel > 0 && FD_ISSET(STDIN_FILENO, &rfds)) {
int ch = getch();
if (ch == KEY_RESIZE) {
continue;
}
ch = tolower(ch);
if (ch == 'q' || ch == 'x') {
nodelay(stdscr, FALSE);
return SCREEN_EXIT;
}
}
}
}
}
static screen_t screen_services(app_state_t* app) {
int ch;
int active_states[N_OS_TR_TUI_MAX_SERVICES] = {0};
size_t i;
int needs_redraw = 1;
int last_cols = COLS;
int last_lines = LINES;
if (!app || !app->identity_loaded) {
return SCREEN_WAIT_SIGNER;
}
load_services_if_needed(app);
for (i = 0; i < app->service_count; ++i) {
app->services[i].desired_on = app->services[i].default_on;
}
timeout(60);
while (1) {
if (needs_redraw) {
draw_centered_header("Services");
refresh_service_states(app, active_states);
mvprintw(ui_row(2), 2, "Identity: %s", app->identity_display[0] ? app->identity_display : "(unknown)");
mvprintw(ui_row(3), 2, "Simulation mode: %s", systemd_is_simulation_mode() ? "ON" : "OFF");
render_services_table(app, active_states);
mvprintw(LINES - 6, 2, "Actions:");
mvprintw(LINES - 5, 2, " R/G/F/N/L/W/S/T toggle | A all on | Z all off");
mvprintw(LINES - 4, 2, " C commit/apply | X skip (leave unchanged) | Q quit");
mvprintw(LINES - 2, 2, "Select action: > ");
render_banner_frame();
refresh();
needs_redraw = 0;
}
ch = getch();
if (ch == ERR) {
if (COLS != last_cols || LINES != last_lines) {
last_cols = COLS;
last_lines = LINES;
needs_redraw = 1;
}
render_banner_frame();
refresh();
continue;
}
if (ch == KEY_RESIZE) {
last_cols = COLS;
last_lines = LINES;
needs_redraw = 1;
continue;
}
ch = tolower(ch);
if (ch == 'q' || ch == 'x') {
timeout(-1);
return SCREEN_EXIT;
}
if (ch == 'a') {
for (i = 0; i < app->service_count; ++i) {
app->services[i].desired_on = 1;
}
needs_redraw = 1;
continue;
}
if (ch == 'z') {
for (i = 0; i < app->service_count; ++i) {
app->services[i].desired_on = 0;
}
needs_redraw = 1;
continue;
}
if (ch == 'c') {
draw_centered_header("Applying Services");
render_banner_frame();
mvprintw(ui_row(2), 2, "Committing selected service states...");
if (apply_services(app) != N_OS_TR_TUI_OK) {
wait_key();
needs_redraw = 1;
continue;
}
wait_key();
timeout(-1);
return SCREEN_EXIT;
}
for (i = 0; i < app->service_count; ++i) {
if (ch == tolower((unsigned char)app->services[i].key)) {
app->services[i].desired_on = !app->services[i].desired_on;
apply_dependency_rules(app, i);
needs_redraw = 1;
break;
}
}
}
}
int main(int argc, char** argv) {
screen_t screen = SCREEN_WAIT_SIGNER;
app_state_t app;
int mode_services_only = 1;
int i;
memset(&app, 0, sizeof(app));
for (i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--mode=services-only") == 0) {
mode_services_only = 1;
} else if (strncmp(argv[i], "--mode=", 7) == 0) {
mode_services_only = 1;
}
}
(void)mode_services_only;
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "nostr_init failed\n");
return 1;
}
initscr();
cbreak();
noecho();
keypad(stdscr, TRUE);
curs_set(0);
while (screen != SCREEN_EXIT) {
switch (screen) {
case SCREEN_WAIT_SIGNER:
screen = screen_wait_signer(&app);
break;
case SCREEN_SERVICES:
screen = screen_services(&app);
break;
case SCREEN_EXIT:
default:
break;
}
}
endwin();
nostr_cleanup();
return 0;
}

View File

@@ -0,0 +1,131 @@
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "n_os_tr_tui.h"
static void copy_str(char* dst, size_t dst_len, const char* src) {
if (!dst || dst_len == 0) return;
if (!src) {
dst[0] = '\0';
return;
}
snprintf(dst, dst_len, "%s", src);
}
static void trim(char* s) {
size_t len;
if (!s) return;
while (*s && isspace((unsigned char)*s)) {
memmove(s, s + 1, strlen(s));
}
len = strlen(s);
while (len > 0 && isspace((unsigned char)s[len - 1])) {
s[len - 1] = '\0';
len--;
}
}
static int parse_default(const char* v) {
if (!v) return 0;
if (strcmp(v, "on") == 0 || strcmp(v, "1") == 0 || strcmp(v, "true") == 0) return 1;
return 0;
}
static void set_entry(service_entry_t* e,
char key,
const char* unit,
const char* label,
int default_on,
const char* deps) {
if (!e) return;
memset(e, 0, sizeof(*e));
e->key = key;
copy_str(e->unit, sizeof(e->unit), unit);
copy_str(e->label, sizeof(e->label), label);
e->default_on = default_on;
copy_str(e->deps, sizeof(e->deps), deps ? deps : "-");
e->desired_on = default_on;
}
int services_conf_load_fallback(service_entry_t* out, size_t max, size_t* out_count) {
size_t i = 0;
if (!out || !out_count || max < 8) return N_OS_TR_TUI_ERR;
set_entry(&out[i++], 'r', "c-relay.service", "c-relay", 0, "nginx.service");
set_entry(&out[i++], 'g', "ginxsom.service", "ginxsom", 0, "nginx.service");
set_entry(&out[i++], 'f', "fips.service", "fips", 0, "-");
set_entry(&out[i++], 'n', "nginx.service", "nginx", 0, "-");
set_entry(&out[i++], 'l', "nostr-config-loader.service", "config-loader", 0, "-");
set_entry(&out[i++], 'w', "nostr-config-writer.service", "config-writer", 0, "nostr-config-loader.service");
set_entry(&out[i++], 's', "ssh.service", "ssh", 0, "-");
set_entry(&out[i++], 't', "tor.service", "tor", 0, "-");
*out_count = i;
return N_OS_TR_TUI_OK;
}
int services_conf_load(const char* path, service_entry_t* out, size_t max, size_t* out_count) {
FILE* fp;
char line[512];
size_t n = 0;
if (!path || !out || !out_count || max == 0) return N_OS_TR_TUI_ERR;
fp = fopen(path, "r");
if (!fp) return N_OS_TR_TUI_ERR;
while (fgets(line, sizeof(line), fp) != NULL) {
char* tok;
char local[512];
char* key_s;
char* unit_s;
char* label_s;
char* def_s;
char* deps_s;
char key;
copy_str(local, sizeof(local), line);
trim(local);
if (local[0] == '\0' || local[0] == '#') continue;
tok = strtok(local, "\t ");
if (!tok) continue;
key_s = tok;
tok = strtok(NULL, "\t ");
if (!tok) continue;
unit_s = tok;
tok = strtok(NULL, "\t ");
if (!tok) continue;
label_s = tok;
tok = strtok(NULL, "\t ");
if (!tok) continue;
def_s = tok;
tok = strtok(NULL, "\t ");
deps_s = tok ? tok : "-";
key = (char)tolower((unsigned char)key_s[0]);
if (!isprint((unsigned char)key)) continue;
if (n >= max) {
fclose(fp);
return N_OS_TR_TUI_ERR;
}
set_entry(&out[n], key, unit_s, label_s, parse_default(def_s), deps_s);
n++;
}
fclose(fp);
if (n == 0) return N_OS_TR_TUI_ERR;
*out_count = n;
return N_OS_TR_TUI_OK;
}

View File

@@ -0,0 +1,100 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "n_os_tr_tui.h"
static int run_systemctl(char* const argv[]) {
pid_t pid;
int status = 0;
if (systemd_is_simulation_mode()) {
return 0;
}
if (access("/bin/systemctl", X_OK) != 0) {
return -1;
}
pid = fork();
if (pid < 0) {
return -1;
}
if (pid == 0) {
execv("/bin/systemctl", argv);
_exit(127);
}
if (waitpid(pid, &status, 0) < 0) {
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
return -1;
}
int systemd_is_simulation_mode(void) {
const char* v = getenv("NOSTR_TUI_SIMULATE_SYSTEMCTL");
return (v && strcmp(v, "1") == 0) ? 1 : 0;
}
int systemd_unit_is_active(const char* unit, int* is_active) {
char* const argv[] = {
"/bin/systemctl", "is-active", "--quiet", (char*)unit, NULL
};
int rc;
if (!unit || !is_active) return N_OS_TR_TUI_ERR;
if (systemd_is_simulation_mode()) {
*is_active = 0;
return N_OS_TR_TUI_OK;
}
rc = run_systemctl(argv);
if (rc < 0) return N_OS_TR_TUI_ERR;
*is_active = (rc == 0) ? 1 : 0;
return N_OS_TR_TUI_OK;
}
int systemd_unit_start(const char* unit, char* err_out, size_t err_out_len) {
char* const argv[] = {
"/bin/systemctl", "start", (char*)unit, NULL
};
int rc;
if (!unit) return N_OS_TR_TUI_ERR;
rc = run_systemctl(argv);
if (rc == 0) return N_OS_TR_TUI_OK;
if (err_out && err_out_len > 0) {
snprintf(err_out, err_out_len, "systemctl start failed for %s (rc=%d, errno=%d)", unit, rc, errno);
}
return N_OS_TR_TUI_ERR;
}
int systemd_unit_stop(const char* unit, char* err_out, size_t err_out_len) {
char* const argv[] = {
"/bin/systemctl", "stop", (char*)unit, NULL
};
int rc;
if (!unit) return N_OS_TR_TUI_ERR;
rc = run_systemctl(argv);
if (rc == 0) return N_OS_TR_TUI_OK;
if (err_out && err_out_len > 0) {
snprintf(err_out, err_out_len, "systemctl stop failed for %s (rc=%d, errno=%d)", unit, rc, errno);
}
return N_OS_TR_TUI_ERR;
}

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
BIN="$ROOT_DIR/stack/nostr-id-tui/build-host/nostr-id-tui"
SERVICES_CONF="$ROOT_DIR/iso/config/includes.chroot/etc/n-os-tr/services.conf"
if [[ ! -x "$BIN" ]]; then
echo "error: missing binary $BIN"
echo "build first: cmake -S stack/nostr-id-tui -B stack/nostr-id-tui/build-host && cmake --build stack/nostr-id-tui/build-host -j"
exit 1
fi
if [[ ! -f "$SERVICES_CONF" ]]; then
echo "error: missing services config $SERVICES_CONF"
exit 1
fi
OUT="$ROOT_DIR/stack/nostr-id-tui/build-host/test-services-sim.out"
# Simulated run (no real systemctl operations). We just verify the TUI launches and renders.
timeout 2s env TERM=xterm-256color \
NOSTR_TUI_SIMULATE_SYSTEMCTL=1 \
NOSTR_TUI_SERVICES_CONF="$SERVICES_CONF" \
script -q -c "$BIN" /dev/null > "$OUT" 2>&1 || true
if grep -q "n-OS-tr Identity" "$OUT"; then
echo "ok: TUI launched and rendered header"
else
echo "error: expected TUI header not found in output"
echo "see $OUT"
exit 1
fi
echo "ok: simulated services smoke test passed"

1
includes/otp Submodule

Submodule includes/otp added at dda69832dc

19
includes/third-party/nak/PROVENANCE.md vendored Normal file
View File

@@ -0,0 +1,19 @@
# nak — Nostr Army Knife (Tier 3: third-party binary)
- **Upstream:** https://github.com/nostrapps/nostr-army-knife
- **Version:** v0.19.7
- **Platform:** linux-amd64
- **Download URL:** https://github.com/nostrapps/nostr-army-knife/releases/download/v0.19.7/nak-v0.19.7-linux-amd64
- **SHA256:** (to be filled after download verification)
## Verification
```sh
sha256sum nak-v0.19.7-linux-amd64
# Compare against value in iso/SHA256SUMS.tier3
```
## Why Tier 3
We do not control this project's source. The binary is included as a convenience
CLI tool available on tty3+ shells. It is NOT used by any system service.