foxygit / TransTUI Log in
commits tags

/src/ui/ui_dialogs.c · 14.38 KB

raw
#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);
}