commit a2c07f1d2c30ed86f7e2910005e0d3412c436d7f
Author: MrJensK <jens.se@icloud.com>
AuthorDate: Fri Jul 31 14:31:18 2026 +0200
Commit: MrJensK <jens.se@icloud.com>
CommitDate: Fri Jul 31 14:31:18 2026 +0200
updates
---
docs/Architecture.md | 19 +++++++
docs/Configuration.md | 20 +++++++
docs/Interface.md | 27 ++++++++--
src/config.c | 50 ++++++++++++++++-
src/config.h | 24 +++++++++
src/ui/ui.c | 6 ++-
src/ui/ui.h | 12 +++++
src/ui/ui_dialogs.c | 100 ++++++++++++++++++++++++----------
src/ui/ui_settings.c | 147 ++++++++++++++++++++++++++++++++++++++++++++++++--
9 files changed, 362 insertions(+), 43 deletions(-)
diff --git a/docs/Architecture.md b/docs/Architecture.md
index b2c9202..f462783 100644
--- a/docs/Architecture.md
+++ b/docs/Architecture.md
@@ -86,6 +86,25 @@ state changed correctly but didn't always visibly redraw) - it's now a
dedicated full-screen view instead (same pattern as the help screen),
which sidesteps that whole class of overlapping-window issue.
+### Session settings survive an unclean daemon restart
+
+`ui_settings.c` reads/writes download dir, speed limits, alt-speed
+schedule, and ratio limit directly against the daemon via
+`session-get`/`session-set` - deliberately not cached locally as the
+source of truth, since the daemon is the shared, authoritative state.
+There's one durability gap in `transmission-daemon` itself though: it only
+writes `settings.json` to disk on a clean shutdown, not on every RPC
+change, so an unclean kill (crash, power loss, `kill -9`) before the next
+clean stop silently reverts any RPC-set session value. `reconcile_cache()`
+papers over this: `Config` keeps a `cache_*` snapshot of the last value
+the user explicitly set through `transtui` (see
+[Configuration](Configuration.md#session-settings-cache-cache_-keys)), and
+on every connect, if the daemon's current session doesn't match it, the
+cached value is pushed back and a status message reports what was
+restored. This is a deliberate one-way sync in `transtui`'s favor - it
+does not try to detect or preserve intentional changes made by other RPC
+clients since it was last used.
+
### Two pinned environment details
- **`LC_NUMERIC` pinned to `"C"`.** `setlocale(LC_ALL, "")` (needed for
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 38c4f34..feac948 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -27,6 +27,26 @@ The file is written automatically when you change connection or
splash-screen settings in the app (`g`, see [Interface](Interface.md#settings-g)) -
you rarely need to edit it by hand.
+### Session settings cache (`cache_*` keys)
+
+Whenever you change a daemon-side session setting - download folder, speed
+limits, alt-speed schedule, ratio limit - `transtui` also remembers that
+value in `config.ini` (as `session_cache_set` and a set of `cache_*` keys).
+This exists because `transmission-daemon` only writes its own
+`settings.json` to disk on a clean shutdown, not immediately when a
+setting is changed over RPC - if the daemon is later killed uncleanly
+(crash, power loss, `kill -9`) before a clean stop happens, it reloads its
+old `settings.json` on the next start and silently forgets the change.
+
+`transtui` checks for this every time it connects (at startup, and each
+time you open settings): if the daemon's current value doesn't match the
+cache, it pushes the cached value back and shows a status message like
+"Restored 1 setting(s) the daemon had reverted". This intentionally wins
+over changes made through any other RPC client (Transmission's own web UI,
+`transmission-remote`, ...) since `transtui` was last used - if you manage
+the daemon from multiple tools, keep that in mind. These keys are
+maintained automatically; there's normally no reason to edit them by hand.
+
## Command-line flags
```
diff --git a/docs/Interface.md b/docs/Interface.md
index f2da522..ab300bb 100644
--- a/docs/Interface.md
+++ b/docs/Interface.md
@@ -79,9 +79,11 @@ wrong host/port. Contains:
command-line flags (`-H`/`-P`/`-u`/`-p`), which are still read at startup
and override what's in `config.ini`.
- **Splash screen** (On/Off).
-- **Download folder** and speed limits/ratio limit/alt-speed schedule -
- read/written directly against the daemon via `session-get`/`session-set`,
- not saved locally.
+- **Download folder** (choose to browse or type a path, same picker as
+ described below) and speed limits/ratio limit/alt-speed schedule -
+ read/written directly against the daemon via `session-get`/`session-set`.
+ Also cached locally so an unclean daemon restart can't silently lose the
+ change - see [Configuration](Configuration.md#session-settings-cache-cache_-keys).
`↑`/`↓` navigates the fields, `Enter`/`space` changes a value or toggles
on/off, `Esc`/`q` closes. If the daemon isn't connected it's shown in the
@@ -110,5 +112,20 @@ showing only subfolders and `.torrent` files to keep it uncluttered:
| `Backspace` / `←` | go up one folder (also listed as `..`) |
| `Esc` / `q` | cancel |
-After picking a file (or typing a source), you're asked for a download
-folder (blank = the daemon's default).
+After picking a file (or typing a source), you're asked whether to use the
+daemon's default download folder, or choose one - and if choosing, whether
+to browse or type a path. The folder picker works like the file picker
+above, but only lists subfolders (no `.torrent` filter) and `space`
+selects whichever folder is currently open, since there's no file to press
+`Enter` on to finish:
+
+| Key | Function |
+|---|---|
+| `↑`/`k`, `↓`/`j` | move selection |
+| `Enter` | open the selected folder |
+| `space` | select the folder currently shown at the top of the box |
+| `Backspace` / `←` | go up one folder (also listed as `..`) |
+| `Esc` / `q` | cancel |
+
+The same folder picker is used for the **Download folder** field in
+settings (`g`).
diff --git a/src/config.c b/src/config.c
index 680e1cf..3561812 100644
--- a/src/config.c
+++ b/src/config.c
@@ -105,6 +105,34 @@ static void parse_file(Config *cfg, FILE *f)
cfg->poll_interval_ms = atoi(val);
else if (strcmp(key, "show_splash") == 0)
cfg->show_splash = (strcmp(val, "0") != 0 && strcasecmp(val, "false") != 0);
+ else if (strcmp(key, "session_cache_set") == 0)
+ cfg->session_cache_set = (strcmp(val, "0") != 0 && strcasecmp(val, "false") != 0);
+ else if (strcmp(key, "cache_download_dir") == 0)
+ snprintf(cfg->cache_download_dir, sizeof(cfg->cache_download_dir), "%s", val);
+ else if (strcmp(key, "cache_speed_down") == 0)
+ cfg->cache_speed_down = atol(val);
+ else if (strcmp(key, "cache_speed_down_enabled") == 0)
+ cfg->cache_speed_down_enabled = (strcmp(val, "0") != 0 && strcasecmp(val, "false") != 0);
+ else if (strcmp(key, "cache_speed_up") == 0)
+ cfg->cache_speed_up = atol(val);
+ else if (strcmp(key, "cache_speed_up_enabled") == 0)
+ cfg->cache_speed_up_enabled = (strcmp(val, "0") != 0 && strcasecmp(val, "false") != 0);
+ else if (strcmp(key, "cache_alt_enabled") == 0)
+ cfg->cache_alt_enabled = (strcmp(val, "0") != 0 && strcasecmp(val, "false") != 0);
+ else if (strcmp(key, "cache_alt_down") == 0)
+ cfg->cache_alt_down = atol(val);
+ else if (strcmp(key, "cache_alt_up") == 0)
+ cfg->cache_alt_up = atol(val);
+ else if (strcmp(key, "cache_alt_time_enabled") == 0)
+ cfg->cache_alt_time_enabled = (strcmp(val, "0") != 0 && strcasecmp(val, "false") != 0);
+ else if (strcmp(key, "cache_alt_time_begin") == 0)
+ cfg->cache_alt_time_begin = atoi(val);
+ else if (strcmp(key, "cache_alt_time_end") == 0)
+ cfg->cache_alt_time_end = atoi(val);
+ else if (strcmp(key, "cache_seed_ratio_limit") == 0)
+ cfg->cache_seed_ratio_limit = atof(val);
+ else if (strcmp(key, "cache_seed_ratio_enabled") == 0)
+ cfg->cache_seed_ratio_enabled = (strcmp(val, "0") != 0 && strcasecmp(val, "false") != 0);
}
}
@@ -153,9 +181,27 @@ int config_save(const Config *cfg, char *err, size_t errlen)
"username = %s\n"
"password = %s\n"
"poll_interval_ms = %d\n"
- "show_splash = %d\n",
+ "show_splash = %d\n"
+ "session_cache_set = %d\n"
+ "cache_download_dir = %s\n"
+ "cache_speed_down = %ld\n"
+ "cache_speed_down_enabled = %d\n"
+ "cache_speed_up = %ld\n"
+ "cache_speed_up_enabled = %d\n"
+ "cache_alt_enabled = %d\n"
+ "cache_alt_down = %ld\n"
+ "cache_alt_up = %ld\n"
+ "cache_alt_time_enabled = %d\n"
+ "cache_alt_time_begin = %d\n"
+ "cache_alt_time_end = %d\n"
+ "cache_seed_ratio_limit = %.2f\n"
+ "cache_seed_ratio_enabled = %d\n",
cfg->host, cfg->port, cfg->username, cfg->password, cfg->poll_interval_ms,
- cfg->show_splash);
+ cfg->show_splash, cfg->session_cache_set, cfg->cache_download_dir,
+ cfg->cache_speed_down, cfg->cache_speed_down_enabled, cfg->cache_speed_up,
+ cfg->cache_speed_up_enabled, cfg->cache_alt_enabled, cfg->cache_alt_down,
+ cfg->cache_alt_up, cfg->cache_alt_time_enabled, cfg->cache_alt_time_begin,
+ cfg->cache_alt_time_end, cfg->cache_seed_ratio_limit, cfg->cache_seed_ratio_enabled);
fclose(f);
return 0;
}
diff --git a/src/config.h b/src/config.h
index dd9b7f9..02cb227 100644
--- a/src/config.h
+++ b/src/config.h
@@ -11,6 +11,30 @@ typedef struct {
int poll_interval_ms;
int show_splash;
char path[1024]; /* resolved config file path, for messages */
+
+ /* Cache of the session settings (download dir, speed limits, ...) the
+ * user last explicitly set via the in-app settings screen.
+ * transmission-daemon only writes its own settings.json to disk on a
+ * clean shutdown, not immediately when changed over RPC - if it's later
+ * killed uncleanly (crash, power loss, kill -9) before that, the change
+ * is lost and the daemon reverts to whatever it last saved. transtui
+ * re-applies this cache whenever the daemon's current session doesn't
+ * match it (see ui_settings.c), so an intentional change survives an
+ * unclean daemon restart even though the daemon itself forgot it. */
+ int session_cache_set; /* 0 until a session field has been changed at least once */
+ char cache_download_dir[512];
+ long cache_speed_down;
+ int cache_speed_down_enabled;
+ long cache_speed_up;
+ int cache_speed_up_enabled;
+ int cache_alt_enabled;
+ long cache_alt_down;
+ long cache_alt_up;
+ int cache_alt_time_enabled;
+ int cache_alt_time_begin;
+ int cache_alt_time_end;
+ double cache_seed_ratio_limit;
+ int cache_seed_ratio_enabled;
} Config;
/* Loads ~/.config/transtui/config.ini (or $XDG_CONFIG_HOME/transtui/config.ini).
diff --git a/src/ui/ui.c b/src/ui/ui.c
index fd2bb98..655d933 100644
--- a/src/ui/ui.c
+++ b/src/ui/ui.c
@@ -413,10 +413,12 @@ int ui_run(RpcClient *rpc, Config *cfg)
int poll_tick_ms = 200; /* how often we wake up to check the poll interval / redraw */
timeout(poll_tick_ms);
- if (ui_refresh_list(&st) != 0)
+ if (ui_refresh_list(&st) != 0) {
ui_offer_reconnect_help(&st);
- else
+ } else {
ui_set_status(&st, "Connected to %s:%d", cfg->host, cfg->port);
+ ui_settings_reconcile_now(&st);
+ }
/* Only redraw when something could actually have changed (a key was
* handled, or a periodic RPC refresh landed) - not unconditionally on
diff --git a/src/ui/ui.h b/src/ui/ui.h
index 129c348..97f85c0 100644
--- a/src/ui/ui.h
+++ b/src/ui/ui.h
@@ -144,6 +144,12 @@ void ui_draw_footer(const KeyHint *hints, size_t n, int nrows);
int ui_confirm(const char *title, const char *msg);
int ui_prompt(const char *title, const char *initial, char *out, size_t outsize);
void ui_message(const char *title, const char *msg);
+/* Directory picker: navigate with arrows/Enter, `space` selects whichever
+ * folder is currently open, Backspace goes up, Esc cancels. start_hint (may
+ * be NULL/empty/nonexistent) is where browsing begins if it's a real
+ * directory; otherwise falls back to the last folder browsed to, then
+ * $HOME. Returns 1 with `out` filled on selection, 0 on cancel. */
+int ui_folder_browser(const char *start_hint, char *out, size_t outsize);
void ui_dialog_add_torrent(AppState *st);
void ui_dialog_remove(AppState *st, const int *ids, size_t n);
@@ -161,6 +167,12 @@ void ui_detail_handle_key(AppState *st, int ch);
void ui_settings_render(AppState *st);
void ui_settings_handle_key(AppState *st, int ch);
+/* Fetches the daemon's current session settings and, if they don't match
+ * what the user last explicitly set via the settings screen, pushes the
+ * remembered values back - see reconcile_cache() in ui_settings.c. Called
+ * once after the initial connection so an unclean daemon restart gets
+ * fixed up even if the user never reopens settings. */
+void ui_settings_reconcile_now(AppState *st);
void ui_help_render(AppState *st);
void ui_help_handle_key(AppState *st, int ch);
diff --git a/src/ui/ui_dialogs.c b/src/ui/ui_dialogs.c
index 5501603..b1e063f 100644
--- a/src/ui/ui_dialogs.c
+++ b/src/ui/ui_dialogs.c
@@ -50,14 +50,15 @@ int ui_confirm(const char *title, const char *msg)
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 [n]o");
+ 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') {
+ if (ch == 'y' || ch == 'Y' || ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
result = 1;
break;
}
@@ -160,10 +161,12 @@ static int browse_entry_cmp(const void *a, const void *b)
return strcasecmp(ea->name, eb->name);
}
-/* Lists `path`'s entries into *out_entries (caller frees), showing only
- * subdirectories and *.torrent files - a plain directory can otherwise be
- * full of unrelated files that just make picking a torrent tedious. */
-static int load_dir(const char *path, BrowseEntry **out_entries, size_t *out_n)
+/* 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)
@@ -187,7 +190,7 @@ static int load_dir(const char *path, BrowseEntry **out_entries, size_t *out_n)
if (stat(full, &st) != 0)
continue;
int is_dir = S_ISDIR(st.st_mode);
- if (!is_dir && !has_torrent_ext(de->d_name))
+ if (!is_dir && (dirs_only || !has_torrent_ext(de->d_name)))
continue;
if (n == cap) {
cap *= 2;
@@ -212,28 +215,41 @@ static int load_dir(const char *path, BrowseEntry **out_entries, size_t *out_n)
return 0;
}
-/* Remembered across calls in this run so re-opening the browser picks up
- * where you left off instead of always starting back at $HOME. */
-static char g_browse_dir[1024];
-
-/* Simple two-pane-free directory browser: navigate with arrows, Enter
- * descends into a directory or selects a *.torrent file, Backspace goes up.
- * Returns 1 with `out` filled on selection, 0 on cancel. */
-static int ui_file_browser(char *out, size_t outsize)
+/* 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)
{
- if (!g_browse_dir[0]) {
+ 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(g_browse_dir, sizeof(g_browse_dir), "%s", (home && home[0]) ? home : "/");
+ snprintf(path, sizeof(path), "%s", (home && home[0]) ? home : "/");
}
- char path[1024];
- snprintf(path, sizeof(path), "%s", g_browse_dir);
int cursor = 0, top = 0, result = 0;
for (;;) {
BrowseEntry *entries = NULL;
size_t n = 0;
- if (load_dir(path, &entries, &n) != 0) {
+ if (load_dir(path, dirs_only, &entries, &n) != 0) {
ui_message("Error", "Could not open directory");
break;
}
@@ -246,18 +262,21 @@ static int ui_file_browser(char *out, size_t outsize)
getmaxyx(stdscr, rows, cols);
int h = rows > 14 ? rows - 4 : rows;
int w = cols > 48 ? cols - 8 : cols;
- WINDOW *win = open_box(h, w, "Select .torrent file");
+ 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, "Select .torrent file");
+ ui_box_title(win, w, title);
mvwprintw(win, 1, 2, "%.*s", w - 4, path);
if (cursor < top)
@@ -268,7 +287,8 @@ static int ui_file_browser(char *out, size_t outsize)
top = 0;
if (n == 0)
- mvwprintw(win, list_top, 2, "(no subfolders or .torrent files here)");
+ 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)
@@ -280,8 +300,7 @@ static int ui_file_browser(char *out, size_t outsize)
wattroff(win, attr);
}
wattron(win, COLOR_PAIR(CP_BORDER));
- mvwprintw(win, h - 2, 2, "%.*s", w - 4,
- "Enter=open/select Backspace=up Esc=cancel");
+ mvwprintw(win, h - 2, 2, "%.*s", w - 4, hint);
wattroff(win, COLOR_PAIR(CP_BORDER));
wrefresh(win);
@@ -312,6 +331,14 @@ static int ui_file_browser(char *out, size_t outsize)
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:
@@ -337,7 +364,7 @@ static int ui_file_browser(char *out, size_t outsize)
reload = 1;
} else {
snprintf(out, outsize, "%s", candidate);
- snprintf(g_browse_dir, sizeof(g_browse_dir), "%s", path);
+ snprintf(remembered, sizeof(path), "%s", path);
result = 1;
reload = 2;
}
@@ -345,7 +372,7 @@ static int ui_file_browser(char *out, size_t outsize)
break;
case 27:
case 'q':
- snprintf(g_browse_dir, sizeof(g_browse_dir), "%s", path);
+ snprintf(remembered, sizeof(path), "%s", path);
result = 0;
reload = 2;
break;
@@ -361,6 +388,16 @@ static int ui_file_browser(char *out, size_t outsize)
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] = "";
@@ -373,8 +410,13 @@ void ui_dialog_add_torrent(AppState *st)
if (!have_source || source[0] == '\0')
return;
- char dir[512] = "";
- ui_prompt("Download folder (blank = default)", "", dir, sizeof(dir));
+ 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)
diff --git a/src/ui/ui_settings.c b/src/ui/ui_settings.c
index b92db60..b3a2985 100644
--- a/src/ui/ui_settings.c
+++ b/src/ui/ui_settings.c
@@ -122,6 +122,107 @@ static int parse_hhmm(const char *s)
return h * 60 + m;
}
+/* If the daemon's current session settings don't match what the user last
+ * explicitly set via this screen (cfg->cache_*), push the cached values
+ * back - transmission-daemon only writes settings.json on a clean
+ * shutdown, so an unclean restart (crash, power loss, kill -9) silently
+ * reverts RPC-set session values to whatever was last saved. This keeps
+ * the user's intent durable across that. Only runs once cache_set has ever
+ * been populated (i.e. the user has changed a session field at least once
+ * via transtui), and intentionally wins over changes made by other RPC
+ * clients since transtui was last opened - that tradeoff is what the user
+ * asked for. */
+static void reconcile_cache(AppState *st)
+{
+ Config *cfg = st->cfg;
+ if (!cfg->session_cache_set)
+ return;
+
+ cJSON *args = cJSON_CreateObject();
+ int changed = 0;
+
+ if (strcmp(cfg->cache_download_dir, g_sess.download_dir) != 0) {
+ cJSON_AddStringToObject(args, "download-dir", cfg->cache_download_dir);
+ changed++;
+ }
+ if (cfg->cache_speed_down_enabled != g_sess.speed_down_enabled) {
+ cJSON_AddBoolToObject(args, "speed-limit-down-enabled", cfg->cache_speed_down_enabled);
+ changed++;
+ }
+ if (cfg->cache_speed_down != g_sess.speed_down) {
+ cJSON_AddNumberToObject(args, "speed-limit-down", (double)cfg->cache_speed_down);
+ changed++;
+ }
+ if (cfg->cache_speed_up_enabled != g_sess.speed_up_enabled) {
+ cJSON_AddBoolToObject(args, "speed-limit-up-enabled", cfg->cache_speed_up_enabled);
+ changed++;
+ }
+ if (cfg->cache_speed_up != g_sess.speed_up) {
+ cJSON_AddNumberToObject(args, "speed-limit-up", (double)cfg->cache_speed_up);
+ changed++;
+ }
+ if (cfg->cache_alt_enabled != g_sess.alt_enabled) {
+ cJSON_AddBoolToObject(args, "alt-speed-enabled", cfg->cache_alt_enabled);
+ changed++;
+ }
+ if (cfg->cache_alt_down != g_sess.alt_down) {
+ cJSON_AddNumberToObject(args, "alt-speed-down", (double)cfg->cache_alt_down);
+ changed++;
+ }
+ if (cfg->cache_alt_up != g_sess.alt_up) {
+ cJSON_AddNumberToObject(args, "alt-speed-up", (double)cfg->cache_alt_up);
+ changed++;
+ }
+ if (cfg->cache_alt_time_enabled != g_sess.alt_time_enabled) {
+ cJSON_AddBoolToObject(args, "alt-speed-time-enabled", cfg->cache_alt_time_enabled);
+ changed++;
+ }
+ if (cfg->cache_alt_time_begin != g_sess.alt_time_begin) {
+ cJSON_AddNumberToObject(args, "alt-speed-time-begin", cfg->cache_alt_time_begin);
+ changed++;
+ }
+ if (cfg->cache_alt_time_end != g_sess.alt_time_end) {
+ cJSON_AddNumberToObject(args, "alt-speed-time-end", cfg->cache_alt_time_end);
+ changed++;
+ }
+ if (cfg->cache_seed_ratio_enabled != g_sess.seed_ratio_enabled) {
+ cJSON_AddBoolToObject(args, "seedRatioLimited", cfg->cache_seed_ratio_enabled);
+ changed++;
+ }
+ double ratio_diff = cfg->cache_seed_ratio_limit - g_sess.seed_ratio_limit;
+ if (ratio_diff < 0)
+ ratio_diff = -ratio_diff;
+ if (ratio_diff > 0.001) {
+ cJSON_AddNumberToObject(args, "seedRatioLimit", cfg->cache_seed_ratio_limit);
+ changed++;
+ }
+
+ if (changed == 0) {
+ cJSON_Delete(args);
+ return;
+ }
+
+ char err[256];
+ if (push_session(st, args, err, sizeof(err)) == 0) {
+ snprintf(g_sess.download_dir, sizeof(g_sess.download_dir), "%s", cfg->cache_download_dir);
+ g_sess.speed_down = cfg->cache_speed_down;
+ g_sess.speed_down_enabled = cfg->cache_speed_down_enabled;
+ g_sess.speed_up = cfg->cache_speed_up;
+ g_sess.speed_up_enabled = cfg->cache_speed_up_enabled;
+ g_sess.alt_enabled = cfg->cache_alt_enabled;
+ g_sess.alt_down = cfg->cache_alt_down;
+ g_sess.alt_up = cfg->cache_alt_up;
+ g_sess.alt_time_enabled = cfg->cache_alt_time_enabled;
+ g_sess.alt_time_begin = cfg->cache_alt_time_begin;
+ g_sess.alt_time_end = cfg->cache_alt_time_end;
+ g_sess.seed_ratio_limit = cfg->cache_seed_ratio_limit;
+ g_sess.seed_ratio_enabled = cfg->cache_seed_ratio_enabled;
+ ui_set_status(st, "Restored %d setting(s) the daemon had reverted", changed);
+ }
+ /* On failure just leave the daemon's current values shown - don't block
+ * opening settings or nag about it every time. */
+}
+
/* Popup geometry, shared between render and the row->y mapping used by input. */
static int g_popup_h, g_popup_w;
@@ -133,6 +234,13 @@ static int field_row(int field)
return row;
}
+void ui_settings_reconcile_now(AppState *st)
+{
+ char err[256];
+ if (fetch_session(st, err, sizeof(err)) == 0)
+ reconcile_cache(st);
+}
+
void ui_settings_render(AppState *st)
{
/* Always open the popup, even if the daemon is unreachable - that's
@@ -145,6 +253,8 @@ void ui_settings_render(AppState *st)
if (!g_connected) {
memset(&g_sess, 0, sizeof(g_sess));
ui_set_status(st, "Not connected: %s", err);
+ } else {
+ reconcile_cache(st);
}
g_attempted = 1;
g_cursor = 0;
@@ -288,8 +398,14 @@ static void apply_field(AppState *st, int field)
break;
case F_DOWNLOAD_DIR: {
char v[512];
- snprintf(v, sizeof(v), "%s", g_sess.download_dir);
- if (ui_prompt("Download folder", v, v, sizeof(v))) {
+ int got;
+ if (ui_confirm("Download folder", "Browse for a folder? (No = type path)")) {
+ got = ui_folder_browser(g_sess.download_dir, v, sizeof(v));
+ } else {
+ snprintf(v, sizeof(v), "%s", g_sess.download_dir);
+ got = ui_prompt("Download folder", v, v, sizeof(v));
+ }
+ if (got) {
snprintf(g_sess.download_dir, sizeof(g_sess.download_dir), "%s", v);
cJSON_AddStringToObject(args, "download-dir", g_sess.download_dir);
} else {
@@ -444,10 +560,31 @@ static void apply_field(AppState *st, int field)
return;
}
- if (push_session(st, args, err, sizeof(err)) != 0)
+ if (push_session(st, args, err, sizeof(err)) != 0) {
ui_set_status(st, "Error: %s", err);
- else
- ui_set_status(st, "Setting saved");
+ return;
+ }
+
+ /* Remember what was just set so it can be restored if the daemon later
+ * forgets it - see reconcile_cache() and Config's cache_* fields. */
+ Config *cfg = st->cfg;
+ cfg->session_cache_set = 1;
+ snprintf(cfg->cache_download_dir, sizeof(cfg->cache_download_dir), "%s", g_sess.download_dir);
+ cfg->cache_speed_down = g_sess.speed_down;
+ cfg->cache_speed_down_enabled = g_sess.speed_down_enabled;
+ cfg->cache_speed_up = g_sess.speed_up;
+ cfg->cache_speed_up_enabled = g_sess.speed_up_enabled;
+ cfg->cache_alt_enabled = g_sess.alt_enabled;
+ cfg->cache_alt_down = g_sess.alt_down;
+ cfg->cache_alt_up = g_sess.alt_up;
+ cfg->cache_alt_time_enabled = g_sess.alt_time_enabled;
+ cfg->cache_alt_time_begin = g_sess.alt_time_begin;
+ cfg->cache_alt_time_end = g_sess.alt_time_end;
+ cfg->cache_seed_ratio_limit = g_sess.seed_ratio_limit;
+ cfg->cache_seed_ratio_enabled = g_sess.seed_ratio_enabled;
+ config_save(cfg, err, sizeof(err)); /* best effort - a failure here just means no cache this time */
+
+ ui_set_status(st, "Setting saved");
}
void ui_settings_handle_key(AppState *st, int ch)