commits
tags
#include "ui.h"
#include <ctype.h>
#include <curses.h>
#include <dirent.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
static WINDOW *open_box(int h, int w, const char *title)
{
int rows, cols;
getmaxyx(stdscr, rows, cols);
/* Callers size w against cols already, but not h against rows - clamp
* here too so a fixed-height dialog can't get pushed off a short
* terminal the same way the settings popup could (see ui_settings.c). */
if (h > rows)
h = rows;
if (w > cols)
w = cols;
int y = (rows - h) / 2;
int x = (cols - w) / 2;
if (y < 0)
y = 0;
if (x < 0)
x = 0;
WINDOW *win = newwin(h, w, y, x);
werase(win);
ui_box(win, h, w);
if (title)
ui_box_title(win, w, title);
return win;
}
static void close_box(WINDOW *win)
{
delwin(win);
touchwin(stdscr);
}
int ui_confirm(const char *title, const char *msg)
{
int w = (int)strlen(msg) + 8;
if (w < 30)
w = 30;
int rows, cols;
getmaxyx(stdscr, rows, cols);
(void)rows;
if (w > cols - 4)
w = cols - 4;
WINDOW *win = open_box(5, w, title);
keypad(win, TRUE);
mvwprintw(win, 2, 2, "%.*s", w - 4, msg);
mvwprintw(win, 3, 2, "[y]es/Enter [n]o/Esc");
wrefresh(win);
int result = 0;
for (;;) {
int ch = wgetch(win);
if (ch == 'y' || ch == 'Y' || ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
result = 1;
break;
}
if (ch == 'n' || ch == 'N' || ch == 27) {
result = 0;
break;
}
}
close_box(win);
return result;
}
void ui_message(const char *title, const char *msg)
{
int w = (int)strlen(msg) + 8;
if (w < 30)
w = 30;
int cols = getmaxx(stdscr);
if (w > cols - 4)
w = cols - 4;
WINDOW *win = open_box(5, w, title);
mvwprintw(win, 2, 2, "%.*s", w - 4, msg);
mvwprintw(win, 3, 2, "Press any key...");
wrefresh(win);
wgetch(win);
close_box(win);
}
int ui_prompt(const char *title, const char *initial, char *out, size_t outsize)
{
int cols = getmaxx(stdscr);
int w = cols - 8;
if (w > 70)
w = 70;
if (w < 30)
w = 30;
WINDOW *win = open_box(4, w, title);
keypad(win, TRUE);
char buf[512];
snprintf(buf, sizeof(buf), "%s", initial ? initial : "");
size_t len = strlen(buf);
size_t cap = sizeof(buf) - 1;
int field_w = w - 4;
curs_set(1);
int result = 0;
for (;;) {
mvwhline(win, 2, 2, ' ', field_w);
size_t show_from = 0;
if ((int)len >= field_w)
show_from = len - field_w + 1;
mvwprintw(win, 2, 2, "%s", buf + show_from);
wmove(win, 2, 2 + (int)(len - show_from));
wrefresh(win);
int ch = wgetch(win);
if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
result = 1;
break;
}
if (ch == 27) {
result = 0;
break;
}
if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
if (len > 0)
buf[--len] = '\0';
continue;
}
if (ch >= 32 && ch < 127 && len < cap) {
buf[len++] = (char)ch;
buf[len] = '\0';
}
}
curs_set(0);
close_box(win);
if (result)
snprintf(out, outsize, "%s", buf);
return result;
}
typedef struct {
char name[256];
int is_dir;
} BrowseEntry;
static int has_torrent_ext(const char *name)
{
size_t n = strlen(name);
return n > 8 && strcasecmp(name + n - 8, ".torrent") == 0;
}
static int browse_entry_cmp(const void *a, const void *b)
{
const BrowseEntry *ea = a, *eb = b;
if (ea->is_dir != eb->is_dir)
return eb->is_dir - ea->is_dir; /* directories first */
return strcasecmp(ea->name, eb->name);
}
/* Lists `path`'s entries into *out_entries (caller frees). In file mode
* shows subdirectories and *.torrent files (a plain directory can
* otherwise be full of unrelated files that just make picking a torrent
* tedious); in dirs_only mode shows only subdirectories, since a folder
* picker has no use for regular files at all. */
static int load_dir(const char *path, int dirs_only, BrowseEntry **out_entries, size_t *out_n)
{
DIR *d = opendir(path);
if (!d)
return -1;
size_t cap = 64, n = 0;
BrowseEntry *entries = malloc(cap * sizeof(BrowseEntry));
if (strcmp(path, "/") != 0) {
snprintf(entries[n].name, sizeof(entries[n].name), "..");
entries[n].is_dir = 1;
n++;
}
struct dirent *de;
while ((de = readdir(d)) != NULL) {
if (de->d_name[0] == '.')
continue; /* skip "." and hidden files/dirs */
char full[1280];
snprintf(full, sizeof(full), "%s/%s", path, de->d_name);
struct stat st;
if (stat(full, &st) != 0)
continue;
int is_dir = S_ISDIR(st.st_mode);
if (!is_dir && (dirs_only || !has_torrent_ext(de->d_name)))
continue;
if (n == cap) {
cap *= 2;
BrowseEntry *ne = realloc(entries, cap * sizeof(BrowseEntry));
if (!ne)
break;
entries = ne;
}
snprintf(entries[n].name, sizeof(entries[n].name), "%s", de->d_name);
entries[n].is_dir = is_dir;
n++;
}
closedir(d);
/* Keep ".." pinned first, sort the rest. */
size_t sort_off = (n > 0 && strcmp(entries[0].name, "..") == 0) ? 1 : 0;
if (n > sort_off + 1)
qsort(entries + sort_off, n - sort_off, sizeof(BrowseEntry), browse_entry_cmp);
*out_entries = entries;
*out_n = n;
return 0;
}
/* Remembered across calls in this run so re-opening a browser picks up
* where you left off instead of always starting back at $HOME - kept
* separate per mode since "last .torrent folder" and "last download
* folder" are usually different places. */
static char g_browse_file_dir[1024];
static char g_browse_folder_dir[1024];
/* Shared directory-browsing engine behind ui_file_browser()/
* ui_folder_browser(). In file mode (dirs_only=0): Enter descends into a
* directory or selects a highlighted *.torrent file. In dirs_only mode:
* only directories are listed/enterable, and `space` selects whichever
* directory is currently open (there's no file to press Enter on to
* finish). Backspace goes up either way. Returns 1 with `out` filled on
* selection, 0 on cancel. */
static int ui_browse(char *out, size_t outsize, int dirs_only, const char *title,
const char *start_hint)
{
char *remembered = dirs_only ? g_browse_folder_dir : g_browse_file_dir;
char path[1024];
struct stat hint_st;
if (start_hint && start_hint[0] && stat(start_hint, &hint_st) == 0 && S_ISDIR(hint_st.st_mode))
snprintf(path, sizeof(path), "%s", start_hint);
else if (remembered[0])
snprintf(path, sizeof(path), "%s", remembered);
else {
const char *home = getenv("HOME");
snprintf(path, sizeof(path), "%s", (home && home[0]) ? home : "/");
}
int cursor = 0, top = 0, result = 0;
for (;;) {
BrowseEntry *entries = NULL;
size_t n = 0;
if (load_dir(path, dirs_only, &entries, &n) != 0) {
ui_message("Error", "Could not open directory");
break;
}
if (cursor >= (int)n)
cursor = n ? (int)n - 1 : 0;
if (cursor < 0)
cursor = 0;
int rows, cols;
getmaxyx(stdscr, rows, cols);
int h = rows > 14 ? rows - 4 : rows;
int w = cols > 48 ? cols - 8 : cols;
WINDOW *win = open_box(h, w, title);
keypad(win, TRUE);
int list_top = 2;
int list_h = h - list_top - 2;
if (list_h < 1)
list_h = 1;
const char *hint = dirs_only ? "Enter=open space=select this folder Backspace=up Esc=cancel"
: "Enter=open/select Backspace=up Esc=cancel";
int reload = 0; /* 0=keep reading input, 1=dir changed, 2=done */
while (!reload) {
werase(win);
ui_box(win, h, w);
ui_box_title(win, w, title);
mvwprintw(win, 1, 2, "%.*s", w - 4, path);
if (cursor < top)
top = cursor;
if (cursor >= top + list_h)
top = cursor - list_h + 1;
if (top < 0)
top = 0;
if (n == 0)
mvwprintw(win, list_top, 2, dirs_only ? "(no subfolders here)"
: "(no subfolders or .torrent files here)");
for (int row = 0; row < list_h; row++) {
int idx = top + row;
if ((size_t)idx >= n)
break;
int attr = (idx == cursor) ? (COLOR_PAIR(CP_SELROW) | A_BOLD) : 0;
wattron(win, attr);
mvwprintw(win, list_top + row, 2, "%s%-*.*s",
entries[idx].is_dir ? "/ " : " ", w - 6, w - 6, entries[idx].name);
wattroff(win, attr);
}
wattron(win, COLOR_PAIR(CP_BORDER));
mvwprintw(win, h - 2, 2, "%.*s", w - 4, hint);
wattroff(win, COLOR_PAIR(CP_BORDER));
wrefresh(win);
int ch = wgetch(win);
switch (ch) {
case KEY_UP:
case 'k':
if (cursor > 0)
cursor--;
break;
case KEY_DOWN:
case 'j':
if ((size_t)(cursor + 1) < n)
cursor++;
break;
case KEY_BACKSPACE:
case 127:
case 8:
case KEY_LEFT:
if (strcmp(path, "/") != 0) {
char *slash = strrchr(path, '/');
if (slash == path)
path[1] = '\0';
else if (slash)
*slash = '\0';
cursor = 0;
top = 0;
reload = 1;
}
break;
case ' ':
if (dirs_only) {
snprintf(out, outsize, "%s", path);
snprintf(remembered, sizeof(path), "%s", path);
result = 1;
reload = 2;
}
break;
case '\n':
case '\r':
case KEY_ENTER:
if (n > 0) {
char candidate[1024];
size_t plen = strlen(path);
int root = (plen > 0 && path[plen - 1] == '/');
if (strcmp(entries[cursor].name, "..") == 0) {
snprintf(candidate, sizeof(candidate), "%s", path);
char *slash = strrchr(candidate, '/');
if (slash == candidate)
candidate[1] = '\0';
else if (slash)
*slash = '\0';
} else {
snprintf(candidate, sizeof(candidate), root ? "%s%s" : "%s/%s", path,
entries[cursor].name);
}
if (entries[cursor].is_dir) {
snprintf(path, sizeof(path), "%s", candidate);
cursor = 0;
top = 0;
reload = 1;
} else {
snprintf(out, outsize, "%s", candidate);
snprintf(remembered, sizeof(path), "%s", path);
result = 1;
reload = 2;
}
}
break;
case 27:
case 'q':
snprintf(remembered, sizeof(path), "%s", path);
result = 0;
reload = 2;
break;
default:
break;
}
}
free(entries);
close_box(win);
if (reload == 2)
break;
}
return result;
}
static int ui_file_browser(char *out, size_t outsize)
{
return ui_browse(out, outsize, 0, "Select .torrent file", NULL);
}
int ui_folder_browser(const char *start_hint, char *out, size_t outsize)
{
return ui_browse(out, outsize, 1, "Select download folder", start_hint);
}
void ui_dialog_add_torrent(AppState *st)
{
char source[1024] = "";
int have_source = 0;
if (ui_confirm("Add torrent", "Browse for a local .torrent file? (No = type magnet/URL/path)")) {
have_source = ui_file_browser(source, sizeof(source));
} else {
have_source = ui_prompt("Add torrent (magnet/URL/file path)", "", source, sizeof(source));
}
if (!have_source || source[0] == '\0')
return;
char dir[1024] = "";
if (!ui_confirm("Download folder", "Use the default download folder?")) {
if (ui_confirm("Download folder", "Browse for a folder? (No = type path)"))
ui_folder_browser(NULL, dir, sizeof(dir));
else
ui_prompt("Download folder", "", dir, sizeof(dir));
}
char err[256];
if (torrent_add(st->rpc, source, dir[0] ? dir : NULL, err, sizeof(err)) != 0)
ui_message("Error", err);
else
ui_set_status(st, "Torrent added");
}
void ui_dialog_remove(AppState *st, const int *ids, size_t n)
{
char msg[128];
snprintf(msg, sizeof(msg), "Remove %zu torrent(s) from the list?", n);
if (!ui_confirm("Remove", msg))
return;
int delete_data = ui_confirm("Remove", "Also delete the files on disk?");
char err[256];
if (torrent_remove(st->rpc, ids, n, delete_data, err, sizeof(err)) != 0)
ui_set_status(st, "Error: %s", err);
else
ui_set_status(st, "Removed %zu torrent(s)%s", n, delete_data ? " (incl. data)" : "");
}
void ui_dialog_speed_limit(AppState *st, const int *ids, size_t n)
{
char down[32] = "0", up[32] = "0";
if (!ui_prompt("Download limit KB/s (0 = unlimited)", "0", down, sizeof(down)))
return;
if (!ui_prompt("Upload limit KB/s (0 = unlimited)", "0", up, sizeof(up)))
return;
long down_kbps = strtol(down, NULL, 10);
long up_kbps = strtol(up, NULL, 10);
char err[256];
if (torrent_set_speed_limit(st->rpc, ids, n, down_kbps, down_kbps > 0, up_kbps, up_kbps > 0,
err, sizeof(err)) != 0)
ui_set_status(st, "Error: %s", err);
else
ui_set_status(st, "Speed limit set for %zu torrent(s)", n);
}