summarylogtreecommitdiffstats
diff options
context:
space:
mode:
authorqdesjardin2019-10-17 11:53:12 -0600
committerqdesjardin2019-10-17 12:15:20 -0600
commitbe66c45ee1342caf8b69464f05f97664d55a4654 (patch)
tree58a6f0f98efe85b2b6430f8c73f341632f0085a8
downloadaur-be66c45ee1342caf8b69464f05f97664d55a4654.tar.gz
Added .SRCINFO file.
-rw-r--r--.SRCINFO59
-rwxr-xr-xPKGBUILD115
-rw-r--r--backport_rt_all_02-display_throttle_speed.patch263
-rw-r--r--backport_rt_all_04-partially_done_and_choke_group_fix.patch653
-rw-r--r--backport_rt_all_05-honor_system_file_allocate_fix.patch202
-rw-r--r--backport_rt_all_08-info_pane_xb_sizes.patch76
-rw-r--r--backport_rt_all_09-inotify_mod.patch137
-rw-r--r--backport_rt_all_80-ps-dl-ui-find.patch51
-rw-r--r--command_pyroscope.cc1370
-rw-r--r--ps-import.return_all.patch10
-rw-r--r--ps-include-timestamps_all.patch18
-rw-r--r--ps-info-pane-is-default_all.patch12
-rw-r--r--ps-issue-515_all.patch29
-rw-r--r--ps-item-stats-human-sizes_all.patch35
-rw-r--r--ps-log_messages_all.patch33
-rw-r--r--ps-object_std-map-serialization_all.patch24
-rw-r--r--ps-silent-catch_all.patch18
-rw-r--r--ps-ui_pyroscope_all.patch7
-rw-r--r--pyroscope_all.patch26
-rw-r--r--ui_pyroscope.cc1445
-rw-r--r--ui_pyroscope.h84
-rw-r--r--ui_pyroscope_all.patch83
22 files changed, 4750 insertions, 0 deletions
diff --git a/.SRCINFO b/.SRCINFO
new file mode 100644
index 000000000000..8b2608476801
--- /dev/null
+++ b/.SRCINFO
@@ -0,0 +1,59 @@
+pkgbase = rtorrent-ps-ch
+ pkgdesc = Extended rTorrent-ps with additional UI patches, general fixes and enhancements
+ pkgver = 1.8.3
+ pkgrel = 1
+ url = https://github.com/chros73/rtorrent-ps-ch
+ arch = any
+ license = GPL2
+ depends = curl>=7.15.4
+ depends = ncurses
+ depends = libtorrent-ps-ch
+ depends = xmlrpc-c
+ provides = rtorrent
+ conflicts = rtorrent
+ conflicts = rtorrent-ps
+ source = https://github.com/rakshasa/rtorrent/archive/v0.9.8.tar.gz
+ source = backport_rt_all_02-display_throttle_speed.patch
+ source = backport_rt_all_04-partially_done_and_choke_group_fix.patch
+ source = backport_rt_all_05-honor_system_file_allocate_fix.patch
+ source = backport_rt_all_08-info_pane_xb_sizes.patch
+ source = backport_rt_all_09-inotify_mod.patch
+ source = backport_rt_all_80-ps-dl-ui-find.patch
+ source = command_pyroscope.cc
+ source = ps-import.return_all.patch
+ source = ps-include-timestamps_all.patch
+ source = ps-info-pane-is-default_all.patch
+ source = ps-issue-515_all.patch
+ source = ps-item-stats-human-sizes_all.patch
+ source = ps-log_messages_all.patch
+ source = ps-object_std-map-serialization_all.patch
+ source = ps-silent-catch_all.patch
+ source = ps-ui_pyroscope_all.patch
+ source = pyroscope_all.patch
+ source = ui_pyroscope_all.patch
+ source = ui_pyroscope.cc
+ source = ui_pyroscope.h
+ md5sums = ca17bdc9eeec19a8dd50cc5c5cf5daf1
+ md5sums = ce66d01d8f2a340a40620c579f568fd7
+ md5sums = b0fb83dbb862afe535ad60effe8aea1f
+ md5sums = 9b4fee1aaeb4174edea92b2227ba708b
+ md5sums = b78b77a7a757bfd49bc280d07470c09e
+ md5sums = 81bc9756831d54e2f2960a20cda1d049
+ md5sums = 4861fe6f9530436490f8a6e70a5d7fac
+ md5sums = d68073da455851d628b587b852b4b54a
+ md5sums = cc9bbf20acf855e551ca2f80cac91903
+ md5sums = af57d10774c66c9cc0e9d3a74fff226d
+ md5sums = 398c132d99dcf9f45252043ece176dd6
+ md5sums = c4b419c3ebdb856ab02d68955d66eea8
+ md5sums = 2d34e8c86c1c6ed1354b55ca21819886
+ md5sums = a4f5a4da3397e4b1d71eb59a5e8e0279
+ md5sums = 0fa551b7cba264bd906e32827d06700c
+ md5sums = e3f367abe42d28168008f99a9bf0f1d6
+ md5sums = 7a88f8ab5d41242fdf1428de0e2ca182
+ md5sums = bd04a0699b80c8042e1cf63a7e0e4222
+ md5sums = b9578a640f5ee30c1a50dccf7531064c
+ md5sums = 5befaa2e705a550a6dcd7f397060df81
+ md5sums = 0e9791d796e2185279d7f109b064576b
+
+pkgname = rtorrent-ps-ch
+
diff --git a/PKGBUILD b/PKGBUILD
new file mode 100755
index 000000000000..dbdf343bf62b
--- /dev/null
+++ b/PKGBUILD
@@ -0,0 +1,115 @@
+# Maintainer: qdesjardin <qdesjardin gmail com>
+
+_pkgname=rtorrent
+pkgname=rtorrent-ps-ch
+_pkgver=0.9.8
+pkgver=1.8.3
+pkgrel=1
+pkgdesc='Extended rTorrent-ps with additional UI patches, general fixes and enhancements'
+license=('GPL2')
+arch=('any')
+url='https://github.com/chros73/rtorrent-ps-ch'
+depends=('curl>=7.15.4' 'ncurses' 'xmlrpc-c')
+provides=('rtorrent')
+conflicts=('rtorrent' 'rtorrent-ps')
+
+source=("https://github.com/rakshasa/$_pkgname/archive/v$_pkgver.tar.gz"
+ 'backport_rt_all_02-display_throttle_speed.patch'
+ 'backport_rt_all_04-partially_done_and_choke_group_fix.patch'
+ 'backport_rt_all_05-honor_system_file_allocate_fix.patch'
+ 'backport_rt_all_08-info_pane_xb_sizes.patch'
+ 'backport_rt_all_09-inotify_mod.patch'
+ 'backport_rt_all_80-ps-dl-ui-find.patch'
+ 'command_pyroscope.cc'
+ 'ps-import.return_all.patch'
+ 'ps-include-timestamps_all.patch'
+ 'ps-info-pane-is-default_all.patch'
+ 'ps-issue-515_all.patch'
+ 'ps-item-stats-human-sizes_all.patch'
+ 'ps-log_messages_all.patch'
+ 'ps-object_std-map-serialization_all.patch'
+ 'ps-silent-catch_all.patch'
+ 'ps-ui_pyroscope_all.patch'
+ 'pyroscope_all.patch'
+ 'ui_pyroscope_all.patch'
+ 'ui_pyroscope.cc'
+ 'ui_pyroscope.h')
+
+md5sums=('ca17bdc9eeec19a8dd50cc5c5cf5daf1'
+ 'ce66d01d8f2a340a40620c579f568fd7'
+ 'b0fb83dbb862afe535ad60effe8aea1f'
+ '9b4fee1aaeb4174edea92b2227ba708b'
+ 'b78b77a7a757bfd49bc280d07470c09e'
+ '81bc9756831d54e2f2960a20cda1d049'
+ '4861fe6f9530436490f8a6e70a5d7fac'
+ 'd68073da455851d628b587b852b4b54a'
+ 'cc9bbf20acf855e551ca2f80cac91903'
+ 'af57d10774c66c9cc0e9d3a74fff226d'
+ '398c132d99dcf9f45252043ece176dd6'
+ 'c4b419c3ebdb856ab02d68955d66eea8'
+ '2d34e8c86c1c6ed1354b55ca21819886'
+ 'a4f5a4da3397e4b1d71eb59a5e8e0279'
+ '0fa551b7cba264bd906e32827d06700c'
+ 'e3f367abe42d28168008f99a9bf0f1d6'
+ '7a88f8ab5d41242fdf1428de0e2ca182'
+ 'bd04a0699b80c8042e1cf63a7e0e4222'
+ 'b9578a640f5ee30c1a50dccf7531064c'
+ '5befaa2e705a550a6dcd7f397060df81'
+ '0e9791d796e2185279d7f109b064576b')
+
+prepare() {
+ cd "$srcdir/$_pkgname-$_pkgver"
+
+ # Version Handling
+ rt_hex_version=$(printf "0x%02X%02X%02X" ${pkgver//./ })
+ sed -i "s:\\(AC_DEFINE(HAVE_CONFIG_H.*\\):\1 AC_DEFINE(RT_HEX_VERSION, $rt_hex_version, for CPP if checks):" configure.ac
+
+ sed -i "s%rTorrent \\\" VERSION \\\"/\\\"%$pkgname $pkgver \\\"%" src/ui/download_list.cc
+ sed -i "s%std::string(torrent::version()) + \\\" - \\\" +%%" src/ui/download_list.cc
+
+ # Patching to rtorrent-ps-ch
+ for corepatch in "$srcdir"/ps-*.patch; do
+ test ! -e "$corepatch" || { msg2 "$(basename $corepatch)"; patch -uNp1 -i "$corepatch"; }
+ done
+
+ for backport in "$srcdir"/{backport,misc}_rt_*.patch; do
+ test ! -e "$backport" || { msg2 "$(basename $backport)"; patch -uNp1 -i "$backport"; }
+ done
+
+ for pyropatch in "$srcdir"/pyroscope_*.patch; do
+ test ! -e "$pyropatch" || { msg2 "$(basename $pyropatch)"; patch -uNp1 -i "$pyropatch"; }
+ done
+
+ for i in "$srcdir"/*.{cc,h}; do
+ ln -nfs "$i" src
+ done
+
+ for uipyropatch in "$srcdir"/ui_pyroscope_*.patch; do
+ test ! -e "$uipyropatch" || { msg2 "$(basename $uipyropatch)"; patch -uNp1 -i "$uipyropatch"; }
+ done
+
+ ./autogen.sh
+}
+
+build() {
+ cd "$srcdir/$_pkgname-$_pkgver"
+
+ ./configure \
+ --prefix=/usr \
+ --with-ncursesw \
+ --with-xmlrpc-c \
+ --disable-debug
+
+ make
+}
+
+package() {
+ cd "$srcdir/$_pkgname-$_pkgver"
+
+ make DESTDIR="$pkgdir" install
+
+ install -Dm644 "doc/faq.xml" "$pkgdir/usr/share/doc/$_pkgname/faq.xml"
+ install -Dm644 "doc/old/rtorrent.1" "$pkgdir/usr/share/man/man1/$_pkgname.1"
+ install -Dm644 "doc/rtorrent.rc" "$pkgdir/usr/share/doc/$_pkgname/rtorrent.rc"
+ install -Dm644 "doc/rtorrent_fast_resume.pl" "$pkgdir/usr/share/doc/$_pkgname/rtorrent_fast_resume.pl"
+}
diff --git a/backport_rt_all_02-display_throttle_speed.patch b/backport_rt_all_02-display_throttle_speed.patch
new file mode 100644
index 000000000000..9e964350a9ba
--- /dev/null
+++ b/backport_rt_all_02-display_throttle_speed.patch
@@ -0,0 +1,263 @@
+--- a/src/command_ui.cc 2017-04-30 20:45:24.094335223 +0100
++++ a/src/command_ui.cc 2017-04-30 20:56:18.000000000 +0100
+@@ -568,6 +568,26 @@ apply_elapsed_greater(const torrent::Obj
+ return (int64_t)(start_time != 0 && rak::timer::current_seconds() - start_time > rpc::convert_to_value(args.back()));
+ }
+
++torrent::Object
++cmd_status_throttle_names(bool up, const torrent::Object::list_type& args) {
++ if (args.size() == 0)
++ return torrent::Object();
++
++ std::vector<std::string> throttle_name_list;
++
++ for (torrent::Object::list_const_iterator itr = args.begin(), last = args.end(); itr != last; itr++) {
++ if (itr->is_string())
++ throttle_name_list.push_back(itr->as_string());
++ }
++
++ if (up)
++ control->ui()->set_status_throttle_up_names(throttle_name_list);
++ else
++ control->ui()->set_status_throttle_down_names(throttle_name_list);
++
++ return torrent::Object();
++}
++
+ void
+ initialize_command_ui() {
+ CMD2_VAR_STRING("keys.layout", "qwerty");
+@@ -608,6 +628,9 @@ initialize_command_ui() {
+ CMD2_ANY ("ui.current_view", std::bind(&cmd_ui_current_view));
+ CMD2_ANY_STRING("ui.current_view.set", std::bind(&cmd_ui_set_view, std::placeholders::_2));
+
++ CMD2_ANY_LIST ("ui.status.throttle.up.set", std::bind(&cmd_status_throttle_names, true, std::placeholders::_2));
++ CMD2_ANY_LIST ("ui.status.throttle.down.set", std::bind(&cmd_status_throttle_names, false, std::placeholders::_2));
++
+ // TODO: Add 'option_string' for rtorrent-specific options.
+ CMD2_VAR_STRING("ui.torrent_list.layout", "full");
+
+--- a/src/core/manager.cc 2017-04-30 20:37:29.000000000 +0100
++++ a/src/core/manager.cc 2017-04-30 20:56:18.000000000 +0100
+@@ -146,6 +146,35 @@ Manager::get_address_throttle(const sock
+ return m_addressThrottles.get(rak::socket_address::cast_from(addr)->sa_inet()->address_h(), torrent::ThrottlePair(NULL, NULL));
+ }
+
++int64_t
++Manager::retrieve_throttle_value(const torrent::Object::string_type& name, bool rate, bool up) {
++ ThrottleMap::iterator itr = throttles().find(name);
++
++ if (itr == throttles().end()) {
++ return (int64_t)-1;
++ } else {
++ torrent::Throttle* throttle = up ? itr->second.first : itr->second.second;
++
++ // check whether the actual up/down throttle exist (one of the pair can be missing)
++ if (throttle == NULL)
++ return (int64_t)-1;
++
++ int64_t throttle_max = (int64_t)throttle->max_rate();
++
++ if (rate) {
++
++ if (throttle_max > 0)
++ return (int64_t)throttle->rate()->rate();
++ else
++ return (int64_t)-1;
++
++ } else {
++ return throttle_max;
++ }
++
++ }
++}
++
+ // Most of this should be possible to move out.
+ void
+ Manager::initialize_second() {
+--- a/src/core/manager.h 2016-10-23 05:33:00.000000000 +0100
++++ a/src/core/manager.h 2017-04-30 20:56:18.000000000 +0100
+@@ -42,6 +42,7 @@
+
+ #include <torrent/utils/log_buffer.h>
+ #include <torrent/connection_manager.h>
++#include <torrent/object.h>
+
+ #include "download_list.h"
+ #include "poll_manager.h"
+@@ -91,6 +92,8 @@ public:
+ ThrottleMap& throttles() { return m_throttles; }
+ torrent::ThrottlePair get_throttle(const std::string& name);
+
++ int64_t retrieve_throttle_value(const torrent::Object::string_type& name, bool rate, bool up);
++
+ // Use custom throttle for the given range of IP addresses.
+ void set_address_throttle(uint32_t begin, uint32_t end, torrent::ThrottlePair throttles);
+ torrent::ThrottlePair get_address_throttle(const sockaddr* addr);
+--- a/src/display/utils.cc 2017-04-30 20:37:29.000000000 +0100
++++ a/src/display/utils.cc 2017-04-30 20:56:18.000000000 +0100
+@@ -57,6 +57,7 @@
+ #include "core/download.h"
+ #include "core/manager.h"
+ #include "rpc/parse_commands.h"
++#include "ui/root.h"
+
+ #include "control.h"
+ #include "globals.h"
+@@ -323,20 +324,100 @@ print_client_version(char* first, char*
+ }
+
+ char*
++print_status_throttle_limit(char* first, char* last, bool up, const ui::ThrottleNameList& throttle_names) {
++ char throttle_str[40];
++ throttle_str[0] = 0;
++ char* firstc = throttle_str;
++ char* lastc = throttle_str + 40 - 1;
++
++ for (ui::ThrottleNameList::const_iterator itr = throttle_names.begin(), laste = throttle_names.end(); itr != laste; itr++) {
++
++ if (!(*itr).empty()) {
++ int64_t throttle_max = control->core()->retrieve_throttle_value(*itr, false, up);
++
++ if (throttle_max > 0)
++ firstc = print_buffer(firstc, lastc, "|%1.0f", (double)throttle_max / 1024.0);
++ }
++
++ }
++
++ // Add temp buffer (chop first char first) into main buffer if temp buffer isn't empty
++ if (throttle_str[0] != 0)
++ first = print_buffer(first, last, "(%s)", &throttle_str[1]);
++
++ return first;
++}
++
++char*
++print_status_throttle_rate(char* first, char* last, bool up, const ui::ThrottleNameList& throttle_names, const double& global_rate) {
++ double main_rate = global_rate;
++ char throttle_str[50];
++ throttle_str[0] = 0;
++ char* firstc = throttle_str;
++ char* lastc = throttle_str + 50 - 1;
++
++ for (ui::ThrottleNameList::const_iterator itr = throttle_names.begin(), laste = throttle_names.end(); itr != laste; itr++) {
++
++ if (!(*itr).empty() && (up ? torrent::up_throttle_global()->is_throttled() : torrent::down_throttle_global()->is_throttled())) {
++ int64_t throttle_rate_value = control->core()->retrieve_throttle_value(*itr, true, up);
++
++ if (throttle_rate_value > -1) {
++ double throttle_rate = (double)throttle_rate_value / 1024.0;
++ main_rate = main_rate - throttle_rate;
++
++ firstc = print_buffer(firstc, lastc, "|%3.1f", throttle_rate);
++ }
++ }
++
++ }
++
++ // Add temp buffer into main buffer if temp buffer isn't empty
++ if (throttle_str[0] != 0)
++ first = print_buffer(first, last, "(%3.1f%s)",
++ main_rate < 0.0 ? 0.0 : main_rate,
++ throttle_str);
++
++ return first;
++}
++
++char*
+ print_status_info(char* first, char* last) {
+- if (!torrent::up_throttle_global()->is_throttled())
++ ui::ThrottleNameList& throttle_up_names = control->ui()->get_status_throttle_up_names();
++ ui::ThrottleNameList& throttle_down_names = control->ui()->get_status_throttle_down_names();
++
++ if (!torrent::up_throttle_global()->is_throttled()) {
+ first = print_buffer(first, last, "[Throttle off");
+- else
++ } else {
+ first = print_buffer(first, last, "[Throttle %3i", torrent::up_throttle_global()->max_rate() / 1024);
+
+- if (!torrent::down_throttle_global()->is_throttled())
+- first = print_buffer(first, last, "/off KB]");
+- else
+- first = print_buffer(first, last, "/%3i KB]", torrent::down_throttle_global()->max_rate() / 1024);
+-
+- first = print_buffer(first, last, " [Rate %5.1f/%5.1f KB]",
+- (double)torrent::up_rate()->rate() / 1024.0,
+- (double)torrent::down_rate()->rate() / 1024.0);
++ if (!throttle_up_names.empty())
++ first = print_status_throttle_limit(first, last, true, throttle_up_names);
++ }
++
++ if (!torrent::down_throttle_global()->is_throttled()) {
++ first = print_buffer(first, last, " / off KB]");
++ } else {
++ first = print_buffer(first, last, " / %3i", torrent::down_throttle_global()->max_rate() / 1024);
++
++ if (!throttle_down_names.empty())
++ first = print_status_throttle_limit(first, last, false, throttle_down_names);
++
++ first = print_buffer(first, last, " KB]");
++ }
++
++ double global_uprate = (double)torrent::up_rate()->rate() / 1024.0;
++ first = print_buffer(first, last, " [Rate %5.1f", global_uprate);
++
++ if (!throttle_up_names.empty())
++ first = print_status_throttle_rate(first, last, true, throttle_up_names, global_uprate);
++
++ double global_downrate = (double)torrent::down_rate()->rate() / 1024.0;
++ first = print_buffer(first, last, " / %5.1f", global_downrate);
++
++ if (!throttle_down_names.empty())
++ first = print_status_throttle_rate(first, last, false, throttle_down_names, global_downrate);
++
++ first = print_buffer(first, last, " KB]");
+
+ first = print_buffer(first, last, " [Port: %i]", (unsigned int)torrent::connection_manager()->listen_port());
+
+--- a/src/display/utils.h 2016-10-23 05:33:00.000000000 +0100
++++ a/src/display/utils.h 2017-04-30 20:56:18.000000000 +0100
+@@ -80,6 +80,9 @@ char* print_client_version(char* f
+ char* print_entry_tags(char* first, char* last);
+ char* print_entry_file(char* first, char* last, const torrent::Entry& entry);
+
++char* print_status_throttle_limit(char* first, char* last, bool up, const std::vector<std::string>& throttle_names);
++char* print_status_throttle_rate(char* first, char* last, bool up, const std::vector<std::string>& throttle_names, const double& global_rate);
++
+ char* print_status_info(char* first, char* last);
+ char* print_status_extra(char* first, char* last);
+
+--- a/src/ui/root.h 2016-10-23 05:33:00.000000000 +0100
++++ a/src/ui/root.h 2019-07-26 20:01:18.000000000 +0100
+@@ -59,6 +59,8 @@ namespace ui {
+
+ class DownloadList;
+
++typedef std::vector<std::string> ThrottleNameList;
++
+ class Root {
+ public:
+ typedef display::WindowTitle WTitle;
+@@ -93,6 +95,12 @@ public:
+
+ const char* get_throttle_keys();
+
++ ThrottleNameList& get_status_throttle_up_names() { return m_throttle_up_names; }
++ ThrottleNameList& get_status_throttle_down_names() { return m_throttle_down_names; }
++
++ void set_status_throttle_up_names(const ThrottleNameList& throttle_list) { m_throttle_up_names = throttle_list; }
++ void set_status_throttle_down_names(const ThrottleNameList& throttle_list) { m_throttle_down_names = throttle_list; }
++
+ void enable_input(const std::string& title, input::TextInput* input, ui::DownloadList::Input type);
+ void disable_input();
+
+@@ -119,6 +127,9 @@ private:
+
+ input::Bindings m_bindings;
+
++ ThrottleNameList m_throttle_up_names;
++ ThrottleNameList m_throttle_down_names;
++
+ int m_input_history_length;
+ std::string m_input_history_last_input;
+ int m_input_history_pointer_get;
diff --git a/backport_rt_all_04-partially_done_and_choke_group_fix.patch b/backport_rt_all_04-partially_done_and_choke_group_fix.patch
new file mode 100644
index 000000000000..6ea36991acb2
--- /dev/null
+++ b/backport_rt_all_04-partially_done_and_choke_group_fix.patch
@@ -0,0 +1,653 @@
+--- a/src/command_download.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/command_download.cc 2017-04-30 21:40:38.853044300 +0100
+@@ -196,19 +196,6 @@ apply_d_connection_type(core::Download*
+ return torrent::Object();
+ }
+
+-torrent::Object
+-apply_d_choke_heuristics(core::Download* download, const std::string& name, bool is_down) {
+- torrent::Download::HeuristicType t =
+- (torrent::Download::HeuristicType)torrent::option_find_string(torrent::OPTION_CHOKE_HEURISTICS, name.c_str());
+-
+- if (is_down)
+- download->download()->set_download_choke_heuristic(t);
+- else
+- download->download()->set_upload_choke_heuristic(t);
+-
+- return torrent::Object();
+-}
+-
+ const char*
+ retrieve_d_priority_str(core::Download* download) {
+ switch (download->priority()) {
+@@ -687,6 +674,7 @@ initialize_command_download() {
+ CMD2_DL ("d.is_partially_done", CMD2_ON_DATA(is_partially_done));
+ CMD2_DL ("d.is_not_partially_done", CMD2_ON_DATA(is_not_partially_done));
+ CMD2_DL ("d.is_meta", CMD2_ON_INFO(is_meta_download));
++ CMD2_DL ("d.is_done", CMD2_ON_FL(is_done));
+
+ CMD2_DL_V ("d.resume", std::bind(&core::DownloadList::resume_default, control->core()->download_list(), std::placeholders::_1));
+ CMD2_DL_V ("d.pause", std::bind(&core::DownloadList::pause_default, control->core()->download_list(), std::placeholders::_1));
+@@ -760,16 +748,6 @@ initialize_command_download() {
+ CMD2_DL_VAR_STRING("d.connection_leech", "rtorrent", "connection_leech");
+ CMD2_DL_VAR_STRING("d.connection_seed", "rtorrent", "connection_seed");
+
+- CMD2_DL ("d.up.choke_heuristics", std::bind(&torrent::option_as_string, torrent::OPTION_CHOKE_HEURISTICS, CMD2_ON_DL(upload_choke_heuristic)));
+- CMD2_DL_STRING("d.up.choke_heuristics.set", std::bind(&apply_d_choke_heuristics, std::placeholders::_1, std::placeholders::_2, false));
+- CMD2_DL ("d.down.choke_heuristics", std::bind(&torrent::option_as_string, torrent::OPTION_CHOKE_HEURISTICS, CMD2_ON_DL(download_choke_heuristic)));
+- CMD2_DL_STRING("d.down.choke_heuristics.set", std::bind(&apply_d_choke_heuristics, std::placeholders::_1, std::placeholders::_2, true));
+-
+- CMD2_DL_VAR_STRING("d.up.choke_heuristics.leech", "rtorrent", "choke_heuristics.up.leech");
+- CMD2_DL_VAR_STRING("d.up.choke_heuristics.seed", "rtorrent", "choke_heuristics.up.seed");
+- CMD2_DL_VAR_STRING("d.down.choke_heuristics.leech", "rtorrent", "choke_heuristics.down.leech");
+- CMD2_DL_VAR_STRING("d.down.choke_heuristics.seed", "rtorrent", "choke_heuristics.down.seed");
+-
+ CMD2_DL ("d.hashing_failed", std::bind(&core::Download::is_hash_failed, std::placeholders::_1));
+ CMD2_DL_VALUE_V ("d.hashing_failed.set", std::bind(&core::Download::set_hash_failed, std::placeholders::_1, std::placeholders::_2));
+
+@@ -820,6 +797,7 @@ initialize_command_download() {
+ CMD2_DL ("d.free_diskspace", CMD2_ON_FL(free_diskspace));
+
+ CMD2_DL ("d.size_files", CMD2_ON_FL(size_files));
++ CMD2_DL ("d.selected_size_bytes", CMD2_ON_FL(selected_size_bytes));
+ CMD2_DL ("d.size_bytes", CMD2_ON_FL(size_bytes));
+ CMD2_DL ("d.size_chunks", CMD2_ON_FL(size_chunks));
+ CMD2_DL ("d.chunk_size", CMD2_ON_FL(chunk_size));
+@@ -853,18 +833,8 @@ initialize_command_download() {
+ CMD2_DL ("d.priority_str", std::bind(&retrieve_d_priority_str, std::placeholders::_1));
+ CMD2_DL_VALUE_V ("d.priority.set", std::bind(&core::Download::set_priority, std::placeholders::_1, std::placeholders::_2));
+
+- // CMD2_DL ("d.group", std::bind(&torrent::resource_manager_entry::group,
+- // std::bind(&torrent::ResourceManager::entry_at, torrent::resource_manager(),
+- // std::bind(&core::Download::main, std::placeholders::_1))));
+-
+- // CMD2_DL_V ("d.group.set", std::bind(&torrent::ResourceManager::set_group,
+- // torrent::resource_manager(),
+- // std::bind(&torrent::ResourceManager::find_throw, torrent::resource_manager(),
+- // std::bind(&core::Download::main, std::placeholders::_1)),
+- // CG_GROUP_INDEX()));
+-
+ CMD2_DL ("d.group", std::bind(&cg_d_group, std::placeholders::_1));
+- CMD2_DL ("d.group.name", std::bind(&cg_d_group, std::placeholders::_1));
++ CMD2_DL ("d.group.name", std::bind(&cg_d_group_name, std::placeholders::_1));
+ CMD2_DL_V ("d.group.set", std::bind(&cg_d_group_set, std::placeholders::_1, std::placeholders::_2));
+
+ CMD2_DL_LIST ("f.multicall", std::bind(&f_multicall, std::placeholders::_1, std::placeholders::_2));
+--- a/src/command_groups.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/command_groups.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -52,12 +52,6 @@
+ // For cg_d_group.
+ #include "core/download.h"
+
+-// A hack to allow testing of the new choke_group API without the
+-// working parts present.
+-#define USE_CHOKE_GROUP 0
+-
+-#if USE_CHOKE_GROUP
+-
+ int64_t
+ cg_get_index(const torrent::Object& raw_args) {
+ const torrent::Object& arg = (raw_args.is_list() && !raw_args.as_list().empty()) ? raw_args.as_list().front() : raw_args;
+@@ -122,104 +116,6 @@ apply_cg_insert(const std::string& arg)
+ return torrent::Object();
+ }
+
+-//
+-// The hacked version:
+-//
+-#else
+-
+-std::vector<torrent::choke_group*> cg_list_hack;
+-
+-int64_t
+-cg_get_index(const torrent::Object& raw_args) {
+- const torrent::Object& arg = (raw_args.is_list() && !raw_args.as_list().empty()) ? raw_args.as_list().front() : raw_args;
+-
+- int64_t index = 0;
+-
+- if (arg.is_string()) {
+- if (!rpc::parse_whole_value_nothrow(arg.as_string().c_str(), &index)) {
+- std::vector<torrent::choke_group*>::iterator itr = std::find_if(cg_list_hack.begin(), cg_list_hack.end(),
+- rak::equal(arg.as_string(), std::mem_fun(&torrent::choke_group::name)));
+-
+- if (itr == cg_list_hack.end())
+- throw torrent::input_error("Choke group not found.");
+-
+- return std::distance(cg_list_hack.begin(), itr);
+- }
+-
+- } else {
+- index = arg.as_value();
+- }
+-
+- if (index < 0)
+- index = (int64_t)cg_list_hack.size() + index;
+-
+- if ((size_t)index >= cg_list_hack.size())
+- throw torrent::input_error("Choke group not found.");
+-
+- return index;
+-}
+-
+-torrent::choke_group*
+-cg_get_group(const torrent::Object& raw_args) {
+- int64_t index = cg_get_index(raw_args);
+-
+- if ((size_t)index >= cg_list_hack.size())
+- throw torrent::input_error("Choke group not found.");
+-
+- return cg_list_hack.at(index);
+-}
+-
+-int64_t cg_d_group(core::Download* download) { return download->group(); }
+-void cg_d_group_set(core::Download* download, const torrent::Object& arg) { download->set_group(cg_get_index(arg)); }
+-
+-torrent::Object
+-apply_cg_list() {
+- torrent::Object::list_type result;
+-
+- for (std::vector<torrent::choke_group*>::iterator itr = cg_list_hack.begin(), last = cg_list_hack.end(); itr != last; itr++)
+- result.push_back((*itr)->name());
+-
+- return torrent::Object::from_list(result);
+-}
+-
+-torrent::Object
+-apply_cg_insert(const std::string& arg) {
+- int64_t dummy;
+-
+- if (rpc::parse_whole_value_nothrow(arg.c_str(), &dummy))
+- throw torrent::input_error("Cannot use a value string as choke group name.");
+-
+- if (arg.empty() ||
+- std::find_if(cg_list_hack.begin(), cg_list_hack.end(),
+- rak::equal(arg, std::mem_fun(&torrent::choke_group::name))) != cg_list_hack.end())
+- throw torrent::input_error("Duplicate name for choke group.");
+-
+- cg_list_hack.push_back(new torrent::choke_group());
+- cg_list_hack.back()->set_name(arg);
+-
+- cg_list_hack.back()->up_queue()->set_heuristics(torrent::choke_queue::HEURISTICS_UPLOAD_LEECH);
+- cg_list_hack.back()->down_queue()->set_heuristics(torrent::choke_queue::HEURISTICS_DOWNLOAD_LEECH);
+-
+- return torrent::Object();
+-}
+-
+-torrent::Object
+-apply_cg_index_of(const std::string& arg) {
+- std::vector<torrent::choke_group*>::iterator itr =
+- std::find_if(cg_list_hack.begin(), cg_list_hack.end(), rak::equal(arg, std::mem_fun(&torrent::choke_group::name)));
+-
+- if (itr == cg_list_hack.end())
+- throw torrent::input_error("Choke group not found.");
+-
+- return std::distance(cg_list_hack.begin(), itr);
+-}
+-
+-//
+-// End of choke group hack.
+-//
+-#endif
+-
+-
+ torrent::Object
+ apply_cg_max_set(const torrent::Object::list_type& args, bool is_up) {
+ if (args.size() != 2)
+@@ -338,15 +234,8 @@ initialize_command_groups() {
+ CMD2_ANY ("choke_group.list", std::bind(&apply_cg_list));
+ CMD2_ANY_STRING ("choke_group.insert", std::bind(&apply_cg_insert, std::placeholders::_2));
+
+-#if USE_CHOKE_GROUP
+ CMD2_ANY ("choke_group.size", std::bind(&torrent::ResourceManager::group_size, torrent::resource_manager()));
+ CMD2_ANY_STRING ("choke_group.index_of", std::bind(&torrent::ResourceManager::group_index_of, torrent::resource_manager(), std::placeholders::_2));
+-#else
+- apply_cg_insert("default");
+-
+- CMD2_ANY ("choke_group.size", std::bind(&std::vector<torrent::choke_group*>::size, cg_list_hack));
+- CMD2_ANY_STRING ("choke_group.index_of", std::bind(&apply_cg_index_of, std::placeholders::_2));
+-#endif
+
+ // Commands specific for a group. Supports as the first argument the
+ // name, the index or a negative index.
+--- a/src/command_local.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/command_local.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -73,7 +73,7 @@ apply_pieces_stats_total_size() {
+
+ for (core::DownloadList::iterator itr = d_list->begin(), last = d_list->end(); itr != last; itr++)
+ if ((*itr)->is_active())
+- size += (*itr)->file_list()->size_bytes();
++ size += (*itr)->file_list()->selected_size_bytes();
+
+ return size;
+ }
+--- a/src/command_network.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/command_network.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -254,11 +254,6 @@ initialize_command_network() {
+ CMD2_VAR_STRING ("protocol.connection.leech", "leech");
+ CMD2_VAR_STRING ("protocol.connection.seed", "seed");
+
+- CMD2_VAR_STRING ("protocol.choke_heuristics.up.leech", "upload_leech");
+- CMD2_VAR_STRING ("protocol.choke_heuristics.up.seed", "upload_leech");
+- CMD2_VAR_STRING ("protocol.choke_heuristics.down.leech", "download_leech");
+- CMD2_VAR_STRING ("protocol.choke_heuristics.down.seed", "download_leech");
+-
+ CMD2_ANY ("network.http.cacert", std::bind(&core::CurlStack::http_cacert, httpStack));
+ CMD2_ANY_STRING_V("network.http.cacert.set", std::bind(&core::CurlStack::set_http_cacert, httpStack, std::placeholders::_2));
+ CMD2_ANY ("network.http.capath", std::bind(&core::CurlStack::http_capath, httpStack));
+--- a/src/command_ui.cc 2017-04-30 21:07:03.000000000 +0100
++++ a/src/command_ui.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -411,6 +411,17 @@ apply_to_throttle(const torrent::Object&
+ return std::string(buffer);
+ }
+
++torrent::Object
++apply_to_group(const torrent::Object& rawArgs) {
++ int64_t arg = rawArgs.as_value();
++ if (arg < 0)
++ return "--";
++
++ char buffer[16];
++ snprintf(buffer, 16, "%2d", (int)(arg));
++ return std::string(buffer);
++}
++
+ // A series of if/else statements. Every even arguments are
+ // conditionals and odd arguments are branches to be executed, except
+ // the last one which is always a branch.
+@@ -778,6 +789,7 @@ initialize_command_ui() {
+ CMD2_ANY_VALUE("convert.mb", std::bind(&apply_to_mb, std::placeholders::_2));
+ CMD2_ANY_VALUE("convert.xb", std::bind(&apply_to_xb, std::placeholders::_2));
+ CMD2_ANY_VALUE("convert.throttle", std::bind(&apply_to_throttle, std::placeholders::_2));
++ CMD2_ANY_VALUE("convert.group", std::bind(&apply_to_group, std::placeholders::_2));
+
+ CMD2_ANY_LIST("math.add", std::bind(&apply_math_basic, std::plus<int64_t>(), std::placeholders::_2));
+ CMD2_ANY_LIST("math.sub", std::bind(&apply_math_basic, std::minus<int64_t>(), std::placeholders::_2));
+--- a/src/core/download.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/core/download.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -94,7 +94,7 @@ Download::set_priority(uint32_t p) {
+ p %= 4;
+
+ // Seeding torrents get half the priority of unfinished torrents.
+- if (!is_done())
++ if (!is_partially_done())
+ torrent::download_set_priority(m_download, p * p * 2);
+ else
+ torrent::download_set_priority(m_download, p * p);
+--- a/src/core/download_factory.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/core/download_factory.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -421,6 +421,12 @@ DownloadFactory::initialize_rtorrent(Dow
+ if (rtorrent->has_key_value("total_downloaded"))
+ download->info()->mutable_down_rate()->set_total(rtorrent->get_key_value("total_downloaded"));
+
++ if (rtorrent->has_key_value("total_skipped"))
++ download->info()->mutable_skip_rate()->set_total(rtorrent->get_key_value("total_skipped"));
++
++ if (rtorrent->has_key_value("size_selected"))
++ download->file_list()->set_selected_size_bytes(rtorrent->get_key_value("size_selected"));
++
+ if (rtorrent->has_key_value("chunks_done") && rtorrent->has_key_value("chunks_wanted"))
+ download->download()->set_chunks_done(rtorrent->get_key_value("chunks_done"), rtorrent->get_key_value("chunks_wanted"));
+
+@@ -433,11 +436,6 @@ DownloadFactory::initialize_rtorrent(Dow
+
+ rtorrent->insert_preserve_type("connection_leech", m_variables["connection_leech"]);
+ rtorrent->insert_preserve_type("connection_seed", m_variables["connection_seed"]);
+-
+- rtorrent->insert_preserve_copy("choke_heuristics.up.leech", std::string());
+- rtorrent->insert_preserve_copy("choke_heuristics.up.seed", std::string());
+- rtorrent->insert_preserve_copy("choke_heuristics.down.leech", std::string());
+- rtorrent->insert_preserve_copy("choke_heuristics.down.seed", std::string());
+ }
+
+ }
+--- a/src/core/download.h 2016-10-23 05:33:00.000000000 +0100
++++ a/src/core/download.h 2017-04-30 21:35:27.000000000 +0100
+@@ -79,8 +79,9 @@ public:
+ bool is_open() const { return m_download.info()->is_open(); }
+ bool is_active() const { return m_download.info()->is_active(); }
+ bool is_done() const { return m_download.file_list()->is_done(); }
+- bool is_downloading() const { return is_active() && !is_done(); }
+- bool is_seeding() const { return is_active() && is_done(); }
++ bool is_partially_done() const { return m_download.data()->is_partially_done(); }
++ bool is_downloading() const { return is_active() && !is_partially_done(); }
++ bool is_seeding() const { return is_active() && is_partially_done(); }
+
+ // FIXME: Fixed a bug in libtorrent that caused is_hash_checked to
+ // return true when the torrent is closed. Remove this redundant
+@@ -129,7 +130,6 @@ public:
+
+ float distributed_copies() const;
+
+- // HACK: Choke group setting.
+ unsigned int group() const { return m_group; }
+ void set_group(unsigned int g) { m_group = g; }
+
+--- a/src/core/download_list.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/core/download_list.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -182,6 +182,7 @@ DownloadList::insert(Download* download)
+ try {
+ (*itr)->data()->slot_initial_hash() = std::bind(&DownloadList::hash_done, this, download);
+ (*itr)->data()->slot_download_done() = std::bind(&DownloadList::received_finished, this, download);
++ (*itr)->data()->slot_partially_restarted() = std::bind(&DownloadList::received_partially_restarted, this, download);
+
+ // This needs to be separated into two different calls to ensure
+ // the download remains in the view.
+@@ -371,31 +372,18 @@ DownloadList::resume(Download* download,
+ rpc::call_command("d.state_changed.set", cachedTime.seconds(), rpc::make_target(download));
+ rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)) + 1, rpc::make_target(download));
+
+- if (download->is_done()) {
+- torrent::Object conn_current = rpc::call_command("d.connection_seed", torrent::Object(), rpc::make_target(download));
+- torrent::Object choke_up = rpc::call_command("d.up.choke_heuristics.seed", torrent::Object(), rpc::make_target(download));
+- torrent::Object choke_down = rpc::call_command("d.down.choke_heuristics.seed", torrent::Object(), rpc::make_target(download));
++ if (download->is_partially_done()) {
++ rpc::call_command("d.group.set", "default_seed", rpc::make_target(download));
+
++ torrent::Object conn_current = rpc::call_command("d.connection_seed", torrent::Object(), rpc::make_target(download));
+ if (conn_current.is_string_empty()) conn_current = rpc::call_command("protocol.connection.seed", torrent::Object(), rpc::make_target(download));
+- if (choke_up.is_string_empty()) choke_up = rpc::call_command("protocol.choke_heuristics.up.seed", torrent::Object(), rpc::make_target(download));
+- if (choke_down.is_string_empty()) choke_down = rpc::call_command("protocol.choke_heuristics.down.seed", torrent::Object(), rpc::make_target(download));
+-
+- rpc::call_command("d.connection_current.set", conn_current, rpc::make_target(download));
+- rpc::call_command("d.up.choke_heuristics.set", choke_up, rpc::make_target(download));
+- rpc::call_command("d.down.choke_heuristics.set", choke_down, rpc::make_target(download));
+-
++ rpc::call_command("d.connection_current.set", conn_current, rpc::make_target(download));
+ } else {
+- torrent::Object conn_current = rpc::call_command("d.connection_leech", torrent::Object(), rpc::make_target(download));
+- torrent::Object choke_up = rpc::call_command("d.up.choke_heuristics.leech", torrent::Object(), rpc::make_target(download));
+- torrent::Object choke_down = rpc::call_command("d.down.choke_heuristics.leech", torrent::Object(), rpc::make_target(download));
++ rpc::call_command("d.group.set", "default_leech", rpc::make_target(download));
+
++ torrent::Object conn_current = rpc::call_command("d.connection_leech", torrent::Object(), rpc::make_target(download));
+ if (conn_current.is_string_empty()) conn_current = rpc::call_command("protocol.connection.leech", torrent::Object(), rpc::make_target(download));
+- if (choke_up.is_string_empty()) choke_up = rpc::call_command("protocol.choke_heuristics.up.leech", torrent::Object(), rpc::make_target(download));
+- if (choke_down.is_string_empty()) choke_down = rpc::call_command("protocol.choke_heuristics.down.leech", torrent::Object(), rpc::make_target(download));
+-
+- rpc::call_command("d.connection_current.set", conn_current, rpc::make_target(download));
+- rpc::call_command("d.up.choke_heuristics.set", choke_up, rpc::make_target(download));
+- rpc::call_command("d.down.choke_heuristics.set", choke_down, rpc::make_target(download));
++ rpc::call_command("d.connection_current.set", conn_current, rpc::make_target(download));
+
+ // For the moment, clear the resume data so we force hash-check
+ // on non-complete downloads after a crash. This shouldn't be
+@@ -528,14 +516,14 @@ DownloadList::hash_done(Download* downlo
+
+ // If the download was previously completed but the files were
+ // f.ex deleted, then we clear the state and complete.
+- if (rpc::call_command_value("d.complete", rpc::make_target(download)) && !download->is_done()) {
++ if (rpc::call_command_value("d.complete", rpc::make_target(download)) && !download->is_partially_done()) {
+ rpc::call_command("d.state.set", (int64_t)0, rpc::make_target(download));
+ download->set_message("Download registered as completed, but hash check returned unfinished chunks.");
+ }
+
+ // Save resume data so we update time-stamps and priorities if
+ // they were invalid/changed while loading/hashing.
+- rpc::call_command("d.complete.set", (int64_t)download->is_done(), rpc::make_target(download));
++ rpc::call_command("d.complete.set", (int64_t)download->is_partially_done(), rpc::make_target(download));
+ torrent::resume_save_progress(*download->download(), download->download()->bencode()->get_key("libtorrent_resume"));
+
+ if (rpc::call_command_value("d.state", rpc::make_target(download)) == 1)
+@@ -545,7 +533,7 @@ DownloadList::hash_done(Download* downlo
+
+ case Download::variable_hashing_last:
+
+- if (download->is_done()) {
++ if (download->is_partially_done()) {
+ confirm_finished(download);
+ } else {
+ download->set_message("Hash check on download completion found bad chunks, consider using \"safe_sync\".");
+@@ -623,19 +611,14 @@ DownloadList::confirm_finished(Download*
+
+ rpc::call_command("d.complete.set", (int64_t)1, rpc::make_target(download));
+
+- // Clean up these settings:
+- torrent::Object conn_current = rpc::call_command("d.connection_seed", torrent::Object(), rpc::make_target(download));
+- torrent::Object choke_up = rpc::call_command("d.up.choke_heuristics.seed", torrent::Object(), rpc::make_target(download));
+- torrent::Object choke_down = rpc::call_command("d.down.choke_heuristics.seed", torrent::Object(), rpc::make_target(download));
++ // Set seeding mode
++ rpc::call_command("d.group.set", "default_seed", rpc::make_target(download));
+
++ torrent::Object conn_current = rpc::call_command("d.connection_seed", torrent::Object(), rpc::make_target(download));
+ if (conn_current.is_string_empty()) conn_current = rpc::call_command("protocol.connection.seed", torrent::Object(), rpc::make_target(download));
+- if (choke_up.is_string_empty()) choke_up = rpc::call_command("protocol.choke_heuristics.up.seed", torrent::Object(), rpc::make_target(download));
+- if (choke_down.is_string_empty()) choke_down = rpc::call_command("protocol.choke_heuristics.down.seed", torrent::Object(), rpc::make_target(download));
+-
+ rpc::call_command("d.connection_current.set", conn_current, rpc::make_target(download));
+- rpc::call_command("d.up.choke_heuristics.set", choke_up, rpc::make_target(download));
+- rpc::call_command("d.down.choke_heuristics.set", choke_down, rpc::make_target(download));
+
++ // Update the priority to ensure it has the correct seeding/unfinished modifiers.
+ download->set_priority(download->priority());
+
+ if (rpc::call_command_value("d.peers_min", rpc::make_target(download)) == rpc::call_command_value("throttle.min_peers.normal") &&
+@@ -656,8 +639,9 @@ DownloadList::confirm_finished(Download*
+ }
+
+ // Send the completed request before resuming so we don't reset the
+- // up/downloaded baseline.
+- download->download()->send_completed();
++ // up/downloaded baseline if download is completely done.
++ if (download->is_done())
++ download->download()->send_completed();
+
+ // Save the hash in case the finished event erases it.
+ torrent::HashString infohash = download->info()->hash();
+@@ -688,6 +672,40 @@ DownloadList::confirm_finished(Download*
+ }
+
+ void
++DownloadList::received_partially_restarted(Download* download) {
++ check_contains(download);
++
++ lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Received partially restarted.");
++
++ rpc::call_command("d.complete.set", (int64_t)0, rpc::make_target(download));
++
++ // Set leeching mode.
++ rpc::call_command("d.group.set", "default_leech", rpc::make_target(download));
++
++ torrent::Object conn_current = rpc::call_command("d.connection_leech", torrent::Object(), rpc::make_target(download));
++ if (conn_current.is_string_empty()) conn_current = rpc::call_command("protocol.connection.leech", torrent::Object(), rpc::make_target(download));
++ rpc::call_command("d.connection_current.set", conn_current, rpc::make_target(download));
++
++ // Update the priority to ensure it has the correct seeding/unfinished modifiers.
++ download->set_priority(download->priority());
++
++ // Set these also back to the original leeching values.
++ if (rpc::call_command_value("throttle.min_peers.seed") >= 0 &&
++ rpc::call_command_value("d.peers_min", rpc::make_target(download)) == rpc::call_command_value("throttle.min_peers.seed"))
++ rpc::call_command("d.peers_min.set", rpc::call_command("throttle.min_peers.normal"), rpc::make_target(download));
++
++ if (rpc::call_command_value("throttle.max_peers.seed") >= 0 &&
++ rpc::call_command_value("d.peers_max", rpc::make_target(download)) == rpc::call_command_value("throttle.max_peers.seed"))
++ rpc::call_command("d.peers_max.set", rpc::call_command("throttle.max_peers.normal"), rpc::make_target(download));
++
++ DL_TRIGGER_EVENT(download, "event.download.partially_restarted");
++
++ if (!download->is_active() && rpc::call_command_value("session.on_completion") != 0) {
++ control->core()->download_store()->save_resume(download);
++ }
++}
++
++void
+ DownloadList::process_meta_download(Download* download) {
+ lt_log_print_info(torrent::LOG_TORRENT_INFO, download->info(), "download_list", "Processing meta download.");
+
+--- a/src/core/download_list.h 2016-10-23 05:33:00.000000000 +0100
++++ a/src/core/download_list.h 2017-04-30 21:35:27.000000000 +0100
+@@ -123,6 +123,7 @@ public:
+ D_SLOTS_HASH_REMOVED,
+ D_SLOTS_HASH_DONE,
+ D_SLOTS_FINISHED,
++ D_SLOTS_PARTIALLY_RESTARTED,
+
+ SLOTS_MAX_SIZE
+ };
+@@ -139,6 +140,7 @@ public:
+ case D_SLOTS_HASH_REMOVED: return "event.download.hash_removed";
+ case D_SLOTS_HASH_DONE: return "event.download.hash_done";
+ case D_SLOTS_FINISHED: return "event.download.finished";
++ case D_SLOTS_PARTIALLY_RESTARTED: return "event.download.partially_restarted";
+ default: return "BORK";
+ }
+ }
+@@ -162,6 +164,7 @@ private:
+
+ void received_finished(Download* d);
+ void confirm_finished(Download* d);
++ void received_partially_restarted(Download* d);
+
+ void process_meta_download(Download* d);
+ };
+--- a/src/core/download_store.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/core/download_store.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -141,6 +141,8 @@ DownloadStore::save(Download* d, int fla
+ rtorrent_base->insert_key("chunks_wanted", d->download()->data()->wanted_chunks());
+ rtorrent_base->insert_key("total_uploaded", d->info()->up_rate()->total());
+ rtorrent_base->insert_key("total_downloaded", d->info()->down_rate()->total());
++ rtorrent_base->insert_key("total_skipped", d->info()->skip_rate()->total());
++ rtorrent_base->insert_key("size_selected", d->download()->file_list()->selected_size_bytes());
+
+ // Don't save for completed torrents when we've cleared the uncertain_pieces.
+ torrent::resume_save_progress(*d->download(), *resume_base);
+--- a/src/display/utils.cc 2017-04-30 21:07:03.000000000 +0100
++++ a/src/display/utils.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -161,8 +161,12 @@ print_download_info_full(char* first, ch
+ first = print_buffer(first, last, " ");
+ first = print_download_percentage_done(first, last, d);
+
+- first = print_buffer(first, last, " ");
+- first = print_download_time_left(first, last, d);
++ if (!d->is_partially_done()) {
++ first = print_buffer(first, last, " ");
++ first = print_download_time_left(first, last, d);
++ } else {
++ first = print_buffer(first, last, " done ");
++ }
+ } else {
+ first = print_buffer(first, last, " ");
+ }
+@@ -261,7 +265,7 @@ print_download_info_compact(char* first,
+ first = print_buffer(first, last, "| %7.1f MB ", (double)d->info()->up_rate()->total() / (1 << 20));
+ first = print_buffer(first, last, "| ");
+
+- if (d->download()->info()->is_active() && !d->is_done())
++ if (d->download()->info()->is_active() && !d->is_partially_done())
+ first = print_download_time_left(first, last, d);
+ else
+ first = print_buffer(first, last, " ");
+@@ -291,7 +295,7 @@ print_download_time_left(char* first, ch
+ if (rate < 512)
+ return print_buffer(first, last, "--d --:--");
+
+- time_t remaining = (d->download()->file_list()->size_bytes() - d->download()->bytes_done()) / (rate & ~(uint32_t)(512 - 1));
++ time_t remaining = (d->download()->file_list()->selected_size_bytes() - d->download()->bytes_done()) / (rate & ~(uint32_t)(512 - 1));
+
+ return print_ddhhmm(first, last, remaining);
+ }
+--- a/src/display/window_download_chunks_seen.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/display/window_download_chunks_seen.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -67,7 +67,7 @@ WindowDownloadChunksSeen::redraw() {
+ return;
+
+ m_canvas->print(2, 0, "Chunks seen: [C/A/D %i/%i/%.2f]",
+- (int)m_download->download()->peers_complete() + m_download->download()->file_list()->is_done(),
++ (int)m_download->download()->peers_complete() + m_download->is_partially_done(),
+ (int)m_download->download()->peers_accounted(),
+ std::floor(m_download->distributed_copies() * 100.0f) / 100.0f);
+
+@@ -78,7 +78,7 @@ WindowDownloadChunksSeen::redraw() {
+ return;
+ }
+
+- if (!m_download->is_done()) {
++ if (!m_download->is_partially_done()) {
+ m_canvas->print(36, 0, "X downloaded missing queued downloading");
+ m_canvas->print_char(50, 0, 'X' | A_BOLD);
+ m_canvas->print_char(61, 0, 'X' | A_BOLD | A_UNDERLINE);
+--- a/src/main.cc 2017-04-30 21:07:03.000000000 +0100
++++ a/src/main.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -262,6 +262,7 @@ main(int argc, char** argv) {
+ "method.insert = event.download.paused,multi|rlookup|static\n"
+
+ "method.insert = event.download.finished,multi|rlookup|static\n"
++ "method.insert = event.download.partially_restarted,multi|rlookup|static\n"
+ "method.insert = event.download.hash_done,multi|rlookup|static\n"
+ "method.insert = event.download.hash_failed,multi|rlookup|static\n"
+ "method.insert = event.download.hash_final_failed,multi|rlookup|static\n"
+@@ -278,7 +279,7 @@ main(int argc, char** argv) {
+ "method.set_key = event.download.erased, ~_delete_tied, d.delete_tied=\n"
+
+ "method.set_key = event.download.resumed, !_timestamp, ((d.timestamp.started.set_if_z, ((system.time)) ))\n"
+- "method.set_key = event.download.finished, !_timestamp, ((d.timestamp.finished.set_if_z, ((system.time)) ))\n"
++ "method.set_key = event.download.finished, !_timestamp, ((d.timestamp.finished.set, ((system.time)) ))\n"
+ "method.set_key = event.download.hash_done, !_timestamp, {(branch,((d.complete)),((d.timestamp.finished.set_if_z,(system.time))))}\n"
+
+ "method.insert.c_simple = group.insert_persistent_view,"
+@@ -287,6 +288,9 @@ main(int argc, char** argv) {
+ "file.prioritize_toc.first.set = {*.avi,*.mp4,*.mkv,*.gz}\n"
+ "file.prioritize_toc.last.set = {*.zip}\n"
+
++ "choke_group.insert = default_seed\n"
++ "choke_group.up.heuristics.set = default_seed,upload_seed\n"
++
+ // Allow setting 'group2.view' as constant, so that we can't
+ // modify the value. And look into the possibility of making
+ // 'const' use non-heap memory, as we know they can't be
+@@ -322,12 +326,13 @@ main(int argc, char** argv) {
+
+ "view.add = complete\n"
+ "view.filter = complete,((d.complete))\n"
+- "view.filter_on = complete,event.download.hash_done,event.download.hash_failed,event.download.hash_final_failed,event.download.finished\n"
++ "view.filter_on = complete,event.download.hash_done,event.download.hash_failed,event.download.hash_final_failed,"
++ "event.download.finished,event.download.partially_restarted\n"
+
+ "view.add = incomplete\n"
+ "view.filter = incomplete,((not,((d.complete))))\n"
+ "view.filter_on = incomplete,event.download.hash_done,event.download.hash_failed,"
+- "event.download.hash_final_failed,event.download.finished\n"
++ "event.download.hash_final_failed,event.download.finished,event.download.partially_restarted\n"
+
+ // The hashing view does not include stopped torrents.
+ "view.add = hashing\n"
+@@ -337,11 +342,11 @@ main(int argc, char** argv) {
+
+ "view.add = seeding\n"
+ "view.filter = seeding,((and,((d.state)),((d.complete))))\n"
+- "view.filter_on = seeding,event.download.resumed,event.download.paused,event.download.finished\n"
++ "view.filter_on = seeding,event.download.resumed,event.download.paused,event.download.finished,event.download.partially_restarted\n"
+
+ "view.add = leeching\n"
+ "view.filter = leeching,((and,((d.state)),((not,((d.complete))))))\n"
+- "view.filter_on = leeching,event.download.resumed,event.download.paused,event.download.finished\n"
++ "view.filter_on = leeching,event.download.resumed,event.download.paused,event.download.finished,event.download.partially_restarted\n"
+
+ "schedule2 = view.main,10,10,((view.sort,main,20))\n"
+ "schedule2 = view.name,10,10,((view.sort,name,20))\n"
+--- a/src/ui/download.cc 2017-04-30 21:07:03.000000000 +0100
++++ a/src/ui/download.cc 2017-04-30 21:35:27.000000000 +0100
+@@ -160,6 +160,7 @@ Download::create_info() {
+ element->push_column("File stats:", te_command("cat=$if=$d.is_multi_file=\\,multi\\,single,\" \",$d.size_files=,\" files\""));
+
+ element->push_back("");
++ element->push_column("Size:", te_command("cat=(convert.xb,(d.bytes_done)),\" / \",(convert.xb,(d.selected_size_bytes)),\" / \",(convert.xb,(d.size_bytes))"));
+ element->push_column("Chunks:", te_command("cat=(d.completed_chunks),\" / \",(d.size_chunks),\" * \",(d.chunk_size),\" (\",(d.wanted_chunks),\")\""));
+ element->push_column("Priority:", te_command("d.priority="));
+ element->push_column("Peer exchange:", te_command("cat=$if=$d.peer_exchange=\\,enabled\\,disabled,\\ ,"
+@@ -176,7 +177,14 @@ Download::create_info() {
+
+ element->push_back("");
+ element->push_column("Connection type:", te_command("cat=(d.connection_current),\" \",(if,(d.accepting_seeders),"",\"no_seeders\")"));
+- element->push_column("Choke heuristic:", te_command("cat=(d.up.choke_heuristics),\", \",(d.down.choke_heuristics),\", \",(d.group)"));
++ element->push_column("Choke group:", te_command("cat=(d.group.name),\" [\",(choke_group.up.heuristics,(d.group)),\", \","
++ "(choke_group.down.heuristics,(d.group)),\", \",(choke_group.tracker.mode,(d.group)),\"] [Max \","
++ "(convert.group,(choke_group.up.max,(d.group))),\"/\",(convert.group,(choke_group.down.max,(d.group))),\"]\""));
++ element->push_column("Choke group stat:", te_command("cat=\"[Size \",(choke_group.general.size,(d.group)),\"] [Unchoked \",(choke_group.up.unchoked,(d.group)),"
++ "\"/\",(choke_group.down.unchoked,(d.group)),\"] [Queued \",(choke_group.up.queued,(d.group)),"
++ "\"/\",(choke_group.down.queued,(d.group)),\"] [Total \",(choke_group.up.total,(d.group)),"
++ "\"/\",(choke_group.down.total,(d.group)),\"] [Rate \",(convert.kb,(choke_group.up.rate,(d.group))),"
++ "\"/\",(convert.kb,(choke_group.down.rate,(d.group))),\" KB]\""));
+ element->push_column("Safe sync:", te_command("if=$pieces.sync.always_safe=,yes,no"));
+ element->push_column("Send buffer:", te_command("cat=$convert.kb=$network.send_buffer.size=,\" KB\""));
+ element->push_column("Receive buffer:", te_command("cat=$convert.kb=$network.receive_buffer.size=,\" KB\""));
diff --git a/backport_rt_all_05-honor_system_file_allocate_fix.patch b/backport_rt_all_05-honor_system_file_allocate_fix.patch
new file mode 100644
index 000000000000..2fe4ec0ae273
--- /dev/null
+++ b/backport_rt_all_05-honor_system_file_allocate_fix.patch
@@ -0,0 +1,202 @@
+--- a/src/command_download.cc 2017-04-30 21:40:38.853044300 +0100
++++ a/src/command_download.cc 2017-04-30 22:48:43.094251399 +0100
+@@ -64,6 +64,7 @@
+ #include "core/download_store.h"
+ #include "core/manager.h"
+ #include "rpc/parse.h"
++#include "rpc/parse_commands.h"
+
+ #include "globals.h"
+ #include "control.h"
+@@ -571,6 +572,13 @@ d_list_remove(core::Download* download,
+ return torrent::Object();
+ }
+
++torrent::Object
++apply_d_update_priorities(core::Download* download) {
++ int fallocate = rpc::call_command_value("system.file.allocate") ? torrent::Download::open_enable_fallocate : 0;
++
++ download->update_priorities(fallocate);
++}
++
+ #define CMD2_ON_INFO(func) std::bind(&torrent::DownloadInfo::func, std::bind(&core::Download::info, std::placeholders::_1))
+ #define CMD2_ON_DATA(func) std::bind(&torrent::download_data::func, std::bind(&core::Download::data, std::placeholders::_1))
+ #define CMD2_ON_DL(func) std::bind(&torrent::Download::func, std::bind(&core::Download::download, std::placeholders::_1))
+@@ -687,7 +695,7 @@ initialize_command_download() {
+ CMD2_DL ("d.save_resume", std::bind(&core::DownloadStore::save_resume, control->core()->download_store(), std::placeholders::_1));
+ CMD2_DL ("d.save_full_session", std::bind(&core::DownloadStore::save_full, control->core()->download_store(), std::placeholders::_1));
+
+- CMD2_DL_V ("d.update_priorities", CMD2_ON_DL(update_priorities));
++ CMD2_DL_V ("d.update_priorities", std::bind(&apply_d_update_priorities, std::placeholders::_1));
+
+ CMD2_DL_STRING_V("add_peer", std::bind(&apply_d_add_peer, std::placeholders::_1, std::placeholders::_2));
+
+@@ -792,14 +799,16 @@ initialize_command_download() {
+ CMD2_DL ("d.throttle_name", std::bind(&download_get_variable, std::placeholders::_1, "rtorrent", "throttle_name"));
+ CMD2_DL_STRING_V("d.throttle_name.set", std::bind(&core::Download::set_throttle_name, std::placeholders::_1, std::placeholders::_2));
+
+- CMD2_DL ("d.bytes_done", CMD2_ON_DL(bytes_done));
+- CMD2_DL ("d.ratio", std::bind(&retrieve_d_ratio, std::placeholders::_1));
+- CMD2_DL ("d.chunks_hashed", CMD2_ON_DL(chunks_hashed));
+- CMD2_DL ("d.free_diskspace", CMD2_ON_FL(free_diskspace));
++ CMD2_DL ("d.bytes_done", CMD2_ON_DL(bytes_done));
++ CMD2_DL ("d.ratio", std::bind(&retrieve_d_ratio, std::placeholders::_1));
++ CMD2_DL ("d.chunks_hashed", CMD2_ON_DL(chunks_hashed));
++ CMD2_DL ("d.free_diskspace", CMD2_ON_FL(free_diskspace));
++ CMD2_DL ("d.is_enough_diskspace", CMD2_ON_FL(is_enough_diskspace));
+
+ CMD2_DL ("d.size_files", CMD2_ON_FL(size_files));
+ CMD2_DL ("d.selected_size_bytes", CMD2_ON_FL(selected_size_bytes));
+ CMD2_DL ("d.size_bytes", CMD2_ON_FL(size_bytes));
++ CMD2_DL ("d.allocatable_size_bytes", CMD2_ON_FL(allocatable_size_bytes));
+ CMD2_DL ("d.size_chunks", CMD2_ON_FL(size_chunks));
+ CMD2_DL ("d.chunk_size", CMD2_ON_FL(chunk_size));
+ CMD2_DL ("d.size_pex", CMD2_ON_DL(size_pex));
+--- a/src/command_file.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/command_file.cc 2017-04-30 22:42:23.000000000 +0100
+@@ -105,11 +105,15 @@ initialize_command_file() {
+
+ CMD2_FILE("f.is_create_queued", std::bind(&torrent::File::is_create_queued, std::placeholders::_1));
+ CMD2_FILE("f.is_resize_queued", std::bind(&torrent::File::is_resize_queued, std::placeholders::_1));
++ CMD2_FILE("f.is_fallocatable", std::bind(&torrent::File::is_fallocatable, std::placeholders::_1));
++ CMD2_FILE("f.is_fallocatable_file", std::bind(&torrent::File::is_fallocatable_file, std::placeholders::_1));
+
+ CMD2_FILE_VALUE_V("f.set_create_queued", std::bind(&torrent::File::set_flags, std::placeholders::_1, torrent::File::flag_create_queued));
+ CMD2_FILE_VALUE_V("f.set_resize_queued", std::bind(&torrent::File::set_flags, std::placeholders::_1, torrent::File::flag_resize_queued));
++ CMD2_FILE_VALUE_V("f.set_fallocate", std::bind(&torrent::File::set_flags, std::placeholders::_1, torrent::File::flag_fallocate));
+ CMD2_FILE_VALUE_V("f.unset_create_queued", std::bind(&torrent::File::unset_flags, std::placeholders::_1, torrent::File::flag_create_queued));
+ CMD2_FILE_VALUE_V("f.unset_resize_queued", std::bind(&torrent::File::unset_flags, std::placeholders::_1, torrent::File::flag_resize_queued));
++ CMD2_FILE_VALUE_V("f.unset_fallocate", std::bind(&torrent::File::unset_flags, std::placeholders::_1, torrent::File::flag_fallocate));
+
+ CMD2_FILE ("f.prioritize_first", std::bind(&torrent::File::has_flags, std::placeholders::_1, torrent::File::flag_prioritize_first));
+ CMD2_FILE_V("f.prioritize_first.enable", std::bind(&torrent::File::set_flags, std::placeholders::_1, torrent::File::flag_prioritize_first));
+--- a/src/core/download.h 2017-04-30 21:35:27.000000000 +0100
++++ a/src/core/download.h 2017-04-30 22:42:23.000000000 +0100
+@@ -119,6 +119,8 @@ public:
+ uint32_t priority();
+ void set_priority(uint32_t p);
+
++ void update_priorities(int flags = 0) { m_download.update_priorities(flags); };
++
+ uint32_t resume_flags() { return m_resumeFlags; }
+ void set_resume_flags(uint32_t flags) { m_resumeFlags = flags; }
+
+--- a/src/core/download_list.cc 2017-04-30 21:35:27.000000000 +0100
++++ a/src/core/download_list.cc 2017-05-14 21:24:57.199934736 +0100
+@@ -120,9 +120,10 @@ DownloadList::find_hex_ptr(const char* h
+ Download*
+ DownloadList::create(torrent::Object* obj, bool printLog) {
+ torrent::Download download;
++ int fallocate = rpc::call_command_value("system.file.allocate") ? torrent::Download::open_enable_fallocate : 0;
+
+ try {
+- download = torrent::download_add(obj);
++ download = torrent::download_add(obj, fallocate);
+
+ } catch (torrent::local_error& e) {
+ delete obj;
+@@ -157,7 +158,9 @@ DownloadList::create(std::istream* str,
+ return NULL;
+ }
+
+- download = torrent::download_add(object);
++ int fallocate = rpc::call_command_value("system.file.allocate") ? torrent::Download::open_enable_fallocate : 0;
++
++ download = torrent::download_add(object, fallocate);
+
+ } catch (torrent::local_error& e) {
+ delete object;
+@@ -341,8 +344,6 @@ DownloadList::resume(Download* download,
+ if (download->download()->info()->is_active())
+ return;
+
+- rpc::parse_command_single(rpc::make_target(download), "view.set_visible=active");
+-
+ // We need to make sure the flags aren't reset if someone decideds
+ // to call resume() while it is hashing, etc.
+ if (download->resume_flags() == ~uint32_t())
+@@ -369,9 +370,6 @@ DownloadList::resume(Download* download,
+ // This will never actually do anything due to the above hash check.
+ // open_throw(download);
+
+- rpc::call_command("d.state_changed.set", cachedTime.seconds(), rpc::make_target(download));
+- rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)) + 1, rpc::make_target(download));
+-
+ if (download->is_partially_done()) {
+ rpc::call_command("d.group.set", "default_seed", rpc::make_target(download));
+
+@@ -401,12 +399,32 @@ DownloadList::resume(Download* download,
+ // Update the priority to ensure it has the correct
+ // seeding/unfinished modifiers.
+ download->set_priority(download->priority());
+- download->download()->start(download->resume_flags());
+
+- download->set_resume_flags(~uint32_t());
++ int openFlags = download->resume_flags();
+
+- DL_TRIGGER_EVENT(download, "event.download.resumed");
++ if (rpc::call_command_value("system.file.allocate"))
++ openFlags |= torrent::Download::open_enable_fallocate;
+
++ try {
++ download->download()->start(openFlags);
++
++ rpc::parse_command_single(rpc::make_target(download), "view.set_visible=active");
++ rpc::call_command("d.state_changed.set", cachedTime.seconds(), rpc::make_target(download));
++ rpc::call_command("d.state_counter.set", rpc::call_command_value("d.state_counter", rpc::make_target(download)) + 1, rpc::make_target(download));
++
++ download->set_resume_flags(~uint32_t());
++
++ DL_TRIGGER_EVENT(download, "event.download.resumed");
++ } catch (torrent::internal_error& e) {
++ std::string errmsg = e.what();
++
++ if (errmsg == "Tried to start an already started download.") {
++ download->set_resume_flags(~uint32_t());
++ } else if (errmsg == "Tried to start a download with not enough disk space for it.") {
++ rpc::call_command("d.stop", torrent::Object(), rpc::make_target(download));
++ control->core()->push_log_std("Not enough disk space to start download " + download->download()->info()->name());
++ }
++ }
+ } catch (torrent::local_error& e) {
+ lt_log_print(torrent::LOG_TORRENT_ERROR, "Could not resume download: %s", e.what());
+ }
+--- a/src/ui/element_file_list.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/ui/element_file_list.cc 2017-04-30 22:42:23.000000000 +0100
+@@ -38,6 +38,7 @@
+
+ #include <rak/algorithm.h>
+ #include <torrent/exceptions.h>
++#include <torrent/download.h>
+ #include <torrent/data/file.h>
+ #include <torrent/data/file_list.h>
+
+@@ -47,6 +48,8 @@
+ #include "display/window_file_list.h"
+ #include "input/manager.h"
+
++#include "rpc/parse_commands.h"
++
+ #include "control.h"
+ #include "element_file_list.h"
+ #include "element_text.h"
+@@ -278,7 +281,8 @@ ElementFileList::receive_priority() {
+ first++;
+ }
+
+- m_download->download()->update_priorities();
++ int flags = rpc::call_command_value("system.file.allocate") ? torrent::Download::open_enable_fallocate : 0;
++ m_download->download()->update_priorities(flags);
+ update_itr();
+ }
+
+@@ -293,7 +297,8 @@ ElementFileList::receive_change_all() {
+ for (torrent::FileList::iterator itr = fl->begin(), last = fl->end(); itr != last; ++itr)
+ (*itr)->set_priority(priority);
+
+- m_download->download()->update_priorities();
++ int flags = rpc::call_command_value("system.file.allocate") ? torrent::Download::open_enable_fallocate : 0;
++ m_download->download()->update_priorities(flags);
+ update_itr();
+ }
+
diff --git a/backport_rt_all_08-info_pane_xb_sizes.patch b/backport_rt_all_08-info_pane_xb_sizes.patch
new file mode 100644
index 000000000000..e185c3c7d25e
--- /dev/null
+++ b/backport_rt_all_08-info_pane_xb_sizes.patch
@@ -0,0 +1,76 @@
+--- a/src/command_ui.cc 2017-06-04 18:27:53.508262194 +0100
++++ a/src/command_ui.cc 2017-06-04 18:28:15.144811646 +0100
+@@ -392,8 +392,16 @@ apply_to_xb(const torrent::Object& rawAr
+ snprintf(buffer, 48, "%5.1f MB", (double)arg / (int64_t(1) << 20));
+ else if (arg < (int64_t(1000) << 30))
+ snprintf(buffer, 48, "%5.1f GB", (double)arg / (int64_t(1) << 30));
+- else
++ else if (arg < (int64_t(1000) << 40))
+ snprintf(buffer, 48, "%5.1f TB", (double)arg / (int64_t(1) << 40));
++ else if (arg < (int64_t(1000) << 50))
++ snprintf(buffer, 48, "%5.1f PB", (double)arg / (int64_t(1) << 50));
++ else if (arg < (int64_t(1000) << 60))
++ snprintf(buffer, 48, "%5.1f EB", (double)arg / (int64_t(1) << 60));
++ else if (arg < (int64_t(1000) << 70))
++ snprintf(buffer, 48, "%5.1f ZB", (double)arg / (int64_t(1) << 70));
++ else
++ snprintf(buffer, 48, "%5.1f YB", (double)arg / (int64_t(1) << 80));
+
+ return std::string(buffer);
+ }
+--- a/src/display/window_peer_list.cc 2017-06-04 18:24:58.355877743 +0100
++++ a/src/display/window_peer_list.cc 2017-06-04 18:25:52.675583468 +0100
+@@ -60,6 +60,8 @@ WindowPeerList::WindowPeerList(core::Dow
+ m_focus(f) {
+ }
+
++std::string human_size(int64_t bytes, unsigned int format=0);
++
+ void
+ WindowPeerList::redraw() {
+ m_slotSchedule(this, (cachedTime + rak::timer::from_seconds(1)).round_seconds());
+@@ -110,9 +112,13 @@ WindowPeerList::redraw() {
+ ip_address.c_str());
+ x += 27;
+
+- m_canvas->print(x, y, "%.1f", (double)p->up_rate()->rate() / 1024); x += 7;
+- m_canvas->print(x, y, "%.1f", (double)p->down_rate()->rate() / 1024); x += 7;
+- m_canvas->print(x, y, "%.1f", (double)p->peer_rate()->rate() / 1024); x += 7;
++ std::string h_up_rate = human_size(p->up_rate()->rate(), 0);
++ std::string h_down_rate = human_size(p->down_rate()->rate(), 0);
++ std::string h_peer_rate = human_size(p->peer_rate()->rate(), 0);
++
++ m_canvas->print(x, y, "%s", h_up_rate.c_str()); x += 7;
++ m_canvas->print(x, y, "%s", h_down_rate.c_str()); x += 7;
++ m_canvas->print(x, y, "%s", h_peer_rate.c_str()); x += 7;
+
+ char remoteChoked;
+ char peerType;
+--- a/src/ui/download.cc 2017-06-04 17:44:36.748996714 +0100
++++ a/src/ui/download.cc 2017-06-04 17:40:55.816860273 +0100
+@@ -171,10 +171,10 @@ Download::create_info() {
+ element->push_column("State changed:", te_command("convert.elapsed_time=$d.state_changed="));
+
+ element->push_back("");
+- element->push_column("Memory usage:", te_command("cat=$convert.mb=$pieces.memory.current=,\" MB\""));
+- element->push_column("Max memory usage:", te_command("cat=$convert.mb=$pieces.memory.max=,\" MB\""));
+- element->push_column("Free diskspace:", te_command("cat=$convert.mb=$d.free_diskspace=,\" MB\""));
+- element->push_column("Safe diskspace:", te_command("cat=$convert.mb=$pieces.sync.safe_free_diskspace=,\" MB\""));
++ element->push_column("Memory usage:", te_command("convert.xb=$pieces.memory.current="));
++ element->push_column("Max memory usage:", te_command("convert.xb=$pieces.memory.max="));
++ element->push_column("Free diskspace:", te_command("convert.xb=$d.free_diskspace="));
++ element->push_column("Safe diskspace:", te_command("convert.xb=$pieces.sync.safe_free_diskspace="));
+
+ element->push_back("");
+ element->push_column("Connection type:", te_command("cat=(d.connection_current),\" \",(if,(d.accepting_seeders),"",\"no_seeders\")"));
+@@ -187,8 +187,8 @@ Download::create_info() {
+ "\"/\",(choke_group.down.total,(d.group)),\"] [Rate \",(convert.kb,(choke_group.up.rate,(d.group))),"
+ "\"/\",(convert.kb,(choke_group.down.rate,(d.group))),\" KB]\""));
+ element->push_column("Safe sync:", te_command("if=$pieces.sync.always_safe=,yes,no"));
+- element->push_column("Send buffer:", te_command("cat=$convert.kb=$network.send_buffer.size=,\" KB\""));
+- element->push_column("Receive buffer:", te_command("cat=$convert.kb=$network.receive_buffer.size=,\" KB\""));
++ element->push_column("Send buffer:", te_command("convert.xb=$network.send_buffer.size="));
++ element->push_column("Receive buffer:", te_command("convert.xb=$network.receive_buffer.size="));
+
+ // TODO: Define a custom command for this and use $argument.0 instead of looking up the name multiple times?
+ element->push_column("Throttle:", te_command("branch=d.throttle_name=,\""
diff --git a/backport_rt_all_09-inotify_mod.patch b/backport_rt_all_09-inotify_mod.patch
new file mode 100644
index 000000000000..fd885b71f20a
--- /dev/null
+++ b/backport_rt_all_09-inotify_mod.patch
@@ -0,0 +1,137 @@
+--- a/src/command_events.cc 2016-10-23 05:33:00.000000000 +0100
++++ a/src/command_events.cc 2017-06-11 10:26:11.545873005 +0100
+@@ -308,24 +308,89 @@ d_multicall(const torrent::Object::list_
+ }
+
+ static void
+-call_watch_command(const std::string& command, const std::string& path) {
+- rpc::commands.call_catch(command.c_str(), rpc::make_target(), path);
++call_watch_command_added(const core::Manager::command_list_type& args, const std::string& path) {
++ if (args.size() < 1)
++ throw torrent::input_error("Too few arguments.");
++
++ std::string rpc_command_str;
++ core::Manager::command_list_type::const_iterator argsItr = args.begin();
++
++ std::string command = *argsItr;
++
++ // Include path in quotes, it can have spaces.
++ rpc_command_str = command + (*command.rbegin() != '=' ? "=" : "") + "\"" + path + "\"";
++
++ while (++argsItr != args.end())
++ rpc_command_str += ",\"" + *argsItr + "\"";
++
++ rpc::parse_command_single_std(rpc_command_str);
+ }
+
+ torrent::Object
+ directory_watch_added(const torrent::Object::list_type& args) {
+- if (args.size() != 2)
++ if (args.size() < 2)
+ throw torrent::input_error("Too few arguments.");
+
+- const std::string& path = args.front().as_string();
+- const std::string& command = args.back().as_string();
+-
+ if (!control->directory_events()->open())
+ throw torrent::input_error("Could not open inotify:" + std::string(rak::error_number::current().c_str()));
+
+- control->directory_events()->notify_on(path.c_str(),
++ core::Manager::command_list_type commands;
++ torrent::Object::list_const_iterator argsItr = args.begin();
++
++ const std::string& path = argsItr->as_string();
++
++ while (++argsItr != args.end())
++ commands.push_back(argsItr->as_string());
++
++ control->directory_events()->notify_on(path,
+ torrent::directory_events::flag_on_added | torrent::directory_events::flag_on_updated,
+- std::bind(&call_watch_command, command, std::placeholders::_1));
++ std::bind(&call_watch_command_added, commands, std::placeholders::_1));
++ return torrent::Object();
++}
++
++static void
++call_watch_command_removed(const std::string& command, const std::string& path) {
++ if (command != "d.stop" && command != "d.close" && command != "d.erase")
++ return;
++
++ for (core::DownloadList::iterator itr = control->core()->download_list()->begin(); itr != control->core()->download_list()->end(); ++itr) {
++ const std::string& tiedToFile = rpc::call_command_string("d.tied_to_file", rpc::make_target(*itr));
++
++ if (!tiedToFile.empty() && rak::path_expand(path) == rak::path_expand(tiedToFile)) {
++
++ if (command == "d.stop" && rpc::call_command_value("d.state", rpc::make_target(*itr)) != 0) {
++ rpc::parse_command_single(rpc::make_target(*itr), "d.try_stop=");
++ } else if (command == "d.close" && rpc::call_command_value("d.ignore_commands", rpc::make_target(*itr)) == 0) {
++ rpc::parse_command_single(rpc::make_target(*itr), "d.try_close=");
++ } else if (command == "d.erase") {
++ // Need to clear tied_to_file so it doesn't try to delete it.
++ rpc::call_command("d.tied_to_file.set", std::string(), rpc::make_target(*itr));
++ control->core()->download_list()->erase(itr);
++ }
++
++ break;
++ }
++ }
++}
++
++torrent::Object
++directory_watch_removed(const torrent::Object::list_type& args) {
++ if (args.size() < 2)
++ throw torrent::input_error("Too few arguments.");
++
++ torrent::Object::list_const_iterator argsItr = args.begin();
++ std::string command = argsItr->as_string();
++
++ if (command != "d.stop" && command != "d.close" && command != "d.erase")
++ throw torrent::input_error("directory.watch.removed command only supports d.stop , d.close , d.erase commands.");
++
++ if (!control->directory_events()->open())
++ throw torrent::input_error("Could not open inotify:" + std::string(rak::error_number::current().c_str()));
++
++ while (++argsItr != args.end())
++ control->directory_events()->notify_on(argsItr->as_string(),
++ torrent::directory_events::flag_on_removed,
++ std::bind(&call_watch_command_removed, command, std::placeholders::_1));
+ return torrent::Object();
+ }
+
+@@ -333,10 +398,10 @@ void
+ initialize_command_events() {
+ CMD2_ANY_STRING ("on_ratio", std::bind(&apply_on_ratio, std::placeholders::_2));
+
+- CMD2_ANY ("start_tied", std::bind(&apply_start_tied));
+- CMD2_ANY ("stop_untied", std::bind(&apply_stop_untied));
+- CMD2_ANY ("close_untied", std::bind(&apply_close_untied));
+- CMD2_ANY ("remove_untied", std::bind(&apply_remove_untied));
++ CMD2_ANY ("tied.start", std::bind(&apply_start_tied));
++ CMD2_ANY ("untied.stop", std::bind(&apply_stop_untied));
++ CMD2_ANY ("untied.close", std::bind(&apply_close_untied));
++ CMD2_ANY ("untied.remove", std::bind(&apply_remove_untied));
+
+ CMD2_ANY_LIST ("schedule2", std::bind(&apply_schedule, std::placeholders::_2));
+ CMD2_ANY_STRING_V("schedule_remove2", std::bind(&rpc::CommandScheduler::erase_str, control->command_scheduler(), std::placeholders::_2));
+@@ -360,5 +425,6 @@ initialize_command_events() {
+ CMD2_ANY_LIST ("download_list", std::bind(&apply_download_list, std::placeholders::_2));
+ CMD2_ANY_LIST ("d.multicall2", std::bind(&d_multicall, std::placeholders::_2));
+
+- CMD2_ANY_LIST ("directory.watch.added", std::bind(&directory_watch_added, std::placeholders::_2));
++ CMD2_ANY_LIST ("directory.watch.added", std::bind(&directory_watch_added, std::placeholders::_2));
++ CMD2_ANY_LIST ("directory.watch.removed", std::bind(&directory_watch_removed, std::placeholders::_2));
+ }
+--- a/src/main.cc 2017-06-04 13:03:39.000000000 +0100
++++ a/src/main.cc 2017-06-10 09:33:10.387844804 +0100
+@@ -429,6 +429,11 @@ main(int argc, char** argv) {
+
+ CMD2_REDIRECT ("torrent_list_layout", "ui.torrent_list.layout.set");
+
++ CMD2_REDIRECT_GENERIC("start_tied", "tied.start");
++ CMD2_REDIRECT_GENERIC("stop_untied", "untied.stop");
++ CMD2_REDIRECT_GENERIC("close_untied", "untied.close");
++ CMD2_REDIRECT_GENERIC("remove_untied", "untied.remove");
++
+ // Deprecated commands. Don't use these anymore.
+
+ if (rpc::call_command_value("method.use_intermediate") == 1) {
diff --git a/backport_rt_all_80-ps-dl-ui-find.patch b/backport_rt_all_80-ps-dl-ui-find.patch
new file mode 100644
index 000000000000..a48dd6bb75c7
--- /dev/null
+++ b/backport_rt_all_80-ps-dl-ui-find.patch
@@ -0,0 +1,51 @@
+--- a/src/ui/download_list.cc
++++ b/src/ui/download_list.cc
+@@ -277,6 +277,10 @@ DownloadList::receive_view_input(Input t
+ title = "filter";
+ break;
+
++ case INPUT_FIND:
++ title = "find";
++ break;
++
+ default:
+ throw torrent::internal_error("DownloadList::receive_view_input(...) Invalid input type.");
+ }
+@@ -372,6 +376,11 @@ DownloadList::receive_exit_input(Input t
+ }
+ break;
+
++ case INPUT_FIND:
++ rpc::call_command("ui.find.term.set", rak::trim(input->str()), rpc::make_target());
++ rpc::call_command("ui.find.next", torrent::Object(), rpc::make_target());
++ break;
++
+ default:
+ throw torrent::internal_error("DownloadList::receive_exit_input(...) Invalid input type.");
+ }
+@@ -384,13 +403,13 @@ DownloadList::setup_keys() {
+ m_bindings['\x0F'] = std::bind(&DownloadList::receive_view_input, this, INPUT_CHANGE_DIRECTORY);
+ m_bindings['X' - '@'] = std::bind(&DownloadList::receive_view_input, this, INPUT_COMMAND);
+ m_bindings['F'] = std::bind(&DownloadList::receive_view_input, this, INPUT_FILTER);
++ m_bindings['F' - '@'] = std::bind(&DownloadList::receive_view_input, this, INPUT_FIND);
+
+ m_uiArray[DISPLAY_LOG]->bindings()[KEY_LEFT] =
+ m_uiArray[DISPLAY_LOG]->bindings()['B' - '@'] =
+ m_uiArray[DISPLAY_LOG]->bindings()[' '] = std::bind(&DownloadList::activate_display, this, DISPLAY_DOWNLOAD_LIST);
+
+- m_uiArray[DISPLAY_DOWNLOAD_LIST]->bindings()[KEY_RIGHT] =
+- m_uiArray[DISPLAY_DOWNLOAD_LIST]->bindings()['F' - '@'] = std::bind(&DownloadList::activate_display, this, DISPLAY_DOWNLOAD);
++ m_uiArray[DISPLAY_DOWNLOAD_LIST]->bindings()[KEY_RIGHT] = std::bind(&DownloadList::activate_display, this, DISPLAY_DOWNLOAD);
+ m_uiArray[DISPLAY_DOWNLOAD_LIST]->bindings()['l'] = std::bind(&DownloadList::activate_display, this, DISPLAY_LOG);
+ }
+
+--- a/src/ui/download_list.h 2018-08-21 15:39:24.381506981 +0100
++++ a/src/ui/download_list.h 2018-08-21 15:47:05.582341775 +0100
+@@ -88,6 +88,7 @@ public:
+ INPUT_CHANGE_DIRECTORY,
+ INPUT_COMMAND,
+ INPUT_FILTER,
++ INPUT_FIND,
+ INPUT_EOI
+ } Input;
+
diff --git a/command_pyroscope.cc b/command_pyroscope.cc
new file mode 100644
index 000000000000..5fdac8b11a42
--- /dev/null
+++ b/command_pyroscope.cc
@@ -0,0 +1,1370 @@
+// PyroScope - rTorrent Command Extensions
+// Copyright (c) 2011 The PyroScope Project <pyroscope.project@gmail.com>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+#include "config.h"
+#include "globals.h"
+
+#include <cstdio>
+#include <climits>
+#include <ctime>
+#include <cwchar>
+#include <set>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <rak/path.h>
+#include <rak/functional.h>
+#include <rak/functional_fun.h>
+
+#include "core/download.h"
+#include "core/manager.h"
+#include "core/view_manager.h"
+#include "rpc/parse.h"
+#include "torrent/tracker.h"
+#include "torrent/tracker_list.h"
+#include "ui/root.h"
+#include "ui/download_list.h"
+#include "ui/element_base.h"
+#include "ui/element_download_list.h"
+
+#include "globals.h"
+#include "control.h"
+#include "command_helpers.h"
+
+#if (RT_HEX_VERSION <= 0x000906)
+ #define _cxxstd_ tr1
+#else
+ #define _cxxstd_ std
+#endif
+
+// List of system capabilities for `system.has` command
+static std::set<std::string> system_capabilities;
+
+// handle for message log file
+namespace core {
+int log_messages_fd = -1;
+};
+
+
+#if RT_HEX_VERSION <= 0x000909
+// will be merged into 0.9.9+ mainline?
+
+namespace torrent {
+
+/* uniform_rng - Uniform distribution random number generator.
+
+ This class implements a no-shared-state random number generator that
+ emits uniformly distributed numbers with high entropy. It solves the
+ two problems of a simple `random() % limit`, which is a skewed
+ distribution due to RAND_MAX typically not being evenly divisble by
+ the limit, and worse, the lower bits of typical PRNGs having extremly
+ low entropy – the end result are grossly un-random number sequences.
+
+ A `uniform_rng` instance carries its own state, unlike the `random()`
+ function, and is thus thread-safe when no instance is shared between
+ threads. It uses `random_r()` and `initstate_r()` from glibc.
+ */
+class uniform_rng {
+public:
+ uniform_rng();
+
+ int rand();
+ int rand_range(int lo, int hi);
+ int rand_below(int limit) { return this->rand_range(0, limit-1); }
+
+private:
+ char m_state[128];
+ struct ::random_data m_data;
+};
+
+
+uniform_rng::uniform_rng() {
+ unsigned int seed = cachedTime.usec() ^ (getpid() << 16) ^ getppid();
+ ::initstate_r(seed, m_state, sizeof(m_state), &m_data);
+}
+
+// return random number in interval [0, RAND_MAX]
+int uniform_rng::rand()
+{
+ int rval;
+ if (::random_r(&m_data, &rval) == -1) {
+ throw torrent::input_error("system.random: random_r() failure!");
+ }
+ return rval;
+}
+
+// return random number in interval [lo, hi]
+int uniform_rng::rand_range(int lo, int hi)
+{
+ if (lo > hi) {
+ throw torrent::input_error("Empty interval passed to rand_range (low > high)");
+ }
+ if (lo < 0 || RAND_MAX < lo) {
+ throw torrent::input_error("Lower bound of rand_range outside 0..RAND_MAX");
+ }
+ if (hi < 0 || RAND_MAX < hi) {
+ throw torrent::input_error("Upper bound of rand_range outside 0..RAND_MAX");
+ }
+
+ int rval;
+ const int64_t range = 1 + hi - lo;
+ const int64_t buckets = RAND_MAX / range;
+ const int64_t limit = buckets * range;
+
+ /* Create equal size buckets all in a row, then fire randomly towards
+ * the buckets until you land in one of them. All buckets are equally
+ * likely. If you land off the end of the line of buckets, try again. */
+ do {
+ rval = this->rand();
+ } while (rval >= limit);
+
+ return (int) (lo + (rval / buckets));
+}
+
+}; // namespace torrent
+
+
+static torrent::uniform_rng system_random_gen;
+
+
+/* @DOC
+ `system.random = [[<lower>,] <upper>]`
+
+ Generate *uniformly* distributed random numbers in the range
+ defined by `lower`..`upper`.
+
+ The default range with no args is `0`..`RAND_MAX`. Providing
+ just one argument sets an *exclusive* upper bound, and two
+ args define an *inclusive* range.
+
+ An example use-case is adding jitter to time values that you
+ later check with `elapsed.greater`, to avoid load spikes and
+ similar effects of clustered time triggers.
+*/
+torrent::Object apply_random(rpc::target_type target, const torrent::Object::list_type& args) {
+ int64_t lo = 0, hi = RAND_MAX;
+
+ torrent::Object::list_const_iterator itr = args.begin();
+ if (args.size() > 2) {
+ throw torrent::input_error("system.random accepts at most two arguments!");
+ }
+ if (args.size() > 1) {
+ lo = (itr++)->as_value();
+ hi = (itr++)->as_value();
+ } else if (args.size() > 0) {
+ hi = (itr++)->as_value() - 1;
+ }
+
+ return (int64_t) system_random_gen.rand_range(lo, hi);
+}
+
+// #else
+// #include "torrent/utils/uniform_rng.h"
+#endif
+
+
+// return the "main" tracker for this download item
+torrent::Tracker* get_active_tracker(torrent::Download* item) {
+ torrent::TrackerList* tl = item->tracker_list();
+ torrent::Tracker* tracker = 0;
+ torrent::Tracker* fallback = 0;
+
+ for (size_t trkidx = 0; trkidx < tl->size(); trkidx++) {
+ tracker = tl->at(trkidx);
+ if (tracker->is_usable() && tracker->type() == torrent::Tracker::TRACKER_HTTP) {
+ if (!fallback) fallback = tracker;
+ if (tracker->scrape_complete() || tracker->scrape_incomplete()) {
+ break;
+ }
+ }
+ tracker = 0;
+ }
+ if (!tracker && tl->size()) tracker = fallback ? fallback : tl->at(0);
+
+ return tracker;
+}
+
+
+// return the domain name of the "main" tracker of the given download item
+std::string get_active_tracker_domain(torrent::Download* item) {
+ std::string url;
+ torrent::Tracker* tracker = get_active_tracker(item);
+
+ if (tracker && !tracker->url().empty()) {
+ url = tracker->url();
+
+ // snip url to domain name
+ if (url.compare(0, 7, "http://") == 0) url = url.substr(7);
+ if (url.compare(0, 8, "https://") == 0) url = url.substr(8);
+ if (url.find('/') > 0) url = url.substr(0, url.find('/'));
+ if (url.find(':') > 0) url = url.substr(0, url.find(':'));
+
+ // remove some common cruft
+ const char* domain_cruft[] = {
+ "tracker", "1.", "2.", "001.", ".",
+ "www.", "cfdata.",
+ 0
+ };
+ for (const char** cruft = domain_cruft; *cruft; cruft++) {
+ int cruft_len = strlen(*cruft);
+ if (url.compare(0, cruft_len, *cruft) == 0) url = url.substr(cruft_len);
+ }
+ }
+
+ return url;
+}
+
+
+// return various scrape information of the "main" tracker for this download item
+int64_t get_active_tracker_scrape_info(const int operation, torrent::Download* item) {
+ int64_t scrape_num = 0;
+ torrent::Tracker* tracker = get_active_tracker(item);
+
+ if (tracker) {
+ switch (operation) {
+ case 1:
+ scrape_num = tracker->scrape_downloaded();
+ break;
+ case 2:
+ scrape_num = tracker->scrape_complete();
+ break;
+ case 3:
+ scrape_num = tracker->scrape_incomplete();
+ break;
+ }
+ }
+
+ return scrape_num;
+}
+
+
+// return the name of the parent directory of the given download item
+std::string get_parent_dir(core::Download* item) {
+ std::string path = rpc::call_command_string("d.directory", rpc::make_target(item)).c_str();
+
+ if (rpc::call_command_value("d.is_multi_file", rpc::make_target(item)) == 1) {
+ path = path.substr(0, path.find_last_of("\\/"));
+ }
+
+ return path.substr(path.find_last_of("\\/") + 1);
+}
+
+
+#if RT_HEX_VERSION <= 0x000907
+// this is merged into 0.9.8 mainline!
+/* @DOC
+ `compare = <order>, <sort_key>=[, ...]`
+
+ Compares two items like `less=` or `greater=`, but allows to compare
+ by several different sort criteria, and ascending or descending
+ order per given field. The first parameter is a string of order
+ indicators, either `aA+` for ascending or `dD-` for descending.
+ The default, i.e. when there's more fields than indicators, is
+ ascending. Field types other than value or string are treated
+ as equal (or in other words, they're ignored).
+
+ If all fields are equal, then items are ordered in a random, but
+ stable fashion.
+
+ Configuration example:
+
+ # VIEW: Show active and incomplete torrents (in view #9) and update every 20 seconds
+ # Items are grouped into complete, incomplete, and queued, in that order.
+ # Within each group, they're sorted by upload and then download speed.
+ view.sort_current = active,"compare=----,d.is_open=,d.complete=,d.up.rate=,d.down.rate="
+ schedule = filter_active,12,20,"view.filter = active,\"or={d.up.rate=,d.down.rate=,not=$d.complete=}\" ;view.sort=active"
+*/
+torrent::Object apply_compare(rpc::target_type target, const torrent::Object::list_type& args) {
+ if (!rpc::is_target_pair(target))
+ throw torrent::input_error("Can only compare a target pair.");
+
+ if (args.size() < 2)
+ throw torrent::input_error("Need at least order and one field.");
+
+ torrent::Object::list_const_iterator itr = args.begin();
+ std::string order = (itr++)->as_string();
+ const char* current = order.c_str();
+
+ torrent::Object result1;
+ torrent::Object result2;
+
+ for (torrent::Object::list_const_iterator last = args.end(); itr != last; itr++) {
+ std::string field = itr->as_string();
+ result1 = rpc::parse_command_single(rpc::get_target_left(target), field);
+ result2 = rpc::parse_command_single(rpc::get_target_right(target), field);
+
+ if (result1.type() != result2.type())
+ throw torrent::input_error(std::string("Type mismatch in compare of ") + field);
+
+ bool descending = *current == 'd' || *current == 'D' || *current == '-';
+ if (*current) {
+ if (!descending && !(*current == 'a' || *current == 'A' || *current == '+'))
+ throw torrent::input_error(std::string("Bad order '") + *current + "' in " + order);
+ ++current;
+ }
+
+ switch (result1.type()) {
+ case torrent::Object::TYPE_VALUE:
+ if (result1.as_value() != result2.as_value())
+ return (int64_t) (descending ^ (result1.as_value() < result2.as_value()));
+ break;
+
+ case torrent::Object::TYPE_STRING:
+ if (result1.as_string() != result2.as_string())
+ return (int64_t) (descending ^ (result1.as_string() < result2.as_string()));
+ break;
+
+ default:
+ break; // treat unknown types as equal
+ }
+ }
+
+ // if all else is equal, ensure stable sort order based on memory location
+ return (int64_t) (target.second < target.third);
+}
+#endif
+
+
+static std::map<int, std::string> bound_commands[ui::DownloadList::DISPLAY_MAX_SIZE];
+
+/* @DOC
+ ui.bind_key=display,key,"command1=[,...]"
+
+ Binds the given key on a specified display to execute the commands when pressed.
+
+ "display" must be one of "download_list", ...
+ "key" can be either a single character for normal keys,
+ ^ plus a character for control keys, or a 4 digit octal key code.
+
+ Configuration example:
+ # VIEW: Bind view #7 to the "rtcontrol" result
+ schedule = bind_7,1,0,"ui.bind_key=download_list,7,ui.current_view.set=rtcontrol"
+*/
+torrent::Object apply_ui_bind_key(rpc::target_type target, const torrent::Object& rawArgs) {
+ const torrent::Object::list_type& args = rawArgs.as_list();
+
+ if (args.size() != 3)
+ throw torrent::input_error("Expecting display, key, and commands.");
+
+ // Parse positional arguments
+ torrent::Object::list_const_iterator itr = args.begin();
+ const std::string& element = (itr++)->as_string();
+ const std::string& keydef = (itr++)->as_string();
+ const std::string& commands = (itr++)->as_string();
+ const bool verbose = rpc::call_command_value("ui.bind_key.verbose");
+
+ // Get key index from definition
+ if (keydef.empty() || keydef.size() > (keydef[0] == '0' ? 4 : keydef[0] == '^' ? 2 : 1))
+ throw torrent::input_error("Bad key definition.");
+ int key = keydef[0];
+ if (key == '^' && keydef.size() > 1) key = keydef[1] & 31;
+ if (key == '0' && keydef.size() != 1) {
+ if (keydef.size() != 4)
+ throw torrent::input_error("Bad key definition (expected 4 digit octal code).");
+ key = (int) strtol(keydef.c_str(), (char **) NULL, 8);
+ }
+
+ // Look up display
+ ui::DownloadList::Display displayType = ui::DownloadList::DISPLAY_MAX_SIZE;
+ if (element == "download_list") {
+ displayType = ui::DownloadList::DISPLAY_DOWNLOAD_LIST;
+ } else {
+ throw torrent::input_error(std::string("Unknown display ") + element);
+ }
+ ui::DownloadList* dl_list = control->ui()->download_list();
+ if (!dl_list)
+ throw torrent::input_error("No download list.");
+ ui::ElementBase* display = dl_list->display(displayType);
+ if (!display)
+ throw torrent::input_error("Display not found.");
+
+ // Bind the key to the given commands
+ bool new_binding = display->bindings().find(key) == display->bindings().end();
+ bound_commands[displayType][key] = commands; // keep hold of the string, so the c_str() below remains valid
+ switch (displayType) {
+ case ui::DownloadList::DISPLAY_DOWNLOAD_LIST:
+ display->bindings()[key] =
+ _cxxstd_::bind(&ui::ElementDownloadList::receive_command, (ui::ElementDownloadList*)display,
+ bound_commands[displayType][key].c_str());
+ break;
+ default:
+ return torrent::Object();
+ }
+
+ if (!new_binding && verbose) {
+ std::string msg = "Replaced key binding";
+ msg += " for " + keydef + " in " + element + " with " + commands.substr(0, 30);
+ if (commands.size() > 30) msg += "...";
+ control->core()->push_log(msg.c_str());
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object cmd_ui_focus_home() {
+ ui::DownloadList* dl_list = control->ui()->download_list();
+ core::View* dl_view = dl_list->current_view();
+
+ if (!dl_view->empty_visible()) {
+ dl_view->set_focus(dl_view->begin_visible());
+ dl_view->set_last_changed();
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object cmd_ui_focus_end() {
+ ui::DownloadList* dl_list = control->ui()->download_list();
+ core::View* dl_view = dl_list->current_view();
+
+ if (!dl_view->empty_visible()) {
+ dl_view->set_focus(dl_view->end_visible() - 1);
+ dl_view->set_last_changed();
+ }
+
+ return torrent::Object();
+}
+
+
+static int ui_page_size() {
+ // TODO: map 0 to the current view size, for adaptive scrolling
+ return std::max(1, (int) rpc::call_command_value("ui.focus.page_size"));
+}
+
+
+torrent::Object cmd_ui_focus_pgup() {
+ ui::DownloadList* dl_list = control->ui()->download_list();
+ core::View* dl_view = dl_list->current_view();
+
+ int skip = ui_page_size();
+ if (!dl_view->empty_visible()) {
+ if (dl_view->focus() == dl_view->end_visible())
+ dl_view->set_focus(dl_view->end_visible() - 1);
+ else if (dl_view->focus() - dl_view->begin_visible() >= skip)
+ dl_view->set_focus(dl_view->focus() - skip);
+ else
+ dl_view->set_focus(dl_view->begin_visible());
+ dl_view->set_last_changed();
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object cmd_ui_focus_pgdn() {
+ ui::DownloadList* dl_list = control->ui()->download_list();
+ core::View* dl_view = dl_list->current_view();
+
+ int skip = ui_page_size();
+ if (!dl_view->empty_visible()) {
+ if (dl_view->focus() == dl_view->end_visible())
+ dl_view->set_focus(dl_view->begin_visible());
+ else if (dl_view->end_visible() - dl_view->focus() > skip)
+ dl_view->set_focus(dl_view->focus() + skip);
+ else
+ dl_view->set_focus(dl_view->end_visible() - 1);
+ dl_view->set_last_changed();
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object cmd_log_messages(const torrent::Object::string_type& arg) {
+ if (arg.empty()) {
+ control->core()->push_log_std("Closing message log file.");
+ }
+
+ if (core::log_messages_fd >= 0) {
+ ::close(core::log_messages_fd);
+ core::log_messages_fd = -1;
+ }
+
+ if (!arg.empty()) {
+ core::log_messages_fd = open(rak::path_expand(arg).c_str(), O_WRONLY | O_APPEND | O_CREAT, 0644);
+
+ if (core::log_messages_fd < 0) {
+ throw torrent::input_error("Could not open message log file.");
+ }
+
+ control->core()->push_log_std("Opened message log file '" + rak::path_expand(arg) + "'.");
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object cmd_import_return(rpc::target_type target, const torrent::Object& args) {
+ // Handled in src/rpc/parse_commands.cc::parse_command_file via patch
+ throw torrent::input_error("import.return");
+}
+
+
+torrent::Object cmd_do(rpc::target_type target, const torrent::Object& args) {
+ return rpc::call_object(args, target);
+}
+
+
+#if RT_HEX_VERSION <= 0x000907
+// this is merged into 0.9.8 mainline!
+torrent::Object retrieve_d_custom_if_z(core::Download* download, const torrent::Object::list_type& args) {
+ torrent::Object::list_const_iterator itr = args.begin();
+ if (itr == args.end())
+ throw torrent::bencode_error("d.custom.if_z: Missing key argument.");
+ const std::string& key = (itr++)->as_string();
+ if (key.empty())
+ throw torrent::bencode_error("d.custom.if_z: Empty key argument.");
+ if (itr == args.end())
+ throw torrent::bencode_error("d.custom.if_z: Missing default argument.");
+
+ try {
+ const std::string& val = download->bencode()->get_key("rtorrent").get_key("custom").get_key_string(key);
+ return val.empty() ? itr->as_string() : val;
+ } catch (torrent::bencode_error& e) {
+ return itr->as_string();
+ }
+}
+#endif
+
+
+torrent::Object cmd_d_custom_set_if_z(core::Download* download, const torrent::Object::list_type& args) {
+ torrent::Object::list_const_iterator itr = args.begin();
+ if (itr == args.end())
+ throw torrent::bencode_error("d.custom.set_if_z: Missing key argument.");
+ const std::string& key = (itr++)->as_string();
+ if (key.empty())
+ throw torrent::bencode_error("d.custom.set_if_z: Empty key argument.");
+ if (itr == args.end())
+ throw torrent::bencode_error("d.custom.set_if_z: Missing value argument.");
+
+ bool set_it = false;
+ try {
+ const std::string& val = download->bencode()->get_key("rtorrent").get_key("custom").get_key_string(key);
+ set_it = val.empty();
+ } catch (torrent::bencode_error& e) {
+ set_it = true;
+ }
+ if (set_it)
+ download->bencode()->get_key("rtorrent").
+ insert_preserve_copy("custom", torrent::Object::create_map()).first->second.
+ insert_key(key, itr->as_string());
+
+ return torrent::Object();
+}
+
+
+torrent::Object cmd_d_custom_erase(core::Download* download, const torrent::Object::list_type& args) {
+ for (torrent::Object::list_type::const_iterator itr = args.begin(), last = args.end(); itr != last; itr++) {
+ const std::string& key = itr->as_string();
+ if (key.empty())
+ throw torrent::bencode_error("d.custom.erase: Empty key argument.");
+
+ download->bencode()->get_key("rtorrent").get_key("custom").erase_key(key);
+ }
+
+ return torrent::Object();
+}
+
+
+#if RT_HEX_VERSION <= 0x000907
+// this is merged into 0.9.8 mainline!
+torrent::Object retrieve_d_custom_map(core::Download* download, bool keys_only, const torrent::Object::list_type& args) {
+ if (args.begin() != args.end())
+ throw torrent::bencode_error("d.custom.keys/items takes no arguments.");
+
+ torrent::Object result = keys_only ? torrent::Object::create_list() : torrent::Object::create_map();
+ torrent::Object::map_type& entries = download->bencode()->get_key("rtorrent").get_key("custom").as_map();
+
+ for (torrent::Object::map_type::const_iterator itr = entries.begin(), last = entries.end(); itr != last; itr++) {
+ if (keys_only) result.as_list().push_back(itr->first);
+ else result.as_map()[itr->first] = itr->second;
+ }
+
+ return result;
+}
+#endif
+
+
+torrent::Object cmd_d_custom_toggle(core::Download* download, const std::string& key) {
+ bool result = true;
+ try {
+ const std::string& strval = download->bencode()->get_key("rtorrent").get_key("custom").get_key_string(key);
+ if (!strval.empty()) {
+ char* junk = 0;
+ long number = strtol(strval.c_str(), &junk, 10);
+ while (std::isspace(*junk)) ++junk;
+ result = !*junk && number == 0;
+ }
+ } catch (torrent::bencode_error& e) {
+ // true
+ }
+
+ download->bencode()->get_key("rtorrent").
+ insert_preserve_copy("custom", torrent::Object::create_map()).first->second.
+ insert_key(key, result ? "1" : "0");
+ return (int64_t) (result ? 1 : 0);
+}
+
+
+torrent::Object retrieve_d_custom_as_value(core::Download* download, const std::string& key) {
+ try {
+ const std::string& strval = download->bencode()->get_key("rtorrent").get_key("custom").get_key_string(key);
+ if (strval.empty())
+ return (int64_t) 0;
+
+ char* junk = 0;
+ long result = strtol(strval.c_str(), &junk, 10);
+ if (*junk)
+ throw torrent::input_error("d.custom.as_value(" + key + "): junk at end of '" + strval + "'!");
+ return (int64_t) result;
+ } catch (torrent::bencode_error& e) {
+ return (int64_t) 0;
+ }
+}
+
+
+#if RT_HEX_VERSION <= 0x000907
+// this is merged into 0.9.8 mainline!
+torrent::Object
+d_multicall_filtered(const torrent::Object::list_type& args) {
+ if (args.size() < 2)
+ throw torrent::input_error("d.multicall.filtered requires at least 2 arguments.");
+ torrent::Object::list_const_iterator arg = args.begin();
+
+ // Find the given view
+ core::ViewManager* viewManager = control->view_manager();
+ core::ViewManager::iterator viewItr = viewManager->find(arg->as_string().empty() ? "default" : arg->as_string());
+
+ if (viewItr == viewManager->end())
+ throw torrent::input_error("Could not find view '" + arg->as_string() + "'.");
+
+ // Make a filtered copy of the current item list
+ core::View::base_type dlist;
+ (*viewItr)->filter_by(*++arg, dlist);
+
+ // Generate result by iterating over all items
+ torrent::Object resultRaw = torrent::Object::create_list();
+ torrent::Object::list_type& result = resultRaw.as_list();
+ ++arg; // skip to first command
+
+ for (core::View::iterator item = dlist.begin(); item != dlist.end(); ++item) {
+ // Add empty row to result
+ torrent::Object::list_type& row = result.insert(result.end(), torrent::Object::create_list())->as_list();
+
+ // Call the provided commands and assemble their results
+ for (torrent::Object::list_const_iterator command = arg; command != args.end(); command++) {
+ const std::string& cmdstr = command->as_string();
+ row.push_back(rpc::parse_command(rpc::make_target(*item), cmdstr.c_str(), cmdstr.c_str() + cmdstr.size()).first);
+ }
+ }
+
+ return resultRaw;
+}
+#endif
+
+
+/* throttle.names=
+ Returns a list of all defined throttle names,
+ including the built-in ones (i.e. '' and NULL).
+ https://github.com/pyroscope/rtorrent-ps/issues/65
+ */
+torrent::Object cmd_throttle_names() {
+ torrent::Object result = torrent::Object::create_list();
+ torrent::Object::list_type& resultList = result.as_list();
+
+ resultList.push_back(std::string());
+ for (core::ThrottleMap::const_iterator itr = control->core()->throttles().begin();
+ itr != control->core()->throttles().end(); itr++) {
+ resultList.push_back(itr->first);
+ }
+
+ return result;
+}
+
+
+// Get length of an UTF8-encoded std::string
+size_t u8_length(const std::string& text) {
+ // Take total length and subtract number of non-leading multi-bytes
+ return text.length() - count_if(text.begin(), text.end(),
+ [](char c)->bool { return (c & 0xC0) == 0x80; });
+}
+
+
+// Chop off an UTF-8 string
+std::string u8_chop(const std::string& text, size_t glyphs) {
+ std::mbstate_t mbs = std::mbstate_t();
+ size_t bytes = 0, skip;
+ const char* pos = text.c_str();
+
+ while (*pos && glyphs-- > 0 && (skip = std::mbrlen(pos, text.length() - bytes, &mbs)) > 0) {
+ pos += skip;
+ bytes += skip;
+ }
+
+ return bytes < text.length() ? text.substr(0, bytes) : text;
+}
+
+
+static const std::string& string_get_first_arg(const char* name, const torrent::Object::list_type& args) {
+ torrent::Object::list_const_iterator itr = args.begin();
+ if (args.size() < 1 || !itr->is_string()) {
+ throw torrent::input_error("string." + std::string(name) + " needs a string argument.0!");
+ }
+ return itr->as_string();
+}
+
+
+// get a numeric arg from a string or value, advancing the passed iterator
+static int64_t string_get_value_arg(const char* name, torrent::Object::list_const_iterator& itr) {
+ int64_t result = 0;
+ if (itr->is_string()) {
+ char* junk = 0;
+ result = strtol(itr->as_string().c_str(), &junk, 10);
+ if (*junk) {
+ throw torrent::input_error("string." + std::string(name) + ": "
+ "junk at end of value: " + itr->as_string());
+ }
+ } else {
+ result = itr->as_value();
+ }
+
+ ++itr;
+ return result;
+}
+
+
+torrent::Object cmd_string_len(rpc::target_type target, const torrent::Object::list_type& args) {
+ std::mbstate_t mbs = std::mbstate_t();
+ std::string text = string_get_first_arg("len", args);
+ const char* pos = text.c_str();
+ int glyphs = 0, bytes = 0, skip;
+
+ while (*pos && (skip = std::mbrlen(pos, text.length() - bytes, &mbs)) > 0) {
+ pos += skip;
+ bytes += skip;
+ ++glyphs;
+ }
+
+ return (int64_t) glyphs;
+}
+
+
+torrent::Object cmd_string_join(rpc::target_type target, const torrent::Object::list_type& args) {
+ std::string delim = string_get_first_arg("join", args);
+ std::string result;
+ torrent::Object::list_const_iterator first = args.begin() + 1, last = args.end();
+
+ for (torrent::Object::list_const_iterator itr = first; itr != last; ++itr) {
+ if (itr != first) result += delim;
+ rpc::print_object_std(&result, &*itr, 0);
+ }
+
+ return result;
+}
+
+
+torrent::Object cmd_string_strip(int where, const torrent::Object::list_type& args) {
+ std::string text = string_get_first_arg("[lr]strip", args);
+ torrent::Object::list_const_iterator first = args.begin() + 1, last = args.end();
+
+ if (args.size() == 1) {
+ // Strip whitespace
+ if (where <= 0) {
+ text.erase(text.begin(),
+ std::find_if(text.begin(), text.end(),
+ std::not1(std::ptr_fun<int, int>(std::isspace))));
+ }
+ if (where >= 0) {
+ text.erase(std::find_if(text.rbegin(), text.rend(),
+ std::not1(std::ptr_fun<int, int>(std::isspace))).base(),
+ text.end());
+ }
+ } else {
+ size_t lpos = 0, rpos = text.length();
+ bool changed;
+ do {
+ changed = false;
+ for (torrent::Object::list_const_iterator itr = first; itr != last; ++itr) {
+ const std::string& strippable = itr->as_string();
+ if (strippable.empty()) continue;
+
+ bool found;
+ do {
+ found = false;
+
+ if (where <= 0) {
+ if (0 == strncmp(text.c_str() + lpos, strippable.c_str(), strippable.length())) {
+ lpos += strippable.length();
+ changed = found = true;
+ }
+ }
+ if (where >= 0 && lpos <= rpos - strippable.length()) {
+ if (0 == strncmp(text.c_str() + rpos - strippable.length(), strippable.c_str(), strippable.length())) {
+ rpos -= strippable.length();
+ changed = found = true;
+ }
+ }
+ } while (found && lpos < rpos);
+ }
+ } while (changed && lpos < rpos);
+ text = lpos < rpos ? text.substr(lpos, rpos - lpos) : "";
+ }
+
+ return text;
+}
+
+
+torrent::Object cmd_string_pad(bool at_end, const torrent::Object::list_type& args) {
+ std::string text;
+ if (args.size() > 0 && args.begin()->is_value()) {
+ char buf[65];
+ snprintf(buf, sizeof(buf), "%ld", (long)args.begin()->as_value());
+ text = buf;
+ } else {
+ text = string_get_first_arg("[lr]pad", args);
+ }
+
+ torrent::Object::list_const_iterator itr = args.begin() + 1;
+ int64_t pad_len = 0;
+ std::string filler;
+ if (itr != args.end()) pad_len = string_get_value_arg("[lr]pad(pad_len)", itr);
+ if (itr != args.end()) filler = (itr++)->as_string();
+ if (pad_len < 0)
+ throw torrent::input_error("string.[lr]pad: Invalid negative padding length!");
+ if (filler.empty()) filler = " ";
+ size_t text_len = u8_length(text), filler_len = u8_length(filler);
+
+ if (size_t(pad_len) > text_len) {
+ std::string pad;
+ size_t count = size_t(pad_len) - text_len;
+
+ if (filler.length() == 1) { // optimize the common case
+ pad.insert(0, count, filler.at(0));
+ } else while (count > 0) {
+ if (count >= filler_len) {
+ pad += filler;
+ count -= filler_len;
+ } else {
+ pad += u8_chop(filler, count);
+ count = 0;
+ }
+ }
+
+ return at_end ? text + pad : pad + text;
+ }
+
+ return text;
+}
+
+
+torrent::Object cmd_string_split(rpc::target_type target, const torrent::Object::list_type& args) {
+ const std::string text = string_get_first_arg("split", args);
+ if (args.size() != 2 || !args.rbegin()->is_string()) {
+ throw torrent::input_error("string.split needs a string argument.1!");
+ }
+ const std::string delim = args.rbegin()->as_string();
+ torrent::Object result = torrent::Object::create_list();
+ torrent::Object::list_type& resultList = result.as_list();
+
+ if (delim.length()) {
+ size_t pos = 0, next = 0;
+
+ while ((next = text.find(delim, pos)) != std::string::npos) {
+ resultList.push_back(text.substr(pos, next - pos));
+ pos = next + delim.length();
+ }
+ resultList.push_back(text.substr(pos));
+ } else {
+ std::mbstate_t mbs = std::mbstate_t();
+ const char* cpos = text.c_str();
+ int bytes = 0, skip;
+
+ while (*cpos && (skip = std::mbrlen(cpos, text.length() - bytes, &mbs)) > 0) {
+ resultList.push_back(std::string(cpos, skip));
+ cpos += skip;
+ bytes += skip;
+ }
+ }
+
+ return result;
+}
+
+
+torrent::Object cmd_string_substr(rpc::target_type target, const torrent::Object::list_type& args) {
+ const std::string text = string_get_first_arg("substr", args);
+
+ torrent::Object::list_const_iterator itr = args.begin() + 1;
+ int64_t glyphs = 0, count = text.length();
+ std::string fallback;
+ if (itr != args.end()) glyphs = string_get_value_arg("substr(pos)", itr);
+ if (itr != args.end()) count = string_get_value_arg("substr(count)", itr);
+ if (itr != args.end()) fallback = (itr++)->as_string();
+
+ if (count < 0) {
+ throw torrent::input_error("string.substr: Invalid negative count!");
+ }
+
+ std::mbstate_t mbs = std::mbstate_t();
+ const char* pos = text.c_str();
+ int bytes = 0, skip;
+
+ if (glyphs < 0) {
+ std::string::size_type offsets[text.length() + 1];
+ int64_t idx = 0;
+ while (*pos && (skip = std::mbrlen(pos, text.length() - bytes, &mbs)) > 0) {
+ offsets[idx++] = bytes;
+ pos += skip;
+ bytes += skip;
+ }
+ offsets[idx] = bytes;
+
+ int64_t begidx = std::max(idx + glyphs, (int64_t) 0);
+ int64_t endidx = std::min(idx, begidx + count);
+ return text.substr(offsets[begidx], offsets[endidx] - offsets[begidx]);
+ }
+
+ while (glyphs-- > 0 && *pos && (skip = std::mbrlen(pos, text.length() - bytes, &mbs)) > 0) {
+ pos += skip;
+ bytes += skip;
+ }
+ if (!*pos) return fallback;
+
+ int bytes_pos = bytes, bytes_count = 0;
+ while (count-- > 0 && *pos && (skip = std::mbrlen(pos, text.length() - bytes, &mbs)) > 0) {
+ pos += skip;
+ bytes += skip;
+ bytes_count += skip;
+ }
+
+ return text.substr(bytes_pos, bytes_count);
+}
+
+
+torrent::Object cmd_string_shorten(rpc::target_type target, const torrent::Object::list_type& args) {
+ const std::string text = string_get_first_arg("shorten", args);
+
+ torrent::Object::list_const_iterator itr = args.begin() + 1;
+ int64_t u8len = u8_length(text), maxlen = u8len, tail = 5;
+ if (itr != args.end()) maxlen = string_get_value_arg("shorten(maxlen)", itr);
+ if (itr != args.end()) tail = string_get_value_arg("shorten(tail)", itr);
+
+ if (maxlen < 0 || tail < 0) {
+ throw torrent::input_error("string.shorten: Invalid negative maximal or tail length!");
+ }
+
+ if (!maxlen) return std::string();
+ if (u8len <= maxlen) return text;
+
+ int64_t head = std::max(int64_t(0), std::min(u8len, maxlen - tail - 1));
+ if (2*tail >= maxlen) {
+ tail = (maxlen - 1) / 2;
+ head = maxlen - tail - 1;
+ }
+
+ std::mbstate_t mbs = std::mbstate_t();
+ const char* pos = text.c_str();
+ int bytes = 0, skip;
+ while (head-- > 0 && *pos && (skip = std::mbrlen(pos, text.length() - bytes, &mbs)) > 0) {
+ pos += skip;
+ bytes += skip;
+ }
+ std::string::size_type head_bytes = bytes;
+ std::string::size_type tail_bytes = bytes;
+
+ std::string::size_type offsets[text.length() + 1];
+ int64_t idx = 0;
+ while (*pos && (skip = std::mbrlen(pos, text.length() - bytes, &mbs)) > 0) {
+ offsets[idx++] = bytes;
+ pos += skip;
+ bytes += skip;
+ }
+ offsets[idx] = bytes;
+ if (tail <= idx) tail_bytes = offsets[idx - tail];
+
+ return text.substr(0, head_bytes) +
+ (head + tail < u8len ? "…" : "") +
+ (tail ? text.substr(tail_bytes) : "");
+}
+
+
+torrent::Object::value_type apply_string_contains(bool ignore_case, const torrent::Object::list_type& args) {
+ if (args.size() < 2) {
+ throw torrent::input_error("string.contains[_i] takes at least two arguments!");
+ }
+
+ torrent::Object::list_const_iterator itr = args.begin();
+ std::string text = itr->as_string();
+ if (ignore_case)
+ std::transform(text.begin(), text.end(), text.begin(), ::tolower);
+
+ for (++itr; itr != args.end(); ++itr) {
+ std::string substr = itr->as_string();
+ if (ignore_case)
+ std::transform(substr.begin(), substr.end(), substr.begin(), ::tolower);
+ if (substr.empty() || text.find(substr) != std::string::npos)
+ return 1;
+ }
+
+ return 0;
+}
+
+
+torrent::Object cmd_string_contains(rpc::target_type target, const torrent::Object::list_type& args) {
+ return apply_string_contains(false, args);
+}
+
+// XXX: Will NOT work correctly for non-ASCII strings!
+torrent::Object cmd_string_contains_i(rpc::target_type target, const torrent::Object::list_type& args) {
+ return apply_string_contains(true, args);
+}
+
+
+torrent::Object apply_string_mutate(int operation, const torrent::Object::list_type& args) {
+ if (args.size() < 1) {
+ throw torrent::input_error("string.* takes at least a string!");
+ }
+
+ torrent::Object::list_const_iterator itr = args.begin();
+ std::string result = itr->as_string();
+
+ for (++itr; itr != args.end(); ++itr) {
+ std::string needle = itr->as_list().begin()->as_string();
+ std::string subst = itr->as_list().rbegin()->as_string();
+
+ switch (operation) {
+ case 1:
+ if (result == needle)
+ result = subst;
+ break;
+ case 2:
+ for (size_t pos = 0; (pos = result.find(needle, pos)) != std::string::npos; pos += subst.length()) {
+ result.replace(pos, needle.length(), subst);
+ }
+ break;
+ }
+ }
+
+ return result;
+}
+
+torrent::Object cmd_string_map(rpc::target_type target, const torrent::Object::list_type& args) {
+ return apply_string_mutate(1, args);
+}
+
+torrent::Object cmd_string_replace(rpc::target_type target, const torrent::Object::list_type& args) {
+ return apply_string_mutate(2, args);
+}
+
+
+torrent::Object cmd_string_compare(int mode, const torrent::Object::list_type& args) {
+ const char* opnames[] = {"equals", "startswith", "endswith"};
+ if (args.size() < 2) {
+ throw torrent::input_error("string." + std::string(opnames[mode]) + " takes at least two arguments!");
+ }
+
+ std::string value = string_get_first_arg(opnames[mode], args);
+ torrent::Object::list_const_iterator first = args.begin() + 1, last = args.end();
+
+ for (torrent::Object::list_const_iterator itr = first; itr != last; ++itr) {
+ const std::string& cmp = itr->as_string();
+ switch (mode) {
+ case 0:
+ if (value == cmp) return (int64_t) 1;
+ break;
+ case 1:
+ if (value.substr(0, cmp.length()) == cmp) return (int64_t) 1;
+ break;
+ case 2:
+ if (value.length() >= cmp.length() && value.substr(value.length() - cmp.length()) == cmp) return (int64_t) 1;
+ break;
+ default:
+ throw torrent::input_error("string comparison: internal error (unknown mode)");
+ }
+ }
+
+ return (int64_t) 0;
+}
+
+
+torrent::Object cmd_array_at(rpc::target_type target, const torrent::Object::list_type& args) {
+ if (args.size() != 2) {
+ throw torrent::input_error("array.at takes at exactly two arguments!");
+ }
+
+ torrent::Object::list_const_iterator itr = args.begin();
+ torrent::Object::list_type array = (itr++)->as_list();
+ torrent::Object::value_type index = (itr++)->as_value();
+
+ if (array.empty()) {
+ throw torrent::input_error("array.at: array is empty!");
+ }
+ if (index < 0 || int(array.size()) <= index) {
+ throw torrent::input_error("array.at: index out of bounds!");
+ }
+
+ return array.at(index);
+}
+
+
+torrent::Object cmd_array_size(rpc::target_type target, const torrent::Object::list_type& args) {
+ if (args.size() != 1) {
+ throw torrent::input_error("array.size takes exactly one argument!");
+ }
+
+ torrent::Object::list_const_iterator itr = args.begin();
+ torrent::Object::list_type array = (itr)->as_list();
+
+ return int(array.size());
+}
+
+
+void add_capability(const char* name) {
+ system_capabilities.insert(name);
+}
+
+
+torrent::Object cmd_system_has(const torrent::Object::string_type& arg) {
+ if (arg.empty()) {
+ throw torrent::input_error("Passed empty string to 'system.has'!");
+ }
+
+ bool result = (system_capabilities.count(arg) != 0);
+ if (!result && '=' == arg.at(arg.size()-1)) {
+ result = rpc::commands.has(arg.substr(0, arg.size()-1));
+ }
+ return (int64_t) result;
+}
+
+
+torrent::Object cmd_system_has_list() {
+ torrent::Object result = torrent::Object::create_list();
+ torrent::Object::list_type& resultList = result.as_list();
+
+ for (std::set<std::string>::const_iterator itr = system_capabilities.begin(); itr != system_capabilities.end(); itr++) {
+ resultList.push_back(*itr);
+ }
+
+ return result;
+}
+
+
+torrent::Object cmd_system_has_methods(bool filter_public) {
+ torrent::Object result = torrent::Object::create_list();
+ torrent::Object::list_type& resultList = result.as_list();
+
+ for (rpc::CommandMap::const_iterator itr = rpc::commands.begin(), last = rpc::commands.end(); itr != last; itr++) {
+ if (bool(itr->second.m_flags & rpc::CommandMap::flag_public_xmlrpc) == filter_public) {
+ resultList.push_back(itr->first);
+ }
+ }
+
+ return result;
+}
+
+
+torrent::Object cmd_system_client_version_as_value() {
+ int64_t result = 0;
+ const char* pos = PACKAGE_VERSION;
+
+ while (*pos) {
+ result = 100 * result + strtol(pos, (char**)&pos, 10);
+ if (*pos && *pos != '.')
+ throw torrent::input_error("INTERNAL ERROR: Bad version " PACKAGE_VERSION);
+ if (*pos) ++pos;
+ }
+ return result;
+}
+
+
+#if RT_HEX_VERSION <= 0x000907
+// this is merged into 0.9.8 mainline!
+torrent::Object cmd_value(rpc::target_type target, const torrent::Object::list_type& args) {
+ if (args.size() < 1) {
+ throw torrent::input_error("'value' takes at least a number argument!");
+ }
+ if (args.size() > 2) {
+ throw torrent::input_error("'value' takes at most two arguments!");
+ }
+
+ torrent::Object::value_type val = 0;
+ if (args.front().is_value()) {
+ val = args.front().as_value();
+ } else {
+ int base = args.size() > 1 ? args.back().is_value() ?
+ args.back().as_value() : strtol(args.back().as_string().c_str(), NULL, 10) : 10;
+ char* endptr = 0;
+
+ val = strtoll(args.front().as_string().c_str(), &endptr, base);
+ while (*endptr == ' ' || *endptr == '\n') ++endptr;
+ if (*endptr) {
+ throw torrent::input_error("Junk at end of number: " + args.front().as_string());
+ }
+ }
+
+ return val;
+}
+#endif
+
+
+torrent::Object cmd_d_tracker_domain(core::Download* download) {
+ return get_active_tracker_domain(download->download());
+}
+
+
+torrent::Object cmd_d_tracker_scrape_info(const int operation, core::Download* download) {
+ return get_active_tracker_scrape_info(operation, download->download());
+}
+
+
+torrent::Object cmd_d_parent_dir(core::Download* download) {
+ return get_parent_dir(download);
+}
+
+
+#if RT_HEX_VERSION <= 0x000906
+// https://github.com/rakshasa/rtorrent/commit/1f5e4d37d5229b63963bb66e76c07ec3e359ecba
+torrent::Object cmd_system_env(const torrent::Object::string_type& arg) {
+ if (arg.empty()) {
+ throw torrent::input_error("system.env: Missing variable name.");
+ }
+
+ char* val = getenv(arg.c_str());
+ return std::string(val ? val : "");
+}
+
+// https://github.com/rakshasa/rtorrent/commit/30d8379391ad4cb3097d57aa56a488d061e68662
+torrent::Object cmd_ui_current_view() {
+ ui::DownloadList* dl = control->ui()->download_list();
+ core::View* view = dl ? dl->current_view() : 0;
+ return view ? view->name() : std::string();
+}
+#endif
+
+
+void initialize_command_pyroscope() {
+ /*
+ *_ANY – no arguments (signature `cmd_*()`)
+ *_ANY_P – the 'P' means 'private'
+ *_STRING – takes (one?) string argument
+ *_LIST – takes any number of arguments
+ *_DL, *_DL_LIST – function gets a `core::Download*` as first parameter
+ *_VAR_VALUE – define a value, with getter and setter, and a default
+ */
+
+#if RT_HEX_VERSION <= 0x000906
+ // these are merged into 0.9.7 mainline!
+ CMD2_ANY_STRING("system.env", _cxxstd_::bind(&cmd_system_env, _cxxstd_::placeholders::_2));
+ CMD2_ANY("ui.current_view", _cxxstd_::bind(&cmd_ui_current_view));
+#endif
+
+#if RT_HEX_VERSION <= 0x000907
+ // this is merged into 0.9.8 mainline!
+ CMD2_ANY_LIST("d.multicall.filtered", _cxxstd_::bind(&d_multicall_filtered, _cxxstd_::placeholders::_2));
+#endif
+
+#if RT_HEX_VERSION <= 0x000909
+ // will be merged into 0.9.9+ mainline?
+ CMD2_ANY_LIST("system.random", &apply_random);
+#endif
+
+ // string.* group
+ CMD2_ANY_LIST("string.len", &cmd_string_len);
+ CMD2_ANY_LIST("string.join", &cmd_string_join);
+ CMD2_ANY_LIST("string.split", &cmd_string_split);
+ CMD2_ANY_LIST("string.substr", &cmd_string_substr);
+ CMD2_ANY_LIST("string.shorten", &cmd_string_shorten);
+ CMD2_ANY_LIST("string.contains", &cmd_string_contains);
+ CMD2_ANY_LIST("string.contains_i", &cmd_string_contains_i);
+ CMD2_ANY_LIST("string.map", &cmd_string_map);
+ CMD2_ANY_LIST("string.replace", &cmd_string_replace);
+ CMD2_ANY_LIST("string.equals", std::bind(&cmd_string_compare, 0, std::placeholders::_2));
+ CMD2_ANY_LIST("string.startswith", std::bind(&cmd_string_compare, 1, std::placeholders::_2));
+ CMD2_ANY_LIST("string.endswith", std::bind(&cmd_string_compare, 2, std::placeholders::_2));
+ CMD2_ANY_LIST("string.strip", std::bind(&cmd_string_strip, 0, std::placeholders::_2));
+ CMD2_ANY_LIST("string.lstrip", std::bind(&cmd_string_strip, -1, std::placeholders::_2));
+ CMD2_ANY_LIST("string.rstrip", std::bind(&cmd_string_strip, 1, std::placeholders::_2));
+ CMD2_ANY_LIST("string.lpad", std::bind(&cmd_string_pad, false, std::placeholders::_2));
+ CMD2_ANY_LIST("string.rpad", std::bind(&cmd_string_pad, true, std::placeholders::_2));
+
+ // array.* group
+ CMD2_ANY_LIST("array.at", &cmd_array_at);
+ CMD2_ANY_LIST("array.size", &cmd_array_size);
+
+ // ui.focus.* – quick paging
+ CMD2_ANY("ui.focus.home", _cxxstd_::bind(&cmd_ui_focus_home));
+ CMD2_ANY("ui.focus.end", _cxxstd_::bind(&cmd_ui_focus_end));
+ CMD2_ANY("ui.focus.pgup", _cxxstd_::bind(&cmd_ui_focus_pgup));
+ CMD2_ANY("ui.focus.pgdn", _cxxstd_::bind(&cmd_ui_focus_pgdn));
+ CMD2_VAR_VALUE("ui.focus.page_size", 50);
+
+ // system.has.*
+ CMD2_ANY_STRING("system.has", _cxxstd_::bind(&cmd_system_has, _cxxstd_::placeholders::_2));
+ CMD2_ANY("system.has.list", _cxxstd_::bind(&cmd_system_has_list));
+ CMD2_ANY("system.has.private_methods", _cxxstd_::bind(&cmd_system_has_methods, false));
+ CMD2_ANY("system.has.public_methods", _cxxstd_::bind(&cmd_system_has_methods, true));
+ CMD2_ANY("system.client_version.as_value", _cxxstd_::bind(&cmd_system_client_version_as_value));
+
+ // d.custom.* extensions
+#if RT_HEX_VERSION <= 0x000907
+ // this is merged into 0.9.8 mainline!
+ CMD2_DL_LIST("d.custom.if_z", _cxxstd_::bind(&retrieve_d_custom_if_z,
+ _cxxstd_::placeholders::_1, _cxxstd_::placeholders::_2));
+#endif
+ CMD2_DL_LIST("d.custom.set_if_z", _cxxstd_::bind(&cmd_d_custom_set_if_z,
+ _cxxstd_::placeholders::_1, _cxxstd_::placeholders::_2));
+ CMD2_DL_LIST("d.custom.erase", _cxxstd_::bind(&cmd_d_custom_erase,
+ _cxxstd_::placeholders::_1, _cxxstd_::placeholders::_2));
+#if RT_HEX_VERSION <= 0x000907
+ // these are merged into 0.9.8 mainline!
+ CMD2_DL_LIST("d.custom.keys", _cxxstd_::bind(&retrieve_d_custom_map,
+ _cxxstd_::placeholders::_1, true, _cxxstd_::placeholders::_2));
+ CMD2_DL_LIST("d.custom.items", _cxxstd_::bind(&retrieve_d_custom_map,
+ _cxxstd_::placeholders::_1, false, _cxxstd_::placeholders::_2));
+#endif
+ CMD2_DL_STRING("d.custom.toggle", _cxxstd_::bind(&cmd_d_custom_toggle,
+ _cxxstd_::placeholders::_1, _cxxstd_::placeholders::_2));
+ CMD2_DL_STRING("d.custom.as_value", _cxxstd_::bind(&retrieve_d_custom_as_value,
+ _cxxstd_::placeholders::_1, _cxxstd_::placeholders::_2));
+ // Misc commands
+#if RT_HEX_VERSION <= 0x000907
+ // these are merged into 0.9.8 mainline!
+ CMD2_ANY_LIST("value", &cmd_value);
+ CMD2_ANY_LIST("compare", &apply_compare);
+#endif
+ CMD2_ANY("ui.bind_key", &apply_ui_bind_key);
+ CMD2_VAR_VALUE("ui.bind_key.verbose", 1);
+ CMD2_ANY("throttle.names", _cxxstd_::bind(&cmd_throttle_names));
+ CMD2_DL("d.tracker_domain", _cxxstd_::bind(&cmd_d_tracker_domain, _cxxstd_::placeholders::_1));
+ CMD2_DL("d.tracker_scrape.downloaded", _cxxstd_::bind(&cmd_d_tracker_scrape_info, 1, _cxxstd_::placeholders::_1));
+ CMD2_DL("d.tracker_scrape.complete", _cxxstd_::bind(&cmd_d_tracker_scrape_info, 2, _cxxstd_::placeholders::_1));
+ CMD2_DL("d.tracker_scrape.incomplete", _cxxstd_::bind(&cmd_d_tracker_scrape_info, 3, _cxxstd_::placeholders::_1));
+ CMD2_DL("d.parent_dir", _cxxstd_::bind(&cmd_d_parent_dir, _cxxstd_::placeholders::_1));
+
+ CMD2_ANY_STRING("log.messages", _cxxstd_::bind(&cmd_log_messages, _cxxstd_::placeholders::_2));
+ CMD2_ANY_P("import.return", &cmd_import_return);
+ CMD2_ANY("do", _cxxstd_::bind(&cmd_do, _cxxstd_::placeholders::_1, _cxxstd_::placeholders::_2));
+
+ // List capabilities of this build
+ add_capability("system.has"); // self
+ add_capability("rtorrent-ps"); // obvious
+ add_capability("colors"); // not monochrome
+ add_capability("canvas_v2"); // new PS 1.1 canvas with fully dynamic columns
+ add_capability("collapsed-views"); // pre-collapsed views
+ add_capability("fixed-log-xmlrpc-close");
+}
diff --git a/ps-import.return_all.patch b/ps-import.return_all.patch
new file mode 100644
index 000000000000..79a804e8cfc2
--- /dev/null
+++ b/ps-import.return_all.patch
@@ -0,0 +1,10 @@
+--- a/src/rpc/parse_commands.cc
++++ b/src/rpc/parse_commands.cc
+@@ -226,6 +226,7 @@ parse_command_file(const std::string& path) {
+ }
+
+ } catch (torrent::input_error& e) {
++ if (!strcmp(e.what(), "import.return")) return true;
+ snprintf(buffer, 2048, "Error in option file: %s:%u: %s", path.c_str(), lineNumber, e.what());
+
+ throw torrent::input_error(buffer);
diff --git a/ps-include-timestamps_all.patch b/ps-include-timestamps_all.patch
new file mode 100644
index 000000000000..0916750483e8
--- /dev/null
+++ b/ps-include-timestamps_all.patch
@@ -0,0 +1,18 @@
+--- a/src/main.cc 2017-06-10 09:33:10.387844804 +0100
++++ b/src/main.cc 2018-04-16 19:28:46.339181188 +0100
+@@ -283,6 +283,15 @@ main(int argc, char** argv) {
+ "method.set_key = event.download.finished, !_timestamp, ((d.timestamp.finished.set, ((system.time)) ))\n"
+ "method.set_key = event.download.hash_done, !_timestamp, {(branch,((d.complete)),((d.timestamp.finished.set_if_z,(system.time))))}\n"
+
++ // EVENTS: Timestamp 'tm_completed' (time of completion)
++ "method.set_key = event.download.finished, !tm_completed, ((d.custom.set, tm_completed, ((cat,((system.time)))) ))\n"
++ "method.set_key = event.download.hash_done, !tm_completed, {(branch, ((and,((d.complete)),((not,((d.custom,tm_completed)))))), ((d.custom.set, tm_completed, (cat,(system.time)))) )}\n"
++
++ // EVENTS/SCHEDULE: Timestamp 'last_active' for items that have peers
++ "method.set_key = event.download.finished, !tm_last_active, ((d.custom.set, last_active, ((cat,((system.time)))) ))\n"
++ "method.set_key = event.download.resumed, !tm_last_active, {(branch, ((or,((d.peers_connected)),((not,((d.custom,last_active)))))), ((d.custom.set, last_active, (cat,(system.time)))) )}\n"
++ "schedule2 = pyro_update_last_active, 24, 42, ((d.multicall2, started, \"branch=((d.peers_connected)),((d.custom.set, last_active, (cat,(system.time))))\" ))\n"
++
+ "method.insert.c_simple = group.insert_persistent_view,"
+ "((view.add,((argument.0)))),((view.persistent,((argument.0)))),((group.insert,((argument.0)),((argument.0))))\n"
+
diff --git a/ps-info-pane-is-default_all.patch b/ps-info-pane-is-default_all.patch
new file mode 100644
index 000000000000..925b7debed3a
--- /dev/null
+++ b/ps-info-pane-is-default_all.patch
@@ -0,0 +1,12 @@
+--- a/src/ui/download.cc 2015-09-03 21:03:30.000000000 +0200
++++ b/src/ui/download.cc 2018-05-16 00:24:40.000000000 +0200
+@@ -129,3 +129,3 @@
+
+- element->set_entry(0, false);
++ element->set_entry(1, false); // 'Info' active by default
+
+@@ -216,3 +216,3 @@
+
+- activate_display_menu(DISPLAY_PEER_LIST);
++ activate_display_menu(DISPLAY_INFO); // 'Info' active by default
+ }
diff --git a/ps-issue-515_all.patch b/ps-issue-515_all.patch
new file mode 100644
index 000000000000..d1e37cedbc9e
--- /dev/null
+++ b/ps-issue-515_all.patch
@@ -0,0 +1,29 @@
+--- a/src/rpc/command_scheduler.cc
++++ b/src/rpc/command_scheduler.cc
+@@ -63,15 +63,18 @@ CommandScheduler::insert(const std::string& key) {
+ if (key.empty())
+ throw torrent::input_error("Scheduler received an empty key.");
+
+- iterator itr = find(key);
++ CommandSchedulerItem* current = new CommandSchedulerItem(key);
++ current->slot() = std::bind(&CommandScheduler::call_item, this, current);
+
+- if (itr == end())
+- itr = base_type::insert(end(), NULL);
+- else
+- delete *itr;
+-
+- *itr = new CommandSchedulerItem(key);
+- (*itr)->slot() = std::bind(&CommandScheduler::call_item, this, *itr);
++ iterator itr = find(key);
++ if (itr == end()) {
++ itr = base_type::insert(end(), current);
++ } else {
++ // swap in fully initialized command, and THEN delete the replaced one
++ CommandSchedulerItem* old = *itr;
++ *itr = current;
++ delete old;
++ }
+
+ return itr;
+ }
diff --git a/ps-item-stats-human-sizes_all.patch b/ps-item-stats-human-sizes_all.patch
new file mode 100644
index 000000000000..4d35520d7f9f
--- /dev/null
+++ b/ps-item-stats-human-sizes_all.patch
@@ -0,0 +1,35 @@
+--- rel-0.9.4/src/display/utils.cc 2012-02-14 04:32:01.000000000 +0100
++++ rtorrent-0.9.4/src/display/utils.cc 2015-08-11 04:35:46.000000000 +0200
+@@ -133,4 +133,6 @@
+ }
+
++std::string human_size(int64_t bytes, unsigned int format=0);
++
+ char*
+ print_download_info(char* first, char* last, core::Download* d) {
+@@ -142,15 +144,16 @@
+ first = print_buffer(first, last, " ");
+
++ std::string h_size = human_size(d->download()->file_list()->size_bytes(), 0);
++ std::string h_done = human_size(d->download()->bytes_done(), 0);
+ if (d->is_done())
+- first = print_buffer(first, last, "done %10.1f MB", (double)d->download()->file_list()->size_bytes() / (double)(1 << 20));
++ first = print_buffer(first, last, " done %s ", h_size.c_str());
+ else
+- first = print_buffer(first, last, "%6.1f / %6.1f MB",
+- (double)d->download()->bytes_done() / (double)(1 << 20),
+- (double)d->download()->file_list()->size_bytes() / (double)(1 << 20));
+-
+- first = print_buffer(first, last, " Rate: %5.1f / %5.1f KB Uploaded: %7.1f MB",
+- (double)d->info()->up_rate()->rate() / (1 << 10),
+- (double)d->info()->down_rate()->rate() / (1 << 10),
+- (double)d->info()->up_rate()->total() / (1 << 20));
++ first = print_buffer(first, last, "%s / %s ", h_done.c_str(), h_size.c_str());
++
++ std::string h_up = human_size(d->info()->up_rate()->rate(), 0);
++ std::string h_down = human_size(d->info()->down_rate()->rate(), 0);
++ std::string h_sum = human_size(d->info()->up_rate()->total(), 0);
++ first = print_buffer(first, last, " Rate: %s / %s Uploaded: %s ",
++ h_up.c_str(), h_down.c_str(), h_sum.c_str());
+
+ if (d->download()->info()->is_active() && !d->is_done()) {
diff --git a/ps-log_messages_all.patch b/ps-log_messages_all.patch
new file mode 100644
index 000000000000..045f1d963a9e
--- /dev/null
+++ b/ps-log_messages_all.patch
@@ -0,0 +1,33 @@
+--- a/src/core/manager.cc 2017-04-30 20:32:33.000000000 +0100
++++ b/src/core/manager.cc 2018-04-20 15:45:33.380910446 +0100
+@@ -36,6 +36,7 @@
+
+ #include "config.h"
+
++#include <ctime>
+ #include <cstdio>
+ #include <cstring>
+ #include <fstream>
+@@ -83,6 +84,22 @@ void
+ Manager::push_log(const char* msg) {
+ m_log_important->lock_and_push_log(msg, strlen(msg), 0);
+ m_log_complete->lock_and_push_log(msg, strlen(msg), 0);
++
++ extern int log_messages_fd;
++ if (log_messages_fd >= 0) {
++ char buf[30];
++ time_t t = std::time(0);
++ std::tm* now = std::localtime(&t);
++
++ snprintf(buf, sizeof(buf), "%04u-%02u-%02u %2d:%02d:%02d ",
++ 1900 + now->tm_year, now->tm_mon + 1, now->tm_mday,
++ now->tm_hour, now->tm_min, now->tm_sec);
++
++ std::string line(buf);
++ line += msg;
++ line += '\n';
++ ::write(log_messages_fd, line.c_str(), line.length());
++ }
+ }
+
+ Manager::Manager() :
diff --git a/ps-object_std-map-serialization_all.patch b/ps-object_std-map-serialization_all.patch
new file mode 100644
index 000000000000..b476f84cd700
--- /dev/null
+++ b/ps-object_std-map-serialization_all.patch
@@ -0,0 +1,24 @@
+--- a/src/rpc/parse.cc
++++ b/src/rpc/parse.cc
+@@ -506,6 +506,21 @@ print_object_std(std::string* dest, const torrent::Object* src, int flags) {
+
+ return;
+
++ case torrent::Object::TYPE_MAP:
++ {
++ bool first = true;
++ for (torrent::Object::map_const_iterator itr = src->as_map().begin(), itrEnd = src->as_map().end(); itr != itrEnd; itr++) {
++ if (!first) *dest += ", ";
++ *dest += itr->first;
++ *dest += ": \"";
++ print_object_std(dest, &(itr->second), flags);
++ *dest += '"';
++ first = false;
++ }
++
++ return;
++ }
++
+ case torrent::Object::TYPE_NONE:
+ return;
+ default:
diff --git a/ps-silent-catch_all.patch b/ps-silent-catch_all.patch
new file mode 100644
index 000000000000..7ae1ea945c68
--- /dev/null
+++ b/ps-silent-catch_all.patch
@@ -0,0 +1,18 @@
+--- a/src/command_dynamic.cc
++++ b/src/command_dynamic.cc
+@@ -425,10 +425,14 @@ system_method_list_keys(const torrent::Object::string_type& args) {
+
+ torrent::Object
+ cmd_catch(rpc::target_type target, const torrent::Object& args) {
++ bool silent = (args.is_list()
++ && !args.as_list().empty()
++ && args.as_list().front().is_string()
++ && args.as_list().front().as_string() == "false=");
+ try {
+ return rpc::call_object(args, target);
+ } catch (torrent::input_error& e) {
+- lt_log_print(torrent::LOG_WARN, "Caught exception: '%s'.", e.what());
++ if (!silent) lt_log_print(torrent::LOG_WARN, "Caught exception: '%s'.", e.what());
+ return torrent::Object();
+ }
+ }
diff --git a/ps-ui_pyroscope_all.patch b/ps-ui_pyroscope_all.patch
new file mode 100644
index 000000000000..4fb11b74a31e
--- /dev/null
+++ b/ps-ui_pyroscope_all.patch
@@ -0,0 +1,7 @@
+--- orig/src/rpc/object_storage.h 2011-04-07 09:44:06.000000000 +0200
++++ rtorrent-0.8.8/src/rpc/object_storage.h 2011-06-05 13:20:24.000000000 +0200
+@@ -124,2 +124,4 @@
+
++ const torrent::Object& set_color_string(const torrent::raw_string& key, const std::string& object);
++
+ // Functions callers:
diff --git a/pyroscope_all.patch b/pyroscope_all.patch
new file mode 100644
index 000000000000..38ff3a5bdfac
--- /dev/null
+++ b/pyroscope_all.patch
@@ -0,0 +1,26 @@
+--- rtorrent-0.8.6/src/ui/download_list.h 2011-05-03 04:05:34.000000000 +0200
++++ rtorrent-0.8.6/src/ui/download_list.h,pyro 2011-05-03 04:11:23.000000000 +0200
+@@ -99,6 +99,7 @@
+ void disable();
+
+ void activate_display(Display d);
++ ElementBase* display(Display d) { return d < DISPLAY_MAX_SIZE ? m_uiArray[d] : 0; }
+
+ core::View* current_view();
+ void set_current_view(const std::string& name);
+--- rtorrent-0.8.7/src/command_helpers.cc.orig 2010-06-26 14:05:08.000000000 +0200
++++ rtorrent-0.8.7/src/command_helpers.cc 2011-05-06 19:42:58.000000000 +0200
+@@ -50,2 +50,3 @@
+ void initialize_command_local();
++void initialize_command_pyroscope();
+ void initialize_command_network();
+@@ -61,2 +62,3 @@
+ initialize_command_local();
++ initialize_command_pyroscope();
+ initialize_command_ui();
+--- rtorrent-0.8.7/src/Makefile.am.orig 2010-06-26 14:05:08.000000000 +0200
++++ rtorrent-0.8.7/src/Makefile.am 2011-05-06 19:40:03.000000000 +0200
+@@ -23,2 +23,3 @@
+ command_ui.cc \
++ command_pyroscope.cc \
+ control.cc \
diff --git a/ui_pyroscope.cc b/ui_pyroscope.cc
new file mode 100644
index 000000000000..d82014227dfd
--- /dev/null
+++ b/ui_pyroscope.cc
@@ -0,0 +1,1445 @@
+/*
+ ⋅ ” ’ ♯ ☢ ☍ ⌘ ✰ ⋮ ☯ ⚑ ↺ ⤴ ⤵ ∆ ∇ ⚠ ◔ ↯ ¿ ⨂ ✖ ⇣ ⇡ ▹ ╍ ▪ ⚯ ⚒ ◌ ⇅ ↡ ↟ ⊛ ♺ ⋆ … ⇳ ⌈ ⌉ ⌊ ⌋ ⊘
+ ∞ ↨ ❢ ʘ ⇕ ⋫ ☡ ↕ ℞ ⟲ ◷ Σ ⇈ ✔ ⛁ ☹ ➀ ➁ ➂ ➃ ➄ ➅ ➆ ➇ ➈ ➉ ⠁ ⠉ ⠋ ⠛ ⠟ ⠿ ⡿ ⣿ ❚ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █
+
+python -c 'print u"\u22c5 \u201d \u2019 \u266f \u2622 \u260d \u2318 \u2730 " \
+ u"\u22ee \u262f \u2691 \u21ba \u2934 \u2935 \u2206 \u2207 \u26a0 \u25d4 " \
+ u"\u21af \u00bf \u2a02 \u2716 \u21e3 \u21e1 \u25b9 \u254d \u25aa \u26af " \
+ u"\u2692 \u25cc \u21c5 \u21a1 \u219f \u229b \u267a \u22c6 \u2026 \u21f3 " \
+ u"\u2308 \u2309 \u230a \u230b \u2298 \u221e \u21a8 \u2762 \u0298 \u21d5 " \
+ u"\u22eb \u2621 \u2195 \u211e \u27f2 \u25f7 \u03a3 \u21c8 \u2714 \u26c1 " \
+ u"\u2639 \u2780 \u2781 \u2782 \u2783 \u2784 \u2785 \u2786 \u2787 \u2788 \u2789 " \
+ u"\u2801 \u2809 \u280b \u281b \u281f \u283f \u287f \u28ff \u275a " \
+ u"\u2581 \u2582 \u2583 \u2584 \u2585 \u2586 \u2587 \u2588 ".encode("utf8")'
+*/
+
+#include "ui_pyroscope.h"
+
+#include "config.h"
+#include "globals.h"
+
+#include <climits>
+#include <cstdio>
+#include <cwchar>
+#include <set>
+#include <list>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <rak/algorithm.h>
+
+#include "core/view.h"
+#include "core/manager.h"
+#include "core/download.h"
+#include "torrent/tracker.h"
+#include "torrent/rate.h"
+#include "display/window.h"
+#include "display/canvas.h"
+#include "display/utils.h"
+#include "ui/root.h"
+#include "ui/download_list.h"
+
+#include "control.h"
+#include "command_helpers.h"
+
+#if (RT_HEX_VERSION <= 0x000906)
+ #define _cxxstd_ tr1
+#else
+ #define _cxxstd_ std
+#endif
+
+#define D_INFO(item) (item->info())
+#include "rpc/object_storage.h"
+#include "rpc/parse.h"
+
+// from command_pyroscope.cc
+extern torrent::Tracker* get_active_tracker(torrent::Download* item);
+extern std::string get_active_tracker_domain(torrent::Download* item);
+
+#define CANVAS_POS_1ST_ITEM 2
+#define X_OF_Y_CANVAS_MIN_WIDTH 28
+#define NAME_RESERVED_WIDTH 6
+#define TRACKER_LABEL_WIDTH 20
+
+// definition from display/window_download_list.cc that is not in the header file
+typedef std::pair<core::View::iterator, core::View::iterator> Range;
+
+// display attribute map (normal, even, odd)
+static unsigned long attr_map[3 * ps::COL_MAX] = {0};
+
+// color indices for progress indication
+int ratio_col[] = {
+ ps::COL_PROGRESS0, ps::COL_PROGRESS20, ps::COL_PROGRESS40, ps::COL_PROGRESS60, ps::COL_PROGRESS80,
+ ps::COL_PROGRESS100, ps::COL_PROGRESS120,
+};
+
+// ps::COL_PRIO
+static int col_idx_prio[] = {
+ ps::COL_PROGRESS0, ps::COL_PROGRESS60, ps::COL_INFO, ps::COL_PROGRESS120
+};
+
+// ps::COL_STATE
+static int col_idx_state[] = {
+ ps::COL_PROGRESS0, ps::COL_PROGRESS0, ps::COL_PROGRESS80, ps::COL_PROGRESS100
+};
+
+// ps::COL_UNSAFE_DATA
+static int col_idx_unsafe[] = {
+ ps::COL_PROGRESS100, ps::COL_PROGRESS80, ps::COL_PROGRESS40
+};
+
+// ps::COL_THROTTLE_CH
+static int col_idx_throttle_ch[] = {
+ ps::COL_PROGRESS0, ps::COL_PROGRESS20, ps::COL_PROGRESS60, ps::COL_PROGRESS100
+};
+
+// basic color names
+static const char* color_names[] = {
+ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"
+};
+
+// color value for custom column rendering
+static std::string ui_canvas_color;
+
+// list of color configuration variables, the order MUST correspond to the ColorKind enum
+static const char* color_vars[ps::COL_MAX] = {
+ 0,
+ "ui.color.custom1",
+ "ui.color.custom2",
+ "ui.color.custom3",
+ "ui.color.custom4",
+ "ui.color.custom5",
+ "ui.color.custom6",
+ "ui.color.custom7",
+ "ui.color.custom8",
+ "ui.color.custom9",
+ "ui.color.progress0", // 10
+ "ui.color.progress20",
+ "ui.color.progress40",
+ "ui.color.progress60",
+ "ui.color.progress80",
+ "ui.color.progress100",
+ "ui.color.progress120",
+ "ui.color.title",
+ "ui.color.footer",
+ "ui.color.focus",
+ "ui.color.label", // 20
+ "ui.color.info",
+ "ui.color.alarm",
+ "ui.color.complete",
+ "ui.color.seeding",
+ "ui.color.stopped",
+ "ui.color.queued",
+ "ui.color.incomplete",
+ "ui.color.leeching",
+ "ui.color.odd",
+ "ui.color.even",
+};
+
+// collapsed state of views (default is false)
+static std::map<std::string, bool> is_collapsed;
+
+// tracker aliases map
+typedef std::map<std::string, std::string> string_kv_map;
+static string_kv_map tracker_aliases;
+
+// Traffic history
+static int network_history_depth = 0;
+static uint32_t network_history_count = 0;
+static uint32_t* network_history_up = 0;
+static uint32_t* network_history_down = 0;
+static std::string network_history_up_str;
+static std::string network_history_down_str;
+
+
+// get custom field contaioning a long (time_t)
+unsigned long get_custom_long(core::Download* d, const char* name) {
+ try {
+ return atol(d->bencode()->get_key("rtorrent").get_key("custom").get_key_string(name).c_str());
+ } catch (torrent::bencode_error& e) {
+ return 0UL;
+ }
+}
+
+
+// get custom field contaioning a string
+std::string get_custom_string(core::Download* d, const char* name) {
+ try {
+ return d->bencode()->get_key("rtorrent").get_key("custom").get_key_string(name);
+ } catch (torrent::bencode_error& e) {
+ return "";
+ }
+}
+
+
+// get a value from arg, either parsing from a string, or arg already being a value
+int64_t parse_value_arg(const torrent::Object& arg) {
+ if (arg.is_string()) {
+ int64_t result;
+ rpc::parse_whole_value(arg.as_string().c_str(), &result);
+ return result;
+ }
+ return arg.as_value(); // this will throw if other types than string/value are passed
+}
+
+
+// convert absolute timestamp to approximate human readable time diff (5 chars wide)
+std::string elapsed_time(unsigned long dt, unsigned long t0) {
+ if (dt == 0) return std::string("⋆ ⋆⋆ ");
+
+ const char* unit[] = {"”", "’", "h", "d", "w", "m", "y"};
+ unsigned long threshold[] = {1, 60, 3600, 86400, 7*86400, 30*86400, 365*86400, 0};
+
+ int dim = 0;
+ dt = std::labs((t0 ? t0 : time(NULL)) - dt);
+ if (dt == 0) return std::string("⋅ ⋅⋅ ");
+ while (threshold[dim] && dt >= threshold[dim]) ++dim;
+ if (dim) --dim;
+ float val = float(dt) / float(threshold[dim]);
+
+ char buffer[15];
+ if (val < 10.0 && dim) {
+ snprintf(buffer, sizeof(buffer), "%1d%s%2d%s", int(val), unit[dim],
+ int(dt % threshold[dim] / threshold[dim-1]), unit[dim-1]);
+ } else {
+ snprintf(buffer, sizeof(buffer), "%4d%s", int(val), unit[dim]);
+ }
+ return std::string(buffer);
+}
+
+
+// return 2-digits number, or digit + dimension indicator
+std::string num2(int64_t num) {
+ if (num < 0 || 10*1000*1000 <= num) return std::string("♯♯");
+ if (!num) return std::string(" ⋅");
+
+ char buffer[10];
+ if (num < 100) {
+ snprintf(buffer, sizeof(buffer), "%2d", int(num));
+ } else {
+ // Roman numeral multipliers 10, 100, 1000, 10x1000, 100x1000, 1000x1000
+ const char* roman = " xcmXCM";
+ int dim = 0;
+ while (num > 9) { ++dim; num /= 10; }
+ snprintf(buffer, sizeof(buffer), "%1d%c", int(num), roman[dim]);
+ }
+
+ return std::string(buffer);
+}
+
+
+namespace display {
+
+// Visibility of canvas columns
+static std::set<int> column_hidden;
+
+
+// function wrapper for what possibly is a macro
+static int get_colors() {
+ return COLORS;
+}
+
+
+// format byte size for humans, if format = 0 use 6 chars (one decimal place),
+// if = 1 just print the rounded value (4 chars), if = 2 combine the two formats
+// into 4 chars by rounding for values >= 9.95.
+// set bit 8 of format and 0 values will return a whitespace string of the correct length.
+std::string human_size(int64_t bytes, unsigned int format=0) {
+ if (format & 8 && bytes <= 0) return std::string((format & 7) ? 4 : 6, ' ');
+ format &= 7;
+
+ int exp;
+ char unit;
+
+ if (bytes < (int64_t(1000) << 10)) { exp = 10; unit = 'K'; }
+ else if (bytes < (int64_t(1000) << 20)) { exp = 20; unit = 'M'; }
+ else if (bytes < (int64_t(1000) << 30)) { exp = 30; unit = 'G'; }
+ else { exp = 40; unit = 'T'; }
+
+ char buffer[48];
+ double value = double(bytes) / (int64_t(1) << exp);
+ const char* formats[] = {"%5.1f%c", "%3.0f%c", "%3.1f%c"};
+
+ if (format > 2) format = 0;
+ if (format == 2 and value >= 9.949999) format = 1;
+ if (format == 1) value = int(value + 0.50002);
+ snprintf(buffer, sizeof(buffer), formats[format], value, unit);
+
+ return std::string(buffer);
+}
+
+
+// split a given string into words separated by delim, and add them to the provided vector
+void split(std::vector<std::string>& words, const char* str, char delim = ' ') {
+ do {
+ const char* begin = str;
+ while (*str && *str != delim) str++;
+ words.push_back(std::string(begin, str));
+ } while (*str++);
+}
+
+
+void ui_pyroscope_canvas_init(); // forward
+static bool color_init_recursion = false;
+
+
+// create color map from configuration strings
+void ui_pyroscope_colormap_init() {
+ // if in early startup stage (configuration), then init the screen so we can query system constants
+ if (!get_colors()) {
+ if (color_init_recursion) {
+ color_init_recursion = false;
+ control->core()->push_log("Terminal color initialization failed, does your terminal have none?!");
+ } else {
+ color_init_recursion = true;
+ initscr();
+ ui_pyroscope_canvas_init(); // this calls us again!
+ }
+ return;
+ }
+ color_init_recursion = false;
+
+ // Those hold the background colors of "odd" and "even"
+ int bg_odd = -1;
+ int bg_even = -1;
+
+ // read the definition for basic colors from configuration
+ for (int k = 1; k < ps::COL_MAX; k++) {
+ init_pair(k, -1, -1);
+ std::string col_def = rpc::call_command_string(color_vars[k]);
+ if (col_def.empty()) continue; // use terminal default if definition is empty
+
+ std::vector<std::string> words;
+ split(words, col_def.c_str());
+
+ short col[2] = {-1, -1}; // fg, bg
+ short col_idx = 0; // 0 = fg; 1 = bg
+ short bright = 0;
+ unsigned long attr = A_NORMAL;
+ for (size_t i = 0; i < words.size(); i++) { // look at all the words
+ if (words[i] == "bold") attr |= A_BOLD;
+ else if (words[i] == "standout") attr |= A_STANDOUT;
+ else if (words[i] == "underline") attr |= A_UNDERLINE;
+ else if (words[i] == "reverse") attr |= A_REVERSE;
+ else if (words[i] == "blink") attr |= A_BLINK;
+ else if (words[i] == "dim") attr |= A_DIM;
+ else if (words[i] == "on") { col_idx = 1; bright = 0; } // switch to background color
+ else if (words[i] == "gray") col[col_idx] = bright ? 7 : 8; // bright gray is white
+ else if (words[i] == "bright") bright = 8;
+ else if (words[i].find_first_not_of("0123456789") == std::string::npos) {
+ // handle numeric index
+ short c = -1;
+ sscanf(words[i].c_str(), "%hd", &c);
+ col[col_idx] = c;
+ } else for (short c = 0; c < 8; c++) { // check for basic color names
+ if (words[i] == color_names[c]) {
+ col[col_idx] = bright + c;
+ break;
+ }
+ }
+ }
+
+ // check that fg & bg color index is valid
+ if ((col[0] != -1 && col[0] >= get_colors()) || (col[1] != -1 && col[1] >= get_colors())) {
+ char buf[33];
+ sprintf(buf, "%d", get_colors());
+ throw torrent::input_error(col_def + ": your terminal only supports " + buf + " colors.");
+ }
+
+ // store the parsed color definition
+ attr_map[k] = attr;
+ init_pair(k, col[0], col[1]);
+ if (k == ps::COL_EVEN) bg_even = col[1];
+ if (k == ps::COL_ODD) bg_odd = col[1];
+ }
+
+ // now make copies of the basic colors with the "odd" and "even" definitions mixed in
+ for (int k = 1; k < ps::COL_MAX; k++) {
+ short fg, bg;
+ pair_content(k, &fg, &bg);
+
+ // replace the background color, and mix in the attributes
+ attr_map[k + 1 * ps::COL_MAX] = attr_map[k] | attr_map[ps::COL_EVEN];
+ attr_map[k + 2 * ps::COL_MAX] = attr_map[k] | attr_map[ps::COL_ODD];
+ init_pair(k + 1 * ps::COL_MAX, fg, bg == -1 ? bg_even : bg);
+ init_pair(k + 2 * ps::COL_MAX, fg, bg == -1 ? bg_odd : bg);
+ }
+}
+
+
+// add color handling to canvas initialization
+void ui_pyroscope_canvas_init() {
+ start_color();
+ use_default_colors();
+ ui_pyroscope_colormap_init();
+}
+
+
+// offset into the color index table, depending on whether this is an odd or even item
+static int row_offset(core::View* view, Range& range) {
+ return (((range.first - view->begin_visible()) & 1) + 1) * ps::COL_MAX;
+}
+
+
+torrent::Object ui_canvas_color_get() {
+ return ::ui_canvas_color;
+}
+
+
+torrent::Object ui_canvas_color_set(const torrent::Object::string_type& arg) {
+ ::ui_canvas_color = arg;
+ return torrent::Object();
+}
+
+
+int64_t cmd_d_message_alert(core::Download* d) {
+ int64_t alert = ps::ALERT_NORMAL;
+ const std::string& msg = d->message();
+
+ if (!msg.empty()) {
+ alert = ps::ALERT_GENERIC;
+
+ if (msg.find("Tried all trackers") != std::string::npos)
+ alert = ps::ALERT_NORMAL_CYCLING;
+ else if (msg.find("no data") != std::string::npos)
+ alert = ps::ALERT_NORMAL_GHOST;
+ else if (msg.find("Timeout was reached") != std::string::npos
+ || msg.find("Timed out") != std::string::npos)
+ alert = ps::ALERT_TIMEOUT;
+ else if (msg.find("Connecting to") != std::string::npos)
+ alert = ps::ALERT_CONNECT;
+ else if (msg.find("Could not parse bencoded data") != std::string::npos
+ || msg.find("Failed sending data") != std::string::npos
+ || msg.find("Server returned nothing") != std::string::npos
+ || msg.find("Couldn't connect to server") != std::string::npos)
+ alert = ps::ALERT_REQUEST;
+ else if (msg.find("not registered") != std::string::npos
+ || msg.find("torrent cannot be found") != std::string::npos
+ || msg.find("nregistered") != std::string::npos)
+ alert = ps::ALERT_GONE;
+ else if (msg.find("not authorized") != std::string::npos
+ || msg.find("blocked from") != std::string::npos
+ || msg.find("denied") != std::string::npos
+ || msg.find("limit exceeded") != std::string::npos
+ || msg.find("active torrents are enough") != std::string::npos)
+ alert = ps::ALERT_PERMS;
+ else if (msg.find("tracker is down") != std::string::npos)
+ alert = ps::ALERT_DOWN;
+ else if (msg.find("n't resolve host name") != std::string::npos)
+ alert = ps::ALERT_DNS;
+ }
+
+ return alert;
+}
+
+
+unsigned long cmd_d_eta_seconds(core::Download* d) {
+ uint32_t rate = d->info()->down_rate()->rate();
+
+#if RT_HEX_VERSION <= 0x000906
+ if (d->is_done())
+#else
+ if (d->data()->is_partially_done())
+#endif
+ return 0UL;
+
+ if (rate < 512)
+ return ULONG_MAX;
+
+#if RT_HEX_VERSION <= 0x000906
+ unsigned long remaining = (d->download()->file_list()->size_bytes() - d->download()->bytes_done()) / (rate & ~(uint32_t)(512 - 1));
+#else
+ unsigned long remaining = (d->download()->file_list()->selected_size_bytes() - d->download()->bytes_done()) / (rate & ~(uint32_t)(512 - 1));
+#endif
+
+ return remaining;
+}
+
+
+torrent::Object cmd_d_eta_time(core::Download* d) {
+ std::string eta_time = "⋆ ⋆⋆ ";
+ unsigned long remaining = cmd_d_eta_seconds(d);
+
+ if (remaining > 0 && remaining < ULONG_MAX) {
+ eta_time = elapsed_time(rpc::call_command_value("system.time") + remaining, 0L);
+ }
+
+ return eta_time;
+}
+
+
+std::string get_active_tracker_alias(torrent::Download* item) {
+ std::string url = get_active_tracker_domain(item);
+ if (!url.empty()) {
+ std::string alias = tracker_aliases[url];
+ if (!alias.empty()) url = alias;
+ }
+
+ return url;
+}
+
+
+torrent::Object cmd_d_tracker_alias(core::Download* download) {
+ return get_active_tracker_alias(download->download());
+}
+
+
+static void decorate_download_title(Window* window, display::Canvas* canvas, core::View* view,
+ int pos, Range& range, int x_title, size_t hilite, size_t hilen) {
+ int offset = row_offset(view, range);
+ core::Download* item = *range.first;
+ bool active = item->is_open() && item->is_active();
+
+ if (int(canvas->width()) <= x_title) return;
+
+ // download title color
+ int title_col;
+ unsigned long focus_attr = range.first == view->focus() ? attr_map[ps::COL_FOCUS] : 0;
+#if RT_HEX_VERSION <= 0x000906
+ if ((*range.first)->is_done())
+#else
+ if ((*range.first)->data()->is_partially_done())
+#endif
+ title_col = (active ? D_INFO(item)->up_rate()->rate() ?
+ ps::COL_SEEDING : ps::COL_COMPLETE : ps::COL_STOPPED) + offset;
+ else
+ title_col = (active ? D_INFO(item)->down_rate()->rate() ?
+ ps::COL_LEECHING : ps::COL_INCOMPLETE : ps::COL_QUEUED) + offset;
+ canvas->set_attr(x_title, pos, -1, attr_map[title_col] | focus_attr, title_col);
+ if (hilen && hilite != std::string::npos && x_title + hilite < int(canvas->width())) {
+ canvas->set_attr(x_title + hilite, pos, std::min(hilen, int(canvas->width()) - x_title - hilite),
+ (attr_map[title_col] | focus_attr | A_REVERSE) ^ A_BOLD, title_col);
+ }
+
+ // show label for active tracker (a/k/a in focus tracker)
+ if (int(canvas->width()) <= x_title + NAME_RESERVED_WIDTH + 3) return;
+ std::string url = get_active_tracker_alias((*range.first)->download());
+ if (url.empty()) return;
+
+ // shorten label if too long
+ int max_len = std::min(TRACKER_LABEL_WIDTH,
+ int(canvas->width()) - x_title - NAME_RESERVED_WIDTH - 3);
+ if (max_len > 0) {
+ int len = url.length();
+ if (len > max_len) {
+ url = "…" + url.substr(len - max_len);
+ len = max_len + 1;
+ }
+
+ // print it right-justified and in braces
+ int td_col = ps::COL_INFO;
+ //int td_col = active ? ps::COL_INFO : (*range.first)->is_done() ? ps::COL_STOPPED : ps::COL_QUEUED;
+ int xpos = canvas->width() - len - 2;
+ canvas->print(xpos, pos, "{%s}", url.c_str());
+ canvas->set_attr(xpos + 1, pos, len, attr_map[td_col + offset] | focus_attr, td_col + offset);
+ canvas->set_attr(xpos, pos, 1, (attr_map[td_col + offset] | focus_attr) ^ A_BOLD, td_col + offset);
+ canvas->set_attr(canvas->width() - 1, pos, 1,
+ (attr_map[td_col + offset] | focus_attr) ^ A_BOLD, td_col + offset);
+ }
+}
+
+
+// show ratio progress by color (ratio is scaled x1000)
+static int ratio_color(int ratio) {
+ int rcol = sizeof(ratio_col) / sizeof(*ratio_col) - 1;
+ return ratio_col[std::min(rcol, std::max(0, ratio) * rcol / 1200)];
+}
+
+
+// patch hook for download list canvas redraw of a single item; "pos" is placed AFTER the item
+void ui_pyroscope_download_list_redraw_item(Window* window, display::Canvas* canvas, core::View* view, int pos, Range& range) {
+ int offset = row_offset(view, range);
+ torrent::Download* item = (*range.first)->download();
+
+ pos -= 3;
+
+ // is this the item in focus?
+ if (range.first == view->focus()) {
+ for (int i = 0; i < 3; i++ ) {
+ canvas->set_attr(0, pos+i, 1, attr_map[ps::COL_FOCUS], ps::COL_FOCUS);
+ }
+ }
+
+ decorate_download_title(window, canvas, view, pos, range, 2, -1, 0);
+
+ // better handling for trail of line 2 (ratio etc.)
+ int status_pos = 91;
+ int ratio = rpc::call_command_value("d.ratio", rpc::make_target(*range.first));
+
+ if (status_pos < int(canvas->width())) {
+ canvas->print(status_pos, pos+1, "R:%6.2f [%c%c] %-4.4s ",
+ float(ratio) / 1000.0,
+ rpc::call_command_string("d.tied_to_file", rpc::make_target(*range.first)).empty() ? ' ' : 'T',
+ (rpc::call_command_value("d.ignore_commands", rpc::make_target(*range.first)) == 0) ? ' ' : 'I',
+ (*range.first)->priority() == 2 ? "" :
+ rpc::call_command_string("d.priority_str", rpc::make_target(*range.first)).c_str()
+ );
+ status_pos += 9 + 5 + 5;
+ }
+
+ // if space is left, show throttle name
+ if (status_pos < int(canvas->width())) {
+ std::string item_status;
+
+ if (!(*range.first)->bencode()->get_key("rtorrent").get_key_string("throttle_name").empty()) {
+ //item_status += "T=";
+ item_status += rpc::call_command_string("d.throttle_name", rpc::make_target(*range.first)) + ' ';
+ }
+
+ // left-justifying this also overwrites any junk from the original display that we overwrite
+ int chars_left = canvas->width() - status_pos - item_status.length();
+ if (chars_left < 0) {
+ item_status = item_status.substr(0, 1-chars_left) + "…";
+ } else if (chars_left > 0) {
+ item_status = std::string(chars_left, ' ') + item_status;
+ }
+ canvas->print(status_pos, pos+1, "%s", item_status.c_str());
+ }
+
+ //.........1.........2.........3.........4.........5.........6.........7.........8.........9.........0.........1
+ //12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
+ // [CLOSED] 0,0 / 15,9 MB Rate: 0,0 / 0,0 KB Uploaded: 0,0 MB [ 0%] --d --:-- R:nnnnnn [TI]
+ // [CLOSED] 0.0K / 0.0K U/D: 0.0K / 0.0K Uploaded: 0.0K R: 0.00 [T ]
+ int label_pos[] = {19, 1, 31, 5, 44, 1, 54, 9, 75, 1, 79, 1, 91, 2, 100, 1, 103, 1};
+ const char* labels[sizeof(label_pos) / sizeof(int) / 2] = {0, " U/D:"};
+ int col_active = ps::COL_INFO;
+ //int col_active = item->is_open() && item->is_active() ? ps::COL_INFO : (*range.first)->is_done() ? ps::COL_STOPPED : ps::COL_QUEUED;
+
+ // apply basic "info" style, and then revert static text to "label"
+ canvas->set_attr(2, pos+1, canvas->width() - 1, attr_map[col_active + offset], col_active + offset);
+ for (size_t label_idx = 0; label_idx < sizeof(label_pos) / sizeof(int); label_idx += 2) {
+ if (labels[label_idx/2]) canvas->print(label_pos[label_idx], pos+1, labels[label_idx/2]);
+ canvas->set_attr(label_pos[label_idx], pos+1, label_pos[label_idx+1], attr_map[ps::COL_LABEL + offset], ps::COL_LABEL + offset);
+ }
+
+ // apply progress color to completion indicator
+ int pcol = ratio_color(item->file_list()->completed_chunks() * 1000 / item->file_list()->size_chunks());
+ canvas->set_attr(76, pos+1, 3, attr_map[pcol + offset], pcol + offset);
+
+ // show ratio progress by color
+ int rcol = ratio_color(ratio);
+ canvas->set_attr(93, pos+1, 6, attr_map[rcol + offset], rcol + offset);
+
+ // mark active up / down ("focus", plus "seeding" or "leeching"), and dim inactive numbers (i.e. 0)
+ canvas->set_attr(37, pos+1, 6, attr_map[ps::COL_SEEDING + offset] | (D_INFO(item)->up_rate()->rate() ? attr_map[ps::COL_FOCUS] : 0),
+ (D_INFO(item)->up_rate()->rate() ? ps::COL_SEEDING : ps::COL_LABEL) + offset);
+ canvas->set_attr(46, pos+1, 6, attr_map[ps::COL_LEECHING + offset] | (D_INFO(item)->down_rate()->rate() ? attr_map[ps::COL_FOCUS] : 0),
+ (D_INFO(item)->down_rate()->rate() ? ps::COL_LEECHING : ps::COL_LABEL) + offset);
+
+ // mark non-trivial messages
+ if (!(*range.first)->message().empty() && (*range.first)->message().find("Tried all trackers") == std::string::npos) {
+ canvas->set_attr(1, pos, 1, attr_map[ps::COL_ALARM + offset], ps::COL_ALARM + offset);
+ canvas->set_attr(1, pos+1, 1, attr_map[ps::COL_ALARM + offset], ps::COL_ALARM + offset);
+ canvas->set_attr(1, pos+2, -1, attr_map[ps::COL_ALARM + offset], ps::COL_ALARM + offset);
+ }
+}
+
+
+torrent::Object ui_column_spec(rpc::target_type target, const torrent::Object::list_type& args) {
+ if (args.size() != 1) {
+ throw torrent::input_error("ui.column.spec takes exactly one argument!");
+ }
+ int64_t colidx_wanted = parse_value_arg(*args.begin());
+ std::string spec;
+
+ const torrent::Object::map_type& column_defs = control->object_storage()->get_str("ui.column.render").as_map();
+ torrent::Object::map_const_iterator cols_itr, last_col = column_defs.end();
+
+ for (cols_itr = column_defs.begin(); cols_itr != last_col; ++cols_itr) {
+ char* header_pos = 0;
+ int64_t colidx = strtol(cols_itr->first.c_str(), &header_pos, 10);
+ if (header_pos[0] == ':' && colidx == colidx_wanted)
+ spec = cols_itr->first;
+ }
+
+ return spec;
+}
+
+
+torrent::Object ui_column_hide(rpc::target_type target, const torrent::Object::list_type& args) {
+ for(torrent::Object::list_const_iterator itr = args.begin(), last = args.end(); itr != last; ++itr) {
+ int64_t colidx = parse_value_arg(*itr);
+ column_hidden.insert(colidx);
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object ui_column_show(rpc::target_type target, const torrent::Object::list_type& args) {
+ for(torrent::Object::list_const_iterator itr = args.begin(), last = args.end(); itr != last; ++itr) {
+ int64_t colidx = parse_value_arg(*itr);
+ column_hidden.erase(colidx);
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object ui_column_is_hidden(rpc::target_type target, const torrent::Object::list_type& args) {
+ if (args.size() != 1) {
+ throw torrent::input_error("ui.column.is_hidden takes exactly one argument!");
+ }
+ int64_t colidx = parse_value_arg(*args.begin());
+
+ return (int64_t) column_hidden.count(colidx);
+}
+
+
+torrent::Object ui_column_hidden_list() {
+ torrent::Object result = torrent::Object::create_list();
+ torrent::Object::list_type& resultList = result.as_list();
+
+ for (std::set<int>::const_iterator itr = column_hidden.begin(); itr != column_hidden.end(); itr++) {
+ resultList.push_back(*itr);
+ }
+
+ return result;
+}
+
+
+torrent::Object ui_column_sacrificial_list() {
+ torrent::Object result = torrent::Object::create_list();
+ torrent::Object::list_type& resultList = result.as_list();
+
+ const torrent::Object::map_type& column_defs = control->object_storage()->get_str("ui.column.render").as_map();
+ torrent::Object::map_const_iterator cols_itr, last_col = column_defs.end();
+
+ for (cols_itr = column_defs.begin(); cols_itr != last_col; ++cols_itr) {
+ char* header_pos = 0;
+ int64_t colidx = strtol(cols_itr->first.c_str(), &header_pos, 10);
+ if (header_pos[0] == ':' && header_pos[1] == '?')
+ resultList.push_back(colidx);
+ }
+
+ return result;
+}
+
+
+// Render columns from `column_defs`, return total length
+int render_columns(bool headers, bool narrow, rpc::target_type target, core::Download* item,
+ display::Canvas* canvas, int column, int pos, int offset,
+ const torrent::Object::map_type& column_defs) {
+ torrent::Object::map_const_iterator cols_itr, last_col = column_defs.end();
+ int total = 0;
+
+ for (cols_itr = column_defs.begin(); cols_itr != last_col; ++cols_itr) {
+ // Handle index / sort key (format is "sort:len:title")
+ char* header_pos = 0;
+ int colidx = (int)strtol(cols_itr->first.c_str(), &header_pos, 10);
+ if (*header_pos++ != ':') continue; // 2nd field is missing
+ if (column_hidden.count(colidx)) continue; // column is hidden
+
+ // Check for 'sacrificial' marker
+ if (*header_pos == '?') {
+ if (narrow) continue; // skip this column
+ ++header_pos;
+ }
+
+ // Parse header length
+ char* header_text = 0;
+ int header_len = (int)strtol(header_pos, &header_text, 10);
+
+ // Check available space
+ if (int(canvas->width()) - NAME_RESERVED_WIDTH < column + header_len) {
+ if (!narrow && headers) return -1; // trigger narrow mode
+ break; // all the space we have used up, get us out of here
+ }
+
+ // Do we have a colordef?
+ std::string color_def;
+ if (*header_text == 'C') {
+ int x = 0;
+ while (header_text[x] && header_text[x] != ':') x++;
+ color_def.assign(header_text, x);
+ header_text += x;
+ }
+ if (*header_text++ != ':') continue; // Header text is missing
+
+ // Render title text, or the result of the column command
+ ui_canvas_color = color_def;
+ if (headers) {
+ std::string header_str = u8_chop(header_text, header_len);
+ canvas->print(column, pos, "%s", header_str.c_str());
+ } else {
+ std::string text;
+ try {
+ text = rpc::call_object(cols_itr->second, target).as_string();
+ } catch (torrent::input_error& e) {
+ // Rows will rotate through the error string (assuming it is thrown for each row)
+ char buf[10];
+ int what_pos = *e.what() ? (pos - CANVAS_POS_1ST_ITEM) * header_len % strlen(e.what()) : 0;
+ snprintf(buf, sizeof(buf), "C22/%d", header_len);
+ ui_canvas_color = buf;
+ text = std::string(e.what()).substr(what_pos, header_len);
+ }
+ canvas->print(column, pos, "%s", u8_chop(text, header_len).c_str());
+ //canvas->print(column, pos, " %s ", ui_canvas_color); // debug: print color index
+
+ // apply colorization
+ if (ui_canvas_color.empty()) {
+ canvas->set_attr(column, pos, header_len,
+ attr_map[ps::COL_INFO + offset], ps::COL_INFO + offset);
+ } else {
+ int attr_col = column;
+ for (const char* ptr = ui_canvas_color.c_str(); *ptr && *ptr++ == 'C'; ) {
+ char* next = 0;
+ int attr_idx = (int)strtol(ptr, &next, 10); if (next == ptr) break; ptr = next;
+ if (*ptr != '/') continue;
+
+ // System colors – these are mapped to a 'normal' color index
+ if (item) {
+ const char* c_down = "C28/4C27/1"; // leeching + incomplete
+ const char* c_seed = "C24/4C23/1"; // seeding + complete
+ const char* c_done = "C21/1C24/1C21/2C24/1"; // info + seeding (is_done)
+ const char* c_part = "C21/1C27/1C21/2C27/1"; // info + incomplete
+ const char* c_queu = "C21/1C26/1C21/2C26/1"; // info + queued
+ const char* c_eta = "C21/1C28/1C21/2C28/1"; // info + leeching
+ const char* c_xfer = "C21/1C13/1C21/2C13/1"; // info + progress60
+
+ switch (attr_idx) {
+ case ps::COL_DOWN_TIME: // C90/5
+#if RT_HEX_VERSION <= 0x000906
+ ptr = item->is_done() ? c_done :
+#else
+ ptr = item->data()->is_partially_done() ? c_done :
+#endif
+ D_INFO(item)->down_rate()->rate() ? c_down : c_part;
+ continue; // with new color definition
+ case ps::COL_UP_TIME: // C96/5
+ ptr = D_INFO(item)->up_rate()->rate() ? c_seed :
+#if RT_HEX_VERSION <= 0x000906
+ item->is_done() ? c_done : c_part;
+#else
+ item->data()->is_partially_done() ? c_done : c_part;
+#endif
+ continue; // with new color definition
+ case ps::COL_ACTIVE_TIME: // C70/5
+ ptr = D_INFO(item)->up_rate()->rate() ? c_seed : c_queu;
+ continue; // with new color definition
+ case ps::COL_ETA_TIME: // C73/5
+#if RT_HEX_VERSION <= 0x000906
+ ptr = item->is_done() ? c_xfer : c_eta;
+#else
+ ptr = item->data()->is_partially_done() ? c_xfer : c_eta;
+#endif
+ continue; // with new color definition
+ case ps::COL_PRIO:
+ attr_idx = col_idx_prio[std::min(3U, (uint32_t) item->priority())];
+ break;
+ case ps::COL_STATE:
+ attr_idx = col_idx_state[(item->is_open() << 1) | item->is_active()];
+ break;
+ case ps::COL_RATIO:
+ attr_idx = ratio_color(rpc::call_command_value("d.ratio", target));
+ break;
+ case ps::COL_PROGRESS:
+ attr_idx = ratio_color(item->file_list()->completed_chunks() * 1000 /
+ item->file_list()->size_chunks());
+ break;
+ case ps::COL_UNSAFE_DATA:
+ attr_idx = col_idx_unsafe[std::min(2U, (uint32_t) get_custom_long(item, "unsafe_data"))];
+ break;
+ case ps::COL_THROTTLE_CH:
+ {
+ std::string throttlename = "";
+ if (!item->bencode()->get_key("rtorrent").get_key_string("throttle_name").empty()) {
+ throttlename = rpc::call_command_string("d.throttle_name", rpc::make_target(item)).c_str();
+ }
+ attr_idx = col_idx_throttle_ch[(!throttlename.empty() ? (throttlename == "NULL" ? 3 : (throttlename == "slowup" ? 2 : 1)) : 0)];
+ }
+ break;
+ case ps::COL_ALERT: // COL_ALARM is the actual color, this is the dynamic one
+ {
+ bool has_alert = !item->message().empty()
+ && item->message().find("Tried all trackers") == std::string::npos;
+ bool no_data = item->message().find("no data") != std::string::npos;
+ attr_idx = no_data ? ps::COL_PROGRESS0 : has_alert ? ps::COL_ALARM : ps::COL_INFO;
+ }
+ break;
+ }
+ }
+
+ // Get color area length, if both pos/len are ok, do it
+ int attr_len = (int)strtol(ptr + 1, &next, 10); if (next == ptr) break; ptr = next;
+ if (attr_idx && attr_len) {
+ if (attr_idx >= ps::COL_MAX) attr_idx = ps::COL_ALARM;
+ canvas->set_attr(attr_col, pos, attr_len, attr_map[attr_idx + offset], attr_idx + offset);
+ attr_col += attr_len;
+ }
+ }
+ }
+ }
+
+ // Advance canvas column position, and add to length
+ column += header_len + 1;
+ total += header_len + 1;
+ }
+
+ return total;
+}
+
+
+// patch hook for download list canvas redraw; if this returns true, the calling
+// function is left immediately (i.e. true indicates we took over ALL redrawing)
+bool ui_pyroscope_download_list_redraw(Window* window, display::Canvas* canvas, core::View* view) {
+ // show "X of Y"
+ if (canvas->width() >= X_OF_Y_CANVAS_MIN_WIDTH) {
+ size_t item_idx = view->focus() - view->begin_visible();
+ if (item_idx == view->size())
+ canvas->print(canvas->width() - 16, 0, "[ none of %-5d]", view->size());
+ else
+ canvas->print(canvas->width() - 16, 0, "[%5d of %-5d]", item_idx + 1, view->size());
+ }
+ canvas->set_attr(0, 0, -1, attr_map[ps::COL_TITLE], ps::COL_TITLE);
+
+ if (is_collapsed.find(view->name()) == is_collapsed.end() || !is_collapsed[view->name()])
+ return false; // continue in calling function
+
+ if (view->empty_visible() || canvas->width() < 5 || canvas->height() < 2)
+ return true;
+
+ // Prepare rendering
+ const torrent::Object::map_type& column_defs = control->object_storage()->get_str("ui.column.render").as_map();
+ int pos = 1, x_base = 2, column = x_base;
+ bool narrow = false;
+ std::string find_term = rpc::call_command_string("ui.find.term");
+ std::transform(find_term.begin(), find_term.end(), find_term.begin(), ::tolower);
+
+ // Render header line
+ canvas->print(0, pos, "⇳ ");
+ int custom_width = render_columns(true, narrow, rpc::make_target(), 0, canvas, column, pos, 0, column_defs);
+ if (custom_width < 0) { // enter narrow mode
+ canvas->print(x_base, pos, "%s", std::string(canvas->width() - x_base, ' ').c_str()); // clean slate
+ narrow = true;
+ custom_width = render_columns(true, narrow, rpc::make_target(), 0, canvas, column, pos, 0, column_defs);
+ }
+ column += custom_width; canvas->print(column, pos, "Name "); column += NAME_RESERVED_WIDTH;
+ if (int(canvas->width()) - 8 > column)
+ canvas->print(canvas->width() - 8, pos, " Tracker");
+ canvas->set_attr(0, pos, -1, attr_map[ps::COL_LABEL], ps::COL_LABEL); // header line unicolor
+
+ // network traffic
+ int network_history_lines = 0;
+ if (network_history_depth) {
+ network_history_lines = 2;
+ pos = canvas->height() - 2;
+
+ canvas->print(0, pos, "%s", network_history_up_str.c_str());
+ canvas->set_attr(0, pos, -1, attr_map[ps::COL_SEEDING], ps::COL_SEEDING);
+ canvas->print(0, pos+1, "%s", network_history_down_str.c_str());
+ canvas->set_attr(0, pos+1, -1, attr_map[ps::COL_LEECHING], ps::COL_LEECHING);
+ }
+
+ // define iterator range
+ Range range = rak::advance_bidirectional(
+ view->begin_visible(),
+ view->focus() != view->end_visible() ? view->focus() : view->begin_visible(),
+ view->end_visible(),
+ canvas->height()-2-2-network_history_lines);
+
+ pos = CANVAS_POS_1ST_ITEM;
+ while (range.first != range.second) {
+ core::Download* d = *range.first;
+ int offset = row_offset(view, range);
+ int col_active = ps::COL_INFO;
+
+ // Render focus marker
+ canvas->print(0, pos, range.first == view->focus() ? "> " : " ");
+
+ // Render custom columns
+ canvas->set_attr(1, pos, -1, attr_map[col_active + offset], col_active + offset); // base color, whole line
+ column = x_base;
+ render_columns(false, narrow, rpc::make_target(d), d, canvas, column, pos, offset, column_defs);
+ column += custom_width;
+
+ // Render name + tracker
+ if (int(canvas->width()) > column) {
+ std::string displayname = get_custom_string(d, "displayname");
+ canvas->print(column, pos, "%s",
+ u8_chop(displayname.empty() ? d->info()->name() : displayname.c_str(),
+ canvas->width() - column - 1).c_str());
+ size_t hilite = std::string::npos;
+ if (!find_term.empty()) {
+ if (displayname.empty()) displayname = d->info()->name();
+ std::transform(displayname.begin(), displayname.end(), displayname.begin(), ::tolower);
+ hilite = displayname.find(find_term);
+ }
+ decorate_download_title(window, canvas, view, pos, range, column, hilite, find_term.length());
+ }
+
+ // Colorize focus marker
+ if (range.first == view->focus()) {
+ canvas->set_attr(0, pos, 1, attr_map[ps::COL_FOCUS], ps::COL_FOCUS);
+ }
+
+ // Advance to next item
+ ++pos;
+ ++range.first;
+ }
+
+ if (view->focus() != view->end_visible()) {
+ char buffer[canvas->width() + 1];
+ char* last = buffer + canvas->width() + 1;
+
+ pos = canvas->height() - 2 - network_history_lines;
+#if RT_HEX_VERSION <= 0x000906
+ print_download_info(buffer, last, *view->focus());
+#else
+ print_download_info_full(buffer, last, *view->focus());
+#endif
+ canvas->print(3, pos, "%s", buffer);
+ canvas->set_attr(0, pos, -1, attr_map[ps::COL_LABEL], ps::COL_LABEL);
+ print_download_status(buffer, last, *view->focus());
+ canvas->print(3, pos+1, "%s", buffer);
+ canvas->set_attr(0, pos+1, -1, attr_map[ps::COL_LABEL], ps::COL_LABEL);
+ }
+
+ return true;
+}
+
+
+// patch hook for window title canvas redraw
+void ui_pyroscope_statusbar_redraw(Window* window, display::Canvas* canvas) {
+ canvas->set_attr(0, 0, -1, attr_map[ps::COL_FOOTER], ps::COL_FOOTER);
+}
+
+} // namespace
+
+
+torrent::Object cmd_view_collapsed_toggle(const torrent::Object::string_type& args) {
+ std::string view_name = args;
+
+ if (view_name.empty()) {
+ view_name = control->ui()->download_list()->current_view()->name();
+ }
+
+ is_collapsed[view_name] = is_collapsed.find(view_name) == is_collapsed.end() ? true : !is_collapsed[view_name];
+
+ return is_collapsed[view_name];
+}
+
+
+// implementation of method we patched into rpc::object_storage
+const torrent::Object& rpc::object_storage::set_color_string(const torrent::raw_string& key, const std::string& object) {
+ const torrent::Object& result = rpc::object_storage::set_string(key, object);
+ display::ui_pyroscope_colormap_init();
+ return result;
+}
+
+
+// Traffic history
+int network_history_depth_get() {
+ return network_history_depth;
+}
+
+torrent::Object network_history_depth_set(int arg) {
+ if (network_history_depth) {
+ delete[] network_history_up;
+ delete[] network_history_down;
+ network_history_up = network_history_down = 0;
+ }
+
+ network_history_depth = arg;
+ network_history_count = 0;
+
+ if (network_history_depth) {
+ network_history_up = new uint32_t[network_history_depth];
+ network_history_down = new uint32_t[network_history_depth];
+ }
+
+ return torrent::Object();
+}
+
+
+void network_history_format(std::string& buf, char kind, uint32_t* data) {
+ uint32_t samples = std::min(network_history_count, (uint32_t) network_history_depth);
+ uint32_t min_rate = *std::min_element(data, data + samples);
+ uint32_t max_rate = *std::max_element(data, data + samples);
+ char buffer[80];
+
+ snprintf(buffer, sizeof(buffer), "%c ⌈%s⌉⌊%s⌋%s", kind,
+ display::human_size(max_rate, 0).c_str(), display::human_size(min_rate, 0).c_str(),
+ rpc::call_command_value("network.history.auto_scale") ? "↨ " : " ");
+ buf = buffer;
+
+ if (max_rate > 102) {
+ const char* meter[] = {"⠀", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"};
+ uint32_t base = rpc::call_command_value("network.history.auto_scale") ? min_rate : 0;
+ for (uint32_t i = 1; i <= samples; ++i) {
+ uint32_t idx = (network_history_count - i) % network_history_depth;
+ if (max_rate > base)
+ buf += meter[std::min(8U, (data[idx] - base) * 9 / (max_rate - base))];
+ else
+ buf += " ";
+ }
+ }
+ buf += " ";
+}
+
+
+// You MUST call this after changing the auto_scale flag, to see any changes immediately!
+torrent::Object network_history_refresh() {
+ if (network_history_depth) {
+ network_history_format(network_history_up_str, 'U', network_history_up);
+ network_history_format(network_history_down_str, 'D', network_history_down);
+ }
+
+ return torrent::Object();
+}
+
+
+torrent::Object network_history_sample() {
+ if (network_history_depth) {
+ network_history_up[network_history_count % network_history_depth] = torrent::up_rate()->rate();
+ network_history_down[network_history_count % network_history_depth] = torrent::down_rate()->rate();
+ ++network_history_count;
+ }
+
+ return network_history_refresh();
+}
+
+
+torrent::Object cmd_trackers_alias_set_key(rpc::target_type target, const torrent::Object::list_type& args) {
+ torrent::Object::list_const_iterator itr = args.begin();
+ if (args.size() != 2) {
+ throw torrent::input_error("trackers.alias.set_key: expecting two arguments!");
+ }
+ std::string domain = (itr++)->as_string();
+ std::string alias = (itr++)->as_string();
+
+ tracker_aliases[domain] = alias;
+
+ return torrent::Object();
+}
+
+
+torrent::Object cmd_trackers_alias_items(rpc::target_type target) {
+ torrent::Object rawResult = torrent::Object::create_list();
+ torrent::Object::list_type& result = rawResult.as_list();
+
+ for (string_kv_map::const_iterator itr = tracker_aliases.begin(), last = tracker_aliases.end(); itr != last; itr++) {
+ std::string mapping = itr->first + "=" + itr->second;
+ result.push_back(mapping);
+ }
+
+ return rawResult;
+}
+
+
+torrent::Object apply_time_delta(const torrent::Object::list_type& args) {
+ if (args.size() != 1 && args.size() != 2)
+ throw torrent::input_error("convert.time_delta takes 1 or 2 arguments!");
+ if (!args.front().is_value())
+ throw torrent::input_error("convert.time_delta: time argument must be a value!");
+ if (args.size() == 2 && !args.back().is_value())
+ throw torrent::input_error("convert.time_delta: time-base argument must be a value!");
+
+ return elapsed_time(args.front().as_value(), args.size() == 2 ? args.back().as_value() : 0L);
+}
+
+
+torrent::Object apply_human_size(const torrent::Object::list_type& args) {
+ if (args.size() != 1 && args.size() != 2)
+ throw torrent::input_error("convert.human_size takes 1 or 2 arguments!");
+
+ torrent::Object::value_type bytes = args.front().as_value();
+ torrent::Object::value_type format = args.size() > 1 ? args.back().as_value() : 2;
+
+ return display::human_size(bytes, format);
+}
+
+
+torrent::Object apply_magnitude(const torrent::Object::list_type& args) {
+ if (args.size() != 1)
+ throw torrent::input_error("convert.magnitude takes 1 value argument!");
+
+ return num2(args.front().as_value());
+}
+
+
+torrent::Object ui_find_next() {
+ std::string term = rpc::call_command_string("ui.find.term");
+ if (term.empty())
+ return torrent::Object(); // no current search term set
+ std::transform(term.begin(), term.end(), term.begin(), ::tolower);
+
+ ui::DownloadList* dl_list = control->ui()->download_list();
+ core::View* dl_view = dl_list->current_view();
+
+ if (dl_view->empty_visible()) {
+ control->core()->push_log("This view is empty, nothing to find!");
+ } else {
+ core::View::iterator itr = dl_view->focus() == dl_view->end_visible() ?
+ dl_view->begin_visible() : dl_view->focus();
+ bool found = false;
+
+ do {
+ if (++itr == dl_view->end_visible())
+ itr = dl_view->begin_visible();
+
+ // In C++11, this can be done more efficiently using std::search;
+ // we only use this interactively, so meh.
+ std::string name = get_custom_string(*itr, "displayname");
+ if (name.empty()) name = (*itr)->info()->name();
+ std::transform(name.begin(), name.end(), name.begin(), ::tolower);
+ found = name.find(term) != std::string::npos;
+ } while (!found && itr != (dl_view->focus() == dl_view->end_visible() ? dl_view->begin_visible() : dl_view->focus()));
+
+ if (!found) {
+ control->core()->push_log(("Cannot find anything matching '" + term + "'").c_str());
+ } else if (itr != dl_view->focus()) {
+ dl_view->set_focus(itr);
+ dl_view->set_last_changed();
+ }
+ }
+
+ return torrent::Object();
+}
+
+
+// register our commands
+void initialize_command_ui_pyroscope() {
+ #define PS_VARIABLE_COLOR(key, value) \
+ control->object_storage()->insert_c_str(key, value, rpc::object_storage::flag_string_type); \
+ CMD2_ANY(key, _cxxstd_::bind(&rpc::object_storage::get, control->object_storage(), \
+ torrent::raw_string::from_c_str(key))); \
+ CMD2_ANY_STRING(key ".set", _cxxstd_::bind(&rpc::object_storage::set_color_string, control->object_storage(), \
+ torrent::raw_string::from_c_str(key), _cxxstd_::placeholders::_2));
+
+ #define PS_CMD_ANY_FUN(key, func) \
+ CMD2_ANY(key, _cxxstd_::bind(&func))
+
+ CMD2_ANY ("network.history.depth", _cxxstd_::bind(&network_history_depth_get));
+ CMD2_ANY_VALUE_V("network.history.depth.set", _cxxstd_::bind(&network_history_depth_set, _cxxstd_::placeholders::_2));
+ CMD2_ANY ("network.history.refresh", _cxxstd_::bind(&network_history_refresh));
+ CMD2_ANY ("network.history.sample", _cxxstd_::bind(&network_history_sample));
+ CMD2_VAR_BOOL ("network.history.auto_scale", true);
+
+ CMD2_ANY_STRING("view.collapsed.toggle", _cxxstd_::bind(&cmd_view_collapsed_toggle, _cxxstd_::placeholders::_2));
+
+ CMD2_ANY_LIST("trackers.alias.set_key", &cmd_trackers_alias_set_key);
+ CMD2_ANY("trackers.alias.items", _cxxstd_::bind(&cmd_trackers_alias_items, _cxxstd_::placeholders::_1));
+ CMD2_DL("d.tracker_alias", _cxxstd_::bind(&display::cmd_d_tracker_alias, _cxxstd_::placeholders::_1));
+
+ CMD2_DL("d.message.alert", _cxxstd_::bind(&display::cmd_d_message_alert, _cxxstd_::placeholders::_1));
+
+ CMD2_DL("d.eta.seconds", _cxxstd_::bind(&display::cmd_d_eta_seconds, _cxxstd_::placeholders::_1));
+ CMD2_DL("d.eta.time", _cxxstd_::bind(&display::cmd_d_eta_time, _cxxstd_::placeholders::_1));
+
+ CMD2_ANY ("ui.canvas_color", _cxxstd_::bind(&display::ui_canvas_color_get));
+ CMD2_ANY_STRING ("ui.canvas_color.set", _cxxstd_::bind(&display::ui_canvas_color_set, _cxxstd_::placeholders::_2));
+
+ CMD2_ANY_LIST("ui.column.spec", &display::ui_column_spec);
+ CMD2_ANY_LIST("ui.column.hide", &display::ui_column_hide);
+ CMD2_ANY_LIST("ui.column.show", &display::ui_column_show);
+ CMD2_ANY_LIST("ui.column.is_hidden", &display::ui_column_is_hidden);
+ CMD2_ANY("ui.column.hidden.list", _cxxstd_::bind(&display::ui_column_hidden_list));
+ CMD2_ANY("ui.column.sacrificial.list", _cxxstd_::bind(&display::ui_column_sacrificial_list));
+ CMD2_VAR_VALUE("ui.column.sacrificed", 0);
+
+ CMD2_ANY ("ui.find.next", _cxxstd_::bind(&ui_find_next));
+ CMD2_VAR_STRING("ui.find.term", "");
+
+ PS_VARIABLE_COLOR("ui.color.progress0", "red");
+ PS_VARIABLE_COLOR("ui.color.progress20", "bold bright red");
+ PS_VARIABLE_COLOR("ui.color.progress40", "bold bright magenta");
+ PS_VARIABLE_COLOR("ui.color.progress60", "yellow");
+ PS_VARIABLE_COLOR("ui.color.progress80", "bold bright yellow");
+ PS_VARIABLE_COLOR("ui.color.progress100", "green");
+ PS_VARIABLE_COLOR("ui.color.progress120", "bold bright green");
+ PS_VARIABLE_COLOR("ui.color.complete", "bright green");
+ PS_VARIABLE_COLOR("ui.color.seeding", "bold bright green");
+ PS_VARIABLE_COLOR("ui.color.stopped", "blue");
+ PS_VARIABLE_COLOR("ui.color.queued", "magenta");
+ PS_VARIABLE_COLOR("ui.color.incomplete", "yellow");
+ PS_VARIABLE_COLOR("ui.color.leeching", "bold bright yellow");
+ PS_VARIABLE_COLOR("ui.color.alarm", "bold white on red");
+ PS_VARIABLE_COLOR("ui.color.title", "bold bright white on blue");
+ PS_VARIABLE_COLOR("ui.color.footer", "bold bright cyan on blue");
+ PS_VARIABLE_COLOR("ui.color.label", "gray");
+ PS_VARIABLE_COLOR("ui.color.odd", "");
+ PS_VARIABLE_COLOR("ui.color.even", "");
+ PS_VARIABLE_COLOR("ui.color.info", "white");
+ PS_VARIABLE_COLOR("ui.color.focus", "reverse");
+ PS_VARIABLE_COLOR("ui.color.custom1", "");
+ PS_VARIABLE_COLOR("ui.color.custom2", "");
+ PS_VARIABLE_COLOR("ui.color.custom3", "");
+ PS_VARIABLE_COLOR("ui.color.custom4", "");
+ PS_VARIABLE_COLOR("ui.color.custom5", "");
+ PS_VARIABLE_COLOR("ui.color.custom6", "");
+ PS_VARIABLE_COLOR("ui.color.custom7", "");
+ PS_VARIABLE_COLOR("ui.color.custom8", "");
+ PS_VARIABLE_COLOR("ui.color.custom9", "");
+
+ PS_CMD_ANY_FUN("system.colors.max", display::get_colors);
+ PS_CMD_ANY_FUN("system.colors.enabled", has_colors);
+ PS_CMD_ANY_FUN("system.colors.rgb", can_change_color);
+
+ CMD2_ANY_LIST("convert.time_delta", _cxxstd_::bind(&apply_time_delta, _cxxstd_::placeholders::_2));
+ CMD2_ANY_LIST("convert.human_size", _cxxstd_::bind(&apply_human_size, _cxxstd_::placeholders::_2));
+ CMD2_ANY_LIST("convert.magnitude", _cxxstd_::bind(&apply_magnitude, _cxxstd_::placeholders::_2));
+
+
+ // Set some defaults by executing an in-memory script
+ std::string init_commands;
+ for (int colidx = ps::COL_DEFAULT + 1; colidx < ps::COL_MAX; colidx++) {
+ char cmdbuf[80];
+ snprintf(cmdbuf, sizeof(cmdbuf),
+ "method.insert = %s.index, private|value|const, %d\n",
+ color_vars[colidx], colidx);
+ init_commands.append(cmdbuf);
+ }
+
+
+ init_commands.append(
+ // Multi-method to store column definitions
+ "method.insert = ui.column.render, multi|rlookup|static\n"
+
+ // Toggle sacrificial columns manually (bound to '/' key)
+ "method.insert = ui.column.sacrificed.toggle, simple, \""
+ "branch = (ui.column.sacrificed), ((ui.column.sacrificed.set, 0)), ((ui.column.sacrificed.set, 1)) ; "
+ "branch = (ui.column.sacrificed),"
+ " \\\"ui.column.show = (ui.column.sacrificial.list)\\\","
+ " \\\"ui.column.hide = (ui.column.sacrificial.list)\\\" ; "
+ "ui.current_view.set = (ui.current_view)\"\n"
+ "schedule2 = column_sacrificed_toggle, 0, 0, ((ui.bind_key,download_list,/,ui.column.sacrificed.toggle=))\n"
+
+ // Bind '*' to toggle between collapsed and expanded display
+ "schedule2 = collapsed_view_toggle, 0, 0, ((ui.bind_key, download_list, *, \""
+ "view.collapsed.toggle= ; ui.current_view.set = (ui.current_view)\"))\n"
+
+ // Bind F3 to find the next item for 'ui.find.term'
+ "schedule2 = ui_find_next_f3, 0, 0, ((ui.bind_key, download_list, 0413, \"ui.find.next=\"))\n"
+
+ // Collapse built-in views
+ "view.collapsed.toggle = main\n"
+ "view.collapsed.toggle = name\n"
+ "view.collapsed.toggle = started\n"
+ "view.collapsed.toggle = stopped\n"
+ "view.collapsed.toggle = complete\n"
+ "view.collapsed.toggle = incomplete\n"
+ "view.collapsed.toggle = hashing\n"
+ "view.collapsed.toggle = seeding\n"
+ "view.collapsed.toggle = leeching\n"
+ "view.collapsed.toggle = active\n"
+
+ // 1: COL_CUSTOM1
+ // …
+ // 9: COL_CUSTOM9
+ // 10: COL_PROGRESS0
+ // 11: COL_PROGRESS20
+ // 12: COL_PROGRESS40
+ // 13: COL_PROGRESS60
+ // 14: COL_PROGRESS80
+ // 15: COL_PROGRESS100
+ // 16: COL_PROGRESS120
+ // 17: COL_TITLE
+ // 18: COL_FOOTER
+ // 19: COL_FOCUS
+ // 20: COL_LABEL
+ // 21: COL_INFO
+ // 22: COL_ALARM
+ // 23: COL_COMPLETE
+ // 24: COL_SEEDING
+ // 25: COL_STOPPED
+ // 26: COL_QUEUED
+ // 27: COL_INCOMPLETE
+ // 28: COL_LEECHING
+ // 29: COL_ODD
+ // 30: COL_EVEN
+
+ // 70: COL_ACTIVE_TIME
+ // 71: COL_UNSAFE_DATA
+ // 72: COL_THROTTLE_CH
+ // 73: COL_ETA_TIME
+
+ // 90: COL_DOWN_TIME
+ // 91: COL_PRIO
+ // 92: COL_STATE
+ // 93: COL_RATIO
+ // 94: COL_PROGRESS
+ // 95: COL_ALERT
+ // 96: COL_UP_TIME
+
+ // Status flags (❢ ☢ ☍ ⌘)
+ "method.set_key = ui.column.render, \"100:1C95/1:❢\","
+ " ((array.at, {\" \", \"♺\", \"ʘ\", \"⚠\", \"◔\", \"⇕\", \"↯\", \"¿\","
+ " \"⨂\", \"⋫\", \"☡\"}, ((d.message.alert)) ))\n"
+ "method.set_key = ui.column.render, \"110:?1C92/1:☢\","
+ " ((string.map, ((cat, ((d.is_open)), ((d.is_active)))), {00, \"▪\"}, {01, \"▪\"}, {10, \"╍\"}, {11, \"▹\"}))\n"
+ "method.set_key = ui.column.render, \"120:?1:☍\","
+ " ((array.at, {\"⚯\", \" \"}, ((not, ((d.tied_to_file)) )) ))\n"
+ "method.set_key = ui.column.render, \"130:?1:⌘\","
+ " ((array.at, {\"⚒\", \"◌\"}, ((d.ignore_commands)) ))\n"
+
+ // Scrape info (↺ ⤴ ⤵)
+ "method.set_key = ui.column.render, \"200:?2C23/2: ↺\", ((convert.magnitude, ((d.tracker_scrape.downloaded)) ))\n"
+ "method.set_key = ui.column.render, \"210:?2C15/2: ⤴\", ((convert.magnitude, ((d.tracker_scrape.complete)) ))\n"
+ "method.set_key = ui.column.render, \"220:?2C14/2: ⤵\", ((convert.magnitude, ((d.tracker_scrape.incomplete)) ))\n"
+
+ // Traffic indicator (↕)
+ "method.set_key = ui.column.render, \"300:?1:↕\","
+ " ((string.map, ((cat, ((not, ((d.up.rate)) )), ((not, ((d.down.rate)) )) )),"
+ " {00, \"⇅\"}, {01, \"↟\"}, {10, \"↡\"}, {11, \" \"} ))\n"
+
+ // Number of connected peers (℞)
+ "method.set_key = ui.column.render, \"400:?2C28/2: ℞\", ((convert.magnitude, ((d.peers_connected)) ))\n"
+
+ // Up|Last Active Time (∆⋮ ⟲)
+ "method.set_key = ui.column.render, \"500:5C70/5: ∆⋮ ⟲\","
+ " ((if, ((d.up.rate)),"
+ " ((cat, \" \", ((convert.human_size, ((d.up.rate)), ((value, 10)) )) )),"
+ " ((if, ((d.peers_connected)), ((cat, \" 0”\")),"
+ " ((convert.time_delta, ((value, ((d.custom, last_active)) )) )) ))"
+ " ))\n"
+
+ // Upload total (Σ⇈)
+ "method.set_key = ui.column.render, \"600:?6C23/5C21/1: Σ⇈ \","
+ " ((if, ((d.up.total)),"
+ " ((convert.human_size, ((d.up.total)), (value, 0))),"
+ " ((cat, \" ⋅ \"))"
+ " ))\n"
+
+ // Down|Completion or Loaded Time (∇⋮ ◷)
+ // TODO: Could use "d.timestamp.started" and "d.timestamp.finished" here, but need to check
+ // when they were introduced, and if they're always set (e.g. what about fast-resumed items?)
+ "method.set_key = ui.column.render, \"700:5C90/5: ∇⋮ ◷\","
+ " ((if, ((d.down.rate)),"
+ " ((cat, \" \", ((convert.human_size, ((d.down.rate)), ((value, 10)) )) )),"
+ " ((convert.time_delta, ((value, ((d.custom.if_z, tm_completed, ((d.custom, tm_loaded)) )) )) ))"
+ " ))\n"
+
+ // Data size (⛁)
+ "method.set_key = ui.column.render, \"800:4C15/3C21/1: ⛁ \","
+#if RT_HEX_VERSION <= 0x000906
+ " ((convert.human_size, ((d.size_bytes)) ))\n"
+#else
+ " ((convert.human_size, ((d.selected_size_bytes)) ))\n"
+#endif
+
+ // Progress (⣿)
+ "method.set_key = ui.column.render, \"900:1C94/1:⣿\","
+ " ((string.substr, \" ⠁⠉⠋⠛⠟⠿⡿⣿❚\", ((math.div, ((math.mul, ((d.completed_chunks)), 10)), ((math.add, ((d.completed_chunks)), ((d.wanted_chunks)))) )), 1, \"✔\"))\n"
+ // " ⠁⠉⠋⠛⠟⠿⡿⣿❚"
+ //⠀" ▁▂▃▄▅▆▇█❚"
+
+ // Ratio (☯)
+ "method.set_key = ui.column.render, \"920:1C93/1:☯\","
+ " ((string.substr, \"☹➀➁➂➃➄➅➆➇➈➉\", ((math.div, ((d.ratio)), 1000)), 1, \"⊛\"))\n"
+ // "☹➀➁➂➃➄➅➆➇➈➉"
+ // "☹①②③④⑤⑥⑦⑧⑨⑩"
+ // "☹➊➋➌➍➎➏➐➑➒➓"
+
+ // Explicitly managed status (✰ = prio; ⊘ = throttle name; ⚑ = tagged)
+ "method.set_key = ui.column.render, \"940:1C91/1:✰\","
+ " ((array.at, {\"✖\", \"⇣\", \" \", \"⇡\"}, ((d.priority)) ))\n"
+ "method.set_key = ui.column.render, \"950:1:⊘\","
+ " {(branch, ((equal,((d.throttle_name)),((cat,NULL)))), ((cat, \"∞\")), ((d.throttle_name)) )}\n"
+ "method.set_key = ui.column.render, \"980:1C16/1:⚑\","
+ " ((array.at, {\" \", \"⚑\"}, ((d.views.has, tagged)) ))\n"
+ );
+
+ //printf("%s", init_commands.c_str());
+ rpc::parse_command_multiple(rpc::make_target(), init_commands.c_str());
+}
diff --git a/ui_pyroscope.h b/ui_pyroscope.h
new file mode 100644
index 000000000000..8cdf71278dec
--- /dev/null
+++ b/ui_pyroscope.h
@@ -0,0 +1,84 @@
+#ifndef UI_PYROSCOPE_H
+#define UI_PYROSCOPE_H
+
+#include <string>
+
+
+namespace ps {
+
+#define COL_SYS_BASE_CH 70
+#define COL_SYS_BASE 90
+
+enum AlertKind {
+ ALERT_NORMAL,
+ ALERT_NORMAL_CYCLING, // Tried all trackers
+ ALERT_NORMAL_GHOST, // no data
+ ALERT_GENERIC,
+ ALERT_TIMEOUT,
+ ALERT_CONNECT,
+ ALERT_REQUEST,
+ ALERT_GONE,
+ ALERT_PERMS,
+ ALERT_DOWN,
+ ALERT_DNS,
+ ALERT_MAX
+};
+
+
+enum ColorKind {
+ COL_DEFAULT,
+ COL_CUSTOM1,
+ COL_CUSTOM2,
+ COL_CUSTOM3,
+ COL_CUSTOM4,
+ COL_CUSTOM5,
+ COL_CUSTOM6,
+ COL_CUSTOM7,
+ COL_CUSTOM8,
+ COL_CUSTOM9,
+ COL_PROGRESS0, // 10
+ COL_PROGRESS20,
+ COL_PROGRESS40,
+ COL_PROGRESS60,
+ COL_PROGRESS80,
+ COL_PROGRESS100,
+ COL_PROGRESS120,
+ COL_TITLE,
+ COL_FOOTER,
+ COL_FOCUS,
+ COL_LABEL, // 20
+ COL_INFO,
+ COL_ALARM,
+ COL_COMPLETE,
+ COL_SEEDING,
+ COL_STOPPED,
+ COL_QUEUED,
+ COL_INCOMPLETE,
+ COL_LEECHING,
+ COL_ODD,
+ COL_EVEN,
+ COL_MAX,
+
+ COL_ACTIVE_TIME = COL_SYS_BASE_CH,
+ COL_UNSAFE_DATA,
+ COL_THROTTLE_CH,
+ COL_ETA_TIME,
+
+ COL_DOWN_TIME = COL_SYS_BASE,
+ COL_PRIO,
+ COL_STATE,
+ COL_RATIO,
+ COL_PROGRESS,
+ COL_ALERT,
+ COL_UP_TIME,
+ COL_SYS_MAX
+};
+
+} // namespace
+
+// defined in command_pyroscope.cc (exported here so we only have to patch in one .h)
+extern void add_capability(const char* name);
+extern size_t u8_length(const std::string& text);
+extern std::string u8_chop(const std::string& text, size_t glyphs);
+
+#endif
diff --git a/ui_pyroscope_all.patch b/ui_pyroscope_all.patch
new file mode 100644
index 000000000000..483ccb7cc062
--- /dev/null
+++ b/ui_pyroscope_all.patch
@@ -0,0 +1,83 @@
+--- rel-0.9.7/src/Makefile.am 2016-08-30 16:58:31.444054971 +0100
++++ rtorrent-0.9.7/src/Makefile.am 2016-08-30 16:57:49.924279903 +0100
+@@ -30,6 +30,7 @@ libsub_root_a_SOURCES = \
+ control.h \
+ globals.cc \
+ globals.h \
++ ui_pyroscope.cc \
+ option_parser.cc \
+ option_parser.h \
+ signal_handler.cc \
+--- rel-0.9.7/src/command_helpers.cc 2016-08-30 16:59:41.970339564 +0100
++++ rtorrent-0.9.7/src/command_helpers.cc 2016-08-30 16:56:49.281275101 +0100
+@@ -57,6 +57,7 @@ void initialize_command_throttle();
+ void initialize_command_tracker();
+ void initialize_command_scheduler();
+ void initialize_command_ui();
++void initialize_command_ui_pyroscope();
+
+ void
+ initialize_commands() {
+@@ -75,4 +76,5 @@ initialize_commands() {
+ initialize_command_throttle();
+ initialize_command_tracker();
+ initialize_command_scheduler();
++ initialize_command_ui_pyroscope();
+ }
+--- rel-0.9.7/src/display/canvas.h 2009-11-12 09:03:47.000000000 +0100
++++ rtorrent-0.9.7/src/display/canvas.h 2016-08-28 12:47:17.252596654 +0100
+@@ -137,8 +137,10 @@ Canvas::print(unsigned int x, unsigned i
+
+ if (!m_isDaemon) {
+ va_start(arglist, str);
+- wmove(m_window, y, x);
+- vw_printw(m_window, const_cast<char*>(str), arglist);
++ if (y < height()) {
++ wmove(m_window, y, x);
++ vw_printw(m_window, const_cast<char*>(str), arglist);
++ }
+ va_end(arglist);
+ }
+ }
+--- rel-0.9.7/src/display/window_statusbar.cc 2016-08-30 18:59:56.054590848 +0100
++++ rtorrent-0.9.7/src/display/window_statusbar.cc 2016-08-30 16:16:24.554410975 +0100
+@@ -67,6 +67,8 @@ WindowStatusbar::redraw() {
+ m_canvas->print(m_canvas->width() - (position - buffer), 0, "%s", buffer);
+ }
+
++ void ui_pyroscope_statusbar_redraw(Window* window, display::Canvas* canvas);
++ ui_pyroscope_statusbar_redraw(this, m_canvas);
+ m_lastTick = control->tick();
+ }
+
+--- rel-0.9.7/src/display/window_download_list.cc 2016-08-26 10:06:55.000000000 +0100
++++ rtorrent-0.9.7/src/display/window_download_list.cc 2016-08-30 12:27:10.745588420 +0100
+@@ -83,6 +83,8 @@ WindowDownloadList::redraw() {
+
+ m_canvas->print(0, 0, "%s", ("[View: " + m_view->name() + (m_view->get_temp_filter().is_empty() ? "" : " (filtered)") + "]").c_str());
+
++ bool ui_pyroscope_download_list_redraw(Window* window, display::Canvas* canvas, core::View* view);
++ if (ui_pyroscope_download_list_redraw(this, m_canvas, m_view)) return;
+ if (m_view->empty_visible() || m_canvas->width() < 5 || m_canvas->height() < 2)
+ return;
+
+@@ -132,6 +134,8 @@ WindowDownloadList::redraw() {
+ print_download_status(buffer, last, *range.first);
+ m_canvas->print(0, pos++, "%c %s", range.first == m_view->focus() ? '*' : ' ', buffer);
+
++ void ui_pyroscope_download_list_redraw_item(Window* window, display::Canvas* canvas, core::View* view, int pos, Range& range);
++ ui_pyroscope_download_list_redraw_item(this, m_canvas, m_view, pos, range);
+ range.first++;
+ }
+
+--- rel-0.9.7/src/display/canvas.cc 2016-08-30 17:01:19.156479729 +0100
++++ rtorrent-0.9.7/src/display/canvas.cc 2016-08-30 16:48:56.697168643 +0100
+@@ -111,6 +111,8 @@ Canvas::initialize() {
+
+ if (!m_isDaemon) {
+ initscr();
++ extern void ui_pyroscope_canvas_init();
++ ui_pyroscope_canvas_init();
+ raw();
+ noecho();
+ nodelay(stdscr, TRUE);