diff options
author | qdesjardin | 2019-10-17 11:53:12 -0600 |
---|---|---|
committer | qdesjardin | 2019-10-17 12:15:20 -0600 |
commit | be66c45ee1342caf8b69464f05f97664d55a4654 (patch) | |
tree | 58a6f0f98efe85b2b6430f8c73f341632f0085a8 | |
download | aur-be66c45ee1342caf8b69464f05f97664d55a4654.tar.gz |
Added .SRCINFO file.
-rw-r--r-- | .SRCINFO | 59 | ||||
-rwxr-xr-x | PKGBUILD | 115 | ||||
-rw-r--r-- | backport_rt_all_02-display_throttle_speed.patch | 263 | ||||
-rw-r--r-- | backport_rt_all_04-partially_done_and_choke_group_fix.patch | 653 | ||||
-rw-r--r-- | backport_rt_all_05-honor_system_file_allocate_fix.patch | 202 | ||||
-rw-r--r-- | backport_rt_all_08-info_pane_xb_sizes.patch | 76 | ||||
-rw-r--r-- | backport_rt_all_09-inotify_mod.patch | 137 | ||||
-rw-r--r-- | backport_rt_all_80-ps-dl-ui-find.patch | 51 | ||||
-rw-r--r-- | command_pyroscope.cc | 1370 | ||||
-rw-r--r-- | ps-import.return_all.patch | 10 | ||||
-rw-r--r-- | ps-include-timestamps_all.patch | 18 | ||||
-rw-r--r-- | ps-info-pane-is-default_all.patch | 12 | ||||
-rw-r--r-- | ps-issue-515_all.patch | 29 | ||||
-rw-r--r-- | ps-item-stats-human-sizes_all.patch | 35 | ||||
-rw-r--r-- | ps-log_messages_all.patch | 33 | ||||
-rw-r--r-- | ps-object_std-map-serialization_all.patch | 24 | ||||
-rw-r--r-- | ps-silent-catch_all.patch | 18 | ||||
-rw-r--r-- | ps-ui_pyroscope_all.patch | 7 | ||||
-rw-r--r-- | pyroscope_all.patch | 26 | ||||
-rw-r--r-- | ui_pyroscope.cc | 1445 | ||||
-rw-r--r-- | ui_pyroscope.h | 84 | ||||
-rw-r--r-- | ui_pyroscope_all.patch | 83 |
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); |