commits
tags
#include "ui.h"
#include "../util.h"
#include <curses.h>
#include <string.h>
#define FOOTER_H 2
static const char *TAB_LABEL[DETAIL_TAB_COUNT] = {"General", "Files", "Peers", "Trackers"};
static void ensure_loaded(AppState *st)
{
if (st->detail_loaded)
return;
char err[256];
if (torrent_get_detail(st->rpc, st->detail_id, &st->detail, err, sizeof(err)) == 0) {
st->detail_loaded = 1;
} else {
ui_set_status(st, "Error: %s", err);
st->view = VIEW_LIST;
}
}
static void draw_title(AppState *st, int cols)
{
attron(COLOR_PAIR(CP_HEADER) | A_BOLD);
mvhline(0, 0, ' ', cols);
mvprintw(0, 0, " %-*.*s", cols - 1, cols - 1,
st->detail_loaded ? st->detail.info.name : "");
attroff(COLOR_PAIR(CP_HEADER) | A_BOLD);
}
static void draw_general(WINDOW *win, AppState *st, int top)
{
TorrentDetail *d = &st->detail;
char sizebuf[24], donebuf[24], downbuf[24], upbuf[24], addedbuf[32], createdbuf[32], donedatebuf[32];
fmt_size(d->info.total_size, sizebuf, sizeof(sizebuf));
fmt_size(d->info.total_size - d->info.left_until_done, donebuf, sizeof(donebuf));
fmt_speed(d->info.rate_download, downbuf, sizeof(downbuf));
fmt_speed(d->info.rate_upload, upbuf, sizeof(upbuf));
fmt_time(d->date_added, addedbuf, sizeof(addedbuf));
fmt_time(d->date_created, createdbuf, sizeof(createdbuf));
fmt_time(d->done_date, donedatebuf, sizeof(donedatebuf));
int y = top;
mvwprintw(win, y++, 2, "Hash: %s", d->info.hash);
mvwprintw(win, y++, 2, "Size: %s (%s done)", sizebuf, donebuf);
mvwprintw(win, y++, 2, "Progress: %.1f%%", d->info.percent_done * 100.0);
mvwprintw(win, y++, 2, "Speed: \xe2\x86\x93 %s \xe2\x86\x91 %s", downbuf, upbuf);
mvwprintw(win, y++, 2, "Ratio: %.2f", d->info.upload_ratio);
mvwprintw(win, y++, 2, "Peers: %d connected (%d sending, %d receiving)",
d->info.peers_connected, d->info.peers_sending_to_us, d->info.peers_getting_from_us);
mvwprintw(win, y++, 2, "Down limit: %s", d->download_limited ? "active" : "unlimited");
mvwprintw(win, y++, 2, "Up limit: %s", d->upload_limited ? "active" : "unlimited");
mvwprintw(win, y++, 2, "Folder: %s", d->info.download_dir);
mvwprintw(win, y++, 2, "Added: %s", addedbuf);
mvwprintw(win, y++, 2, "Created: %s", createdbuf);
mvwprintw(win, y++, 2, "Completed: %s", donedatebuf);
if (d->info.error)
mvwprintw(win, y++, 2, "Error: %s", d->info.error_string);
}
static void draw_files(WINDOW *win, AppState *st, int top, int rows, int cols)
{
TorrentDetail *d = &st->detail;
if (d->file_count == 0) {
mvwprintw(win, top, 2, "(no file information)");
return;
}
if (st->detail_cursor >= (int)d->file_count)
st->detail_cursor = (int)d->file_count - 1;
if (st->detail_cursor < 0)
st->detail_cursor = 0;
int vis = rows;
int first = 0;
if (st->detail_cursor >= vis)
first = st->detail_cursor - vis + 1;
int name_w = cols - 36 > 10 ? cols - 36 : 10;
for (int row = 0; row < vis; row++) {
int i = first + row;
if (i >= (int)d->file_count)
break;
TorrentFile *f = &d->files[i];
char sizebuf[24];
fmt_size(f->length, sizebuf, sizeof(sizebuf));
double pct = f->length > 0 ? (100.0 * (double)f->bytes_completed / (double)f->length) : 100.0;
const char *prio = f->priority > 0 ? "High" : (f->priority < 0 ? "Low" : "Normal");
int attr = (i == st->detail_cursor) ? (COLOR_PAIR(CP_SELROW) | A_BOLD) : 0;
mvwhline(win, top + row, 2, ' ', getmaxx(win) - 4);
wattron(win, attr);
mvwprintw(win, top + row, 2, "%c %-*.*s %9s %5.1f%% %-7s %s",
f->wanted ? ' ' : '-', name_w, name_w, f->name, sizebuf, pct, prio,
f->wanted ? "" : "(skipped)");
wattroff(win, attr);
}
}
static void draw_peers(WINDOW *win, AppState *st, int top, int rows)
{
TorrentDetail *d = &st->detail;
if (d->peer_count == 0) {
mvwprintw(win, top, 2, "(no connected peers)");
return;
}
for (int row = 0; row < rows; row++) {
size_t i = (size_t)row;
if (i >= d->peer_count)
break;
TorrentPeer *p = &d->peers[i];
char downbuf[24], upbuf[24];
fmt_speed(p->rate_to_client, downbuf, sizeof(downbuf));
fmt_speed(p->rate_to_peer, upbuf, sizeof(upbuf));
mvwprintw(win, top + row, 2, "%-22s %-20s %5.1f%% %10s %10s %-8s",
p->address, p->client_name, p->progress * 100.0, downbuf, upbuf, p->flags);
}
}
static void draw_trackers(WINDOW *win, AppState *st, int top, int rows, int cols)
{
TorrentDetail *d = &st->detail;
if (d->tracker_count == 0) {
mvwprintw(win, top, 2, "(no trackers)");
return;
}
int url_w = cols - 22 > 10 ? cols - 22 : 10;
for (int row = 0; row < rows; row++) {
size_t i = (size_t)row;
if (i >= d->tracker_count)
break;
TorrentTracker *t = &d->trackers[i];
mvwprintw(win, top + row, 2, "[%d] %-*.*s S:%d L:%d", t->tier, url_w, url_w, t->announce,
t->seeder_count, t->leecher_count);
if (t->last_announce_result[0])
mvwprintw(win, top + row + 1, 6, "%.*s", cols - 7, t->last_announce_result);
}
}
static const KeyHint DETAIL_HINTS[] = {
{"Esc/q", "Back"}, {"Tab", "Next tab"}, {"1-4", "Select tab"},
{"Space", "Select/skip (Files)"}, {"+/-", "Priority (Files)"},
};
void ui_detail_render(AppState *st)
{
ensure_loaded(st);
if (st->view != VIEW_DETAIL)
return;
int rows, cols;
getmaxyx(stdscr, rows, cols);
erase();
draw_title(st, cols);
int body_top = 1;
int body_h = rows - FOOTER_H - 1 - body_top;
if (body_h < 3)
body_h = 3;
WINDOW *win = derwin(stdscr, body_h, cols, body_top, 0);
ui_box(win, body_h, cols);
int x = 2;
for (int i = 0; i < DETAIL_TAB_COUNT; i++) {
int active = (i == (int)st->detail_tab);
wattron(win, active ? (COLOR_PAIR(CP_SELROW) | A_BOLD) : (COLOR_PAIR(CP_ACCENT)));
mvwprintw(win, 0, x, " %d:%s ", i + 1, TAB_LABEL[i]);
wattroff(win, active ? (COLOR_PAIR(CP_SELROW) | A_BOLD) : (COLOR_PAIR(CP_ACCENT)));
x += (int)strlen(TAB_LABEL[i]) + 5;
}
int content_top = 2;
int content_rows = body_h - content_top - 1;
if (content_rows < 1)
content_rows = 1;
switch (st->detail_tab) {
case DETAIL_GENERAL:
draw_general(win, st, content_top);
break;
case DETAIL_FILES:
draw_files(win, st, content_top, content_rows, cols);
break;
case DETAIL_PEERS:
draw_peers(win, st, content_top, content_rows);
break;
case DETAIL_TRACKERS:
draw_trackers(win, st, content_top, content_rows, cols);
break;
default:
break;
}
wnoutrefresh(win);
delwin(win);
ui_draw_footer(DETAIL_HINTS, sizeof(DETAIL_HINTS) / sizeof(DETAIL_HINTS[0]), FOOTER_H);
doupdate();
}
void ui_detail_handle_key(AppState *st, int ch)
{
switch (ch) {
case 'q':
case 27:
case KEY_BACKSPACE:
case 127:
ui_close_detail(st);
return;
case '\t':
st->detail_tab = (st->detail_tab + 1) % DETAIL_TAB_COUNT;
st->detail_cursor = 0;
return;
case KEY_BTAB:
st->detail_tab = (st->detail_tab + DETAIL_TAB_COUNT - 1) % DETAIL_TAB_COUNT;
st->detail_cursor = 0;
return;
case '1':
st->detail_tab = DETAIL_GENERAL;
st->detail_cursor = 0;
return;
case '2':
st->detail_tab = DETAIL_FILES;
st->detail_cursor = 0;
return;
case '3':
st->detail_tab = DETAIL_PEERS;
st->detail_cursor = 0;
return;
case '4':
st->detail_tab = DETAIL_TRACKERS;
st->detail_cursor = 0;
return;
default:
break;
}
if (st->detail_tab == DETAIL_FILES && st->detail_loaded) {
TorrentDetail *d = &st->detail;
switch (ch) {
case KEY_UP:
case 'k':
if (st->detail_cursor > 0)
st->detail_cursor--;
break;
case KEY_DOWN:
case 'j':
if ((size_t)(st->detail_cursor + 1) < d->file_count)
st->detail_cursor++;
break;
case ' ': {
if ((size_t)st->detail_cursor >= d->file_count)
break;
TorrentFile *f = &d->files[st->detail_cursor];
int newwanted = !f->wanted;
char err[256];
if (torrent_set_file_wanted(st->rpc, st->detail_id, &st->detail_cursor, 1, newwanted,
err, sizeof(err)) == 0) {
f->wanted = newwanted;
} else {
ui_set_status(st, "Error: %s", err);
}
break;
}
case '+':
case '-': {
if ((size_t)st->detail_cursor >= d->file_count)
break;
TorrentFile *f = &d->files[st->detail_cursor];
int prio = f->priority + (ch == '+' ? 1 : -1);
if (prio > 1)
prio = 1;
if (prio < -1)
prio = -1;
char err[256];
if (torrent_set_file_priority(st->rpc, st->detail_id, &st->detail_cursor, 1, prio,
err, sizeof(err)) == 0) {
f->priority = prio;
} else {
ui_set_status(st, "Error: %s", err);
}
break;
}
default:
break;
}
}
}