Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ All notable changes to bitcoin-tui are documented here.
- **Peer actions** - from the peer detail overlay, select Disconnect or Ban (24h) and press `Enter` to execute; result is shown in a floating overlay with a success or error message; press `Esc` to dismiss
- **Added Nodes overlay** - press `[a]` from the Peers tab to open a centered overlay listing all added nodes with connection status (● connected / ○ not connected); press `down/up-arrow` to navigate, `Enter` to remove a node, `[a]` to add a new node, `Esc` to close
- **Ban List overlay** - press `[b]` from the Peers tab to open a centered overlay listing all banned addresses with their expiry time; press `down/up-arrow` to navigate, `Enter` to unban, `Esc` to close
- **Soft-fork Tracking** - Network tab now shows a table of all consensus deployments (`getdeploymentinfo`) with name, type (buried/bip9), status, and activation height; loaded once on first visit; status colored green (active), yellow (locked\_in), cyan (started)

### Changed
- Network tab: Network Status and Node panels are now displayed side by side
- Long peer addresses (onion, i2p) in the peer detail overlay are displayed on their own line to avoid truncation; IPv4 and IPv6 addresses remain on one line

## [0.6.1] - 2026-03-04
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Connects to a local or remote Bitcoin Core node via JSON-RPC and displays live b
- **Dashboard** - blockchain height, difficulty, sync progress, network status, and mempool summary at a glance
- **Mempool** - transaction count, virtual size, total fees, min relay fee, memory usage gauge, and animated recent block fill visualization (newest first, colored green/yellow/orange by weight - blocks slide right when a new block arrives; block age shown per column; number of columns adapts to terminal width)
- **Search** - press `/` to search mempool or confirmed transactions (txid); drill into blocks, inputs, and outputs (`txindex=1` required for confirmed lookups)
- **Network** - connection counts (inbound/outbound), client version, protocol version, relay fee
- **Network** - connection counts (inbound/outbound), client version, protocol version, relay fee; soft-fork tracking table showing all consensus deployments with status and activation height (`getdeploymentinfo`, loaded on first visit)
- **Peers** - live peer table with address, network type, direction, ping, bytes sent/received, and tip height; navigate with `down/up-arrow` and press `Enter` to open a detail overlay for any peer; disconnect or ban (24h) from the detail overlay; press `[a]` to view/manage added nodes, `[b]` to view/manage the ban list
- **Tools** - broadcast raw transactions via `sendrawtransaction`; live private broadcast queue (Bitcoin Core PR #29415, shown when non-empty)
- Background polling thread - non-blocking UI with configurable refresh interval
Expand Down
9 changes: 9 additions & 0 deletions src/json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,15 @@ class json {
auto begin() const { return aval_.begin(); }
auto end() const { return aval_.end(); }

// -----------------------------------------------------------------------
// Iteration (objects) — returns reference to underlying map so callers
// can iterate with range-for and access .first/.second (key/value).
// -----------------------------------------------------------------------
[[nodiscard]] const object_t& items() const {
static const object_t empty_map{};
return kind_ == Kind::Object ? oval_ : empty_map;
}

// -----------------------------------------------------------------------
// Static factories
// -----------------------------------------------------------------------
Expand Down
78 changes: 76 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ static int run(int argc, char* argv[]) {
std::thread remove_addednode_thread;
std::thread unban_action_thread;

// Network tab: softfork deployment info
std::vector<SoftFork> softforks;
std::mutex softforks_mtx;
std::atomic<bool> softforks_loaded{false};
std::atomic<bool> softforks_loading{false};
std::thread softforks_thread;

// Peers tab: panel focus (0=peers, 1=added nodes, 2=ban list)
int peers_panel = 0;
int addednodes_sel = -1;
Expand Down Expand Up @@ -586,6 +593,62 @@ static int run(int argc, char* argv[]) {
});
};

auto fetch_softforks = [&] {
if (softforks_loading.load())
return;
if (softforks_thread.joinable())
softforks_thread.join();
softforks_loading = true;
softforks_thread = std::thread([&] {
std::vector<SoftFork> result;
try {
RpcClient rc(cfg);
auto dep = rc.call("getdeploymentinfo")["result"]["deployments"];
if (dep.is_object()) {
for (const auto& [name, val] : dep.items()) {
SoftFork f;
f.name = name;
f.type = val.value("type", std::string{});
f.active = val.value("active", false);
f.height = val.value("height", int64_t{-1});
if (val.contains("bip9") && val["bip9"].is_object()) {
const auto& b9 = val["bip9"];
f.bip9_status = b9.value("status", std::string{});
f.bip9_since = b9.value("since", int64_t{0});
f.bip9_start_time = b9.value("start_time", int64_t{0});
f.bip9_timeout = b9.value("timeout", int64_t{0});
f.bip9_min_activation = b9.value("min_activation_height", int64_t{0});
if (b9.contains("statistics") && b9["statistics"].is_object()) {
const auto& st = b9["statistics"];
f.bip9_elapsed = st.value("elapsed", int64_t{0});
f.bip9_count = st.value("count", int64_t{0});
f.bip9_period = st.value("period", int64_t{0});
f.bip9_threshold = st.value("threshold", int64_t{0});
}
}
result.push_back(std::move(f));
}
std::sort(result.begin(), result.end(),
[](const SoftFork& a, const SoftFork& b) {
if (a.active != b.active)
return a.active > b.active;
return a.name < b.name;
});
}
} catch (...) { // NOLINT(bugprone-empty-catch)
}
if (!running.load())
return;
{
std::lock_guard lock(softforks_mtx);
softforks = std::move(result);
}
softforks_loading = false;
softforks_loaded = true;
screen.PostEvent(Event::Custom);
});
};

// Layout: tab toggle only — global search is handled via '/' key in the event handler
// (The Toggle component consumes Tab internally, so FTXUI Input focus is unreachable.)
auto layout = Container::Vertical({tab_toggle});
Expand Down Expand Up @@ -876,9 +939,17 @@ static int run(int argc, char* argv[]) {
}
break;
}
case 2:
tab_content = render_network(snap);
case 2: {
if (!softforks_loaded.load() && !softforks_loading.load())
fetch_softforks();
std::vector<SoftFork> forks_snap;
{
std::lock_guard lock(softforks_mtx);
forks_snap = softforks;
}
tab_content = render_network(snap, forks_snap, softforks_loading.load());
break;
}
case 3:
if (peer_disconnect_overlay) {
PeerActionResult action_snap;
Expand Down Expand Up @@ -1294,6 +1365,7 @@ static int run(int argc, char* argv[]) {
return false;
}
}

// Peers tab: disconnecting overlay
if (tab_index == 3 && peer_disconnect_overlay) {
if (event == Event::Escape && !peer_action_in_flight.load()) {
Expand Down Expand Up @@ -1863,6 +1935,8 @@ static int run(int argc, char* argv[]) {
added_nodes_thread.join();
if (banned_list_thread.joinable())
banned_list_thread.join();
if (softforks_thread.joinable())
softforks_thread.join();
poll_thread.join();
anim_thread.join();

Expand Down
72 changes: 55 additions & 17 deletions src/render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <cmath>
#include <ctime>
#include <iomanip>
#include <limits>
#include <sstream>

#include <ftxui/dom/elements.hpp>
Expand Down Expand Up @@ -210,24 +211,61 @@ Element render_mempool(const AppState& s, int mempool_sel) {
}

// --- Network ----------------------------------------------------------------
Element render_network(const AppState& s) {
Element render_network(const AppState& s, const std::vector<SoftFork>& forks, bool forks_loading) {
auto left_col = section_box(
"Network Status", {
label_value(" Active : ", s.network_active ? "yes" : "no",
s.network_active ? Color::Green : Color::Red),
label_value(" Peers : ", std::to_string(s.connections)),
label_value(" Inbound : ", std::to_string(s.connections_in)),
label_value(" Outbound : ", std::to_string(s.connections_out)),
});

auto right_col =
section_box("Node", {
label_value(" Client : ", s.subversion),
label_value(" Protocol : ", std::to_string(s.protocol_version)),
label_value(" Relay fee : ", fmt_satsvb(s.relay_fee)),
});

Elements fork_rows;
if (forks_loading) {
fork_rows.push_back(text(" Loading\u2026") | color(Color::GrayDark));
} else if (forks.empty()) {
fork_rows.push_back(text(" (unavailable \u2014 node may not support getdeploymentinfo)") |
color(Color::GrayDark));
} else {
// Header
fork_rows.push_back(hbox({
text(" "),
text("Name") | bold | size(WIDTH, EQUAL, 18),
text("Type") | bold | size(WIDTH, EQUAL, 8),
text("Status") | bold | size(WIDTH, EQUAL, 12),
text("Height") | bold,
filler(),
}) |
color(Color::Gold1));
fork_rows.push_back(separator());
for (const auto& f : forks) {
const std::string& status = f.bip9_status.empty() ? f.type : f.bip9_status;
Color status_color = f.active ? Color::Green
: status == "locked_in" ? Color::Yellow
: status == "started" ? Color::Cyan
: Color::GrayDark;
fork_rows.push_back(hbox({
text(" "),
text(f.name) | size(WIDTH, EQUAL, 18),
text(f.type) | color(Color::GrayDark) | size(WIDTH, EQUAL, 8),
text(status) | color(status_color) | size(WIDTH, EQUAL, 12),
f.height >= 0 ? text(fmt_height(f.height)) | color(Color::GrayDark) : text("—"),
filler(),
}));
}
}

return vbox({
section_box(
"Network Status",
{
label_value(" Network active : ", s.network_active ? "yes" : "no",
s.network_active ? Color::Green : Color::Red),
label_value(" Total peers : ", std::to_string(s.connections)),
label_value(" Inbound : ", std::to_string(s.connections_in)),
label_value(" Outbound : ", std::to_string(s.connections_out)),
}),
section_box(
"Node",
{
label_value(" Client version : ", s.subversion),
label_value(" Protocol : ", std::to_string(s.protocol_version)),
label_value(" Relay fee : ", fmt_satsvb(s.relay_fee)),
}),
hbox({std::move(left_col) | flex, std::move(right_col) | flex}),
section_box("Softfork Tracking", std::move(fork_rows)),
filler(),
}) |
flex;
Expand Down
3 changes: 2 additions & 1 deletion src/render.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ ftxui::Element label_value(const std::string& lbl, const std::string& val,

ftxui::Element render_dashboard(const AppState& s);
ftxui::Element render_mempool(const AppState& s, int mempool_sel = -1);
ftxui::Element render_network(const AppState& s);
ftxui::Element render_network(const AppState& s, const std::vector<SoftFork>& forks,
bool forks_loading);
ftxui::Element render_peers(const AppState& s, int selected = -1);
ftxui::Element render_peer_detail(const PeerInfo& p,
const PeerActionResult& action = PeerActionResult{}, int sel = 0);
Expand Down
18 changes: 18 additions & 0 deletions src/state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,24 @@ struct AppState {
std::vector<std::string> privbcast_txids;
};

struct SoftFork {
std::string name;
std::string type; // "buried" | "bip9"
bool active = false;
int64_t height = -1; // activation height (-1 = unknown)
// bip9 extras (empty/0 for buried)
std::string bip9_status; // defined | started | locked_in | active | failed
int64_t bip9_since = 0; // block height status started
int64_t bip9_start_time = 0; // unix timestamp
int64_t bip9_timeout = 0; // unix timestamp
int64_t bip9_min_activation = 0;
// signalling stats (only present during "started")
int64_t bip9_elapsed = 0;
int64_t bip9_count = 0;
int64_t bip9_period = 0;
int64_t bip9_threshold = 0;
};

struct TxVin {
std::string txid;
int vout = 0;
Expand Down
Loading