diff --git a/src/command_ui.cc b/src/command_ui.cc index b5fd6bdde..12f80b52e 100644 --- a/src/command_ui.cc +++ b/src/command_ui.cc @@ -764,6 +764,10 @@ 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 ("ui.input.history.size", std::bind(&ui::Root::get_input_history_size, control->ui())); + CMD2_ANY_VALUE_V("ui.input.history.size.set", std::bind(&ui::Root::set_input_history_size, control->ui(), std::placeholders::_2)); + CMD2_ANY_V ("ui.input.history.clear", std::bind(&ui::Root::clear_input_history, control->ui())); + CMD2_VAR_VALUE ("ui.throttle.global.step.small", 5); CMD2_VAR_VALUE ("ui.throttle.global.step.medium", 50); CMD2_VAR_VALUE ("ui.throttle.global.step.large", 500); diff --git a/src/core/download_list.cc b/src/core/download_list.cc index 3047888c6..680810d2b 100644 --- a/src/core/download_list.cc +++ b/src/core/download_list.cc @@ -63,6 +63,7 @@ #include "download.h" #include "download_list.h" #include "download_store.h" +#include "ui/root.h" #define DL_TRIGGER_EVENT(download, event_name) \ rpc::commands.call_catch(event_name, rpc::make_target(download), torrent::Object(), "Event '" event_name "' failed: "); @@ -93,6 +94,7 @@ DownloadList::session_save() { lt_log_print(torrent::LOG_ERROR, "Failed to save session torrents."); control->dht_manager()->save_dht_cache(); + control->ui()->save_input_history(); } DownloadList::iterator diff --git a/src/input/text_input.cc b/src/input/text_input.cc index 206470eb5..1d1462081 100644 --- a/src/input/text_input.cc +++ b/src/input/text_input.cc @@ -47,22 +47,6 @@ TextInput::pressed(int key) { if (m_bindings.pressed(key)) { return true; - } else if (m_alt) { - m_alt = false; - - switch (key) { -// case 'b': -// Base::insert(m_pos, "M^b"); -// break; - -// case 'f': -// Base::insert(m_pos, "M^f"); -// break; - - default: - return false; - } - } else if (key >= 0x20 && key < 0x7F) { Base::insert(m_pos++, 1, key); @@ -113,11 +97,6 @@ TextInput::pressed(int key) { Base::erase(m_pos, size()-m_pos); break; - case 0x1B: - m_alt = true; - - break; - default: return false; } diff --git a/src/input/text_input.h b/src/input/text_input.h index 511aca9b3..f703e60ef 100644 --- a/src/input/text_input.h +++ b/src/input/text_input.h @@ -54,7 +54,7 @@ class TextInput : private std::string { using Base::size_type; using Base::npos; - TextInput() : m_pos(0), m_alt(false) {} + TextInput() : m_pos(0) {} virtual ~TextInput() {} size_type get_pos() { return m_pos; } @@ -62,7 +62,7 @@ class TextInput : private std::string { virtual bool pressed(int key); - void clear() { m_pos = 0; m_alt = false; Base::clear(); } + void clear() { m_pos = 0; Base::clear(); } void slot_dirty(slot_void s) { m_slot_dirty = s; } void mark_dirty() { if (m_slot_dirty) m_slot_dirty(); } @@ -74,7 +74,6 @@ class TextInput : private std::string { private: size_type m_pos; - bool m_alt; slot_void m_slot_dirty; Bindings m_bindings; diff --git a/src/main.cc b/src/main.cc index cc2421e89..6be6a4dee 100644 --- a/src/main.cc +++ b/src/main.cc @@ -66,6 +66,7 @@ #include "display/window.h" #include "display/manager.h" #include "input/bindings.h" +#include "ui/root.h" #include "rpc/command_scheduler.h" #include "rpc/command_scheduler_item.h" @@ -466,6 +467,7 @@ main(int argc, char** argv) { } control->initialize(); + control->ui()->load_input_history(); // Load session torrents and perform scheduled tasks to ensure // session torrents are loaded before arg torrents. diff --git a/src/ui/download_list.cc b/src/ui/download_list.cc index 003f9cd01..25f89fc4e 100644 --- a/src/ui/download_list.cc +++ b/src/ui/download_list.cc @@ -274,21 +274,31 @@ DownloadList::receive_view_input(Input type) { std::placeholders::_1, std::placeholders::_2)); + // reset ESC delay for input prompt + set_escdelay(0); + input->bindings()['\n'] = std::bind(&DownloadList::receive_exit_input, this, type); input->bindings()[KEY_ENTER] = std::bind(&DownloadList::receive_exit_input, this, type); - input->bindings()['\x07'] = std::bind(&DownloadList::receive_exit_input, this, INPUT_NONE); + input->bindings()['\x07'] = std::bind(&DownloadList::receive_exit_input, this, INPUT_NONE); // ^G + input->bindings()['\x1B'] = std::bind(&DownloadList::receive_exit_input, this, INPUT_NONE); // ESC , ^[ - control->ui()->enable_input(title, input); + control->ui()->enable_input(title, input, type); } void DownloadList::receive_exit_input(Input type) { + // set back ESC delay to default + set_escdelay(1000); + input::TextInput* input = control->ui()->current_input(); // We should check that this object is the one holding the input. if (input == NULL) return; + if (type != INPUT_NONE && type != INPUT_EOI) + control->ui()->add_to_input_history(type, input->str()); + control->ui()->disable_input(); try { diff --git a/src/ui/download_list.h b/src/ui/download_list.h index 72ab5ae57..7dbb8bfd5 100644 --- a/src/ui/download_list.h +++ b/src/ui/download_list.h @@ -86,7 +86,8 @@ class DownloadList : public ElementBase { INPUT_LOAD_DEFAULT, INPUT_LOAD_MODIFIED, INPUT_CHANGE_DIRECTORY, - INPUT_COMMAND + INPUT_COMMAND, + INPUT_EOI } Input; DownloadList(); diff --git a/src/ui/root.cc b/src/ui/root.cc index 29e45d3f2..7813b6b60 100644 --- a/src/ui/root.cc +++ b/src/ui/root.cc @@ -36,11 +36,14 @@ #include "config.h" +#include #include #include +#include #include #include #include +#include #include "core/manager.h" #include "display/frame.h" @@ -54,6 +57,7 @@ #include "control.h" #include "download_list.h" +#include "core/download_store.h" #include "root.h" @@ -65,7 +69,17 @@ Root::Root() : m_windowTitle(NULL), m_windowHttpQueue(NULL), m_windowInput(NULL), - m_windowStatusbar(NULL) { + m_windowStatusbar(NULL), + m_input_history_length(99), + m_input_history_pointer_get(0), + m_input_history_last_input("") { + + // Initialise prefilled m_input_history and m_input_history_pointers objects. + for (int type = ui::DownloadList::INPUT_LOAD_DEFAULT; type != ui::DownloadList::INPUT_EOI; type++) { + m_input_history.insert( std::make_pair(type, InputHistoryCategory(m_input_history_length)) ); + m_input_history_pointers.insert( std::make_pair(type, 0) ); + } + } void @@ -230,7 +244,7 @@ Root::adjust_up_throttle(int throttle) { } void -Root::enable_input(const std::string& title, input::TextInput* input) { +Root::enable_input(const std::string& title, input::TextInput* input, ui::DownloadList::Input type) { if (m_windowInput->input() != NULL) throw torrent::internal_error("Root::enable_input(...) m_windowInput->input() != NULL."); @@ -243,8 +257,14 @@ Root::enable_input(const std::string& title, input::TextInput* input) { m_windowInput->set_title(title); m_windowInput->set_focus(true); + reset_input_history_attributes(type); + input->bindings()['\x0C'] = std::bind(&display::Manager::force_redraw, m_control->display()); // ^L input->bindings()['\x11'] = std::bind(&Control::receive_normal_shutdown, m_control); // ^Q + input->bindings()[KEY_UP] = std::bind(&Root::prev_in_input_history, this, type); // UP arrow + input->bindings()['\x10'] = std::bind(&Root::prev_in_input_history, this, type); // ^P + input->bindings()[KEY_DOWN] = std::bind(&Root::next_in_input_history, this, type); // DOWN arrow + input->bindings()['\x0E'] = std::bind(&Root::next_in_input_history, this, type); // ^N control->input()->set_text_input(input); control->display()->adjust_layout(); @@ -272,4 +292,216 @@ Root::current_input() { return m_windowInput->input(); } +void +Root::add_to_input_history(ui::DownloadList::Input type, std::string item) { + InputHistory::iterator itr = m_input_history.find(type); + InputHistoryPointers::iterator pitr = m_input_history_pointers.find(type); + int prev_item_pointer = (pitr->second - 1) < 0 ? (m_input_history_length - 1) : (pitr->second - 1); + + // Don't store item if it's empty or the same as the last one in the category. + if (!item.empty() && item != itr->second.at(prev_item_pointer)) { + itr->second.at(pitr->second) = rak::trim(item); + m_input_history_pointers[type] = (pitr->second + 1) % m_input_history_length; + } +} + +void +Root::prev_in_input_history(ui::DownloadList::Input type) { + if (m_windowInput->input() == NULL) + throw torrent::internal_error("Root::prev_in_input_history() m_windowInput->input() == NULL."); + + InputHistory::iterator itr = m_input_history.find(type); + InputHistoryPointers::const_iterator pitr = m_input_history_pointers.find(type); + + if (m_input_history_pointer_get == pitr->second) + m_input_history_last_input = m_windowInput->input()->str(); + else + itr->second.at(m_input_history_pointer_get) = m_windowInput->input()->str(); + + std::string tmp_input = m_input_history_last_input; + int prev_pointer_get = (m_input_history_pointer_get - 1) < 0 ? (m_input_history_length - 1) : (m_input_history_pointer_get - 1); + + if (prev_pointer_get != pitr->second && itr->second.at(prev_pointer_get) != "") + m_input_history_pointer_get = prev_pointer_get; + + if (m_input_history_pointer_get != pitr->second) + tmp_input = itr->second.at(m_input_history_pointer_get); + + m_windowInput->input()->str() = tmp_input; + m_windowInput->input()->set_pos(tmp_input.length()); + m_windowInput->input()->mark_dirty(); +} + +void +Root::next_in_input_history(ui::DownloadList::Input type) { + if (m_windowInput->input() == NULL) + throw torrent::internal_error("Root::next_in_input_history() m_windowInput->input() == NULL."); + + InputHistory::iterator itr = m_input_history.find(type); + InputHistoryPointers::const_iterator pitr = m_input_history_pointers.find(type); + + if (m_input_history_pointer_get == pitr->second) + m_input_history_last_input = m_windowInput->input()->str(); + else + itr->second.at(m_input_history_pointer_get) = m_windowInput->input()->str(); + + std::string tmp_input = m_input_history_last_input; + + if (m_input_history_pointer_get != pitr->second) { + m_input_history_pointer_get = (m_input_history_pointer_get + 1) % m_input_history_length; + tmp_input = (m_input_history_pointer_get == pitr->second) ? m_input_history_last_input : itr->second.at(m_input_history_pointer_get); + } + + m_windowInput->input()->str() = tmp_input; + m_windowInput->input()->set_pos(tmp_input.length()); + m_windowInput->input()->mark_dirty(); +} + +void +Root::reset_input_history_attributes(ui::DownloadList::Input type) { + InputHistoryPointers::const_iterator itr = m_input_history_pointers.find(type); + + // Clear last_input and set pointer_get to the same as pointer_insert. + m_input_history_last_input = ""; + m_input_history_pointer_get = itr->second; +} + +void +Root::set_input_history_size(int size) { + if (size < 1) + throw torrent::input_error("Invalid input history size."); + + for (InputHistory::iterator itr = m_input_history.begin(), last = m_input_history.end(); itr != last; itr++) { + // Reserve the latest input history entries if new size is smaller than original. + if (size < m_input_history_length) { + int pointer_offset = m_input_history_length - size; + InputHistoryPointers::iterator pitr = m_input_history_pointers.find(itr->first); + InputHistoryCategory input_history_category_tmp = itr->second; + + for (int i=0; i != size; i++) + itr->second.at(i) = input_history_category_tmp.at((pitr->second + pointer_offset + i) % m_input_history_length); + + m_input_history_pointers[pitr->first] = 0; + } + + itr->second.resize(size); + } + + m_input_history_length = size; +} + +void +Root::load_input_history() { + if (!m_control->core()->download_store()->is_enabled()) { + lt_log_print(torrent::LOG_DEBUG, "ignoring input history file"); + return; + } + + std::string history_filename = m_control->core()->download_store()->path() + "rtorrent.input_history"; + std::fstream history_file(history_filename.c_str(), std::ios::in); + + if (history_file.is_open()) { + // Create a temp object of the content since size of history categories can be smaller than this. + InputHistory input_history_tmp; + + for (int type = ui::DownloadList::INPUT_LOAD_DEFAULT; type != ui::DownloadList::INPUT_EOI; type++) + input_history_tmp.insert( std::make_pair(type, InputHistoryCategory()) ); + + std::string line; + + while (std::getline(history_file, line)) { + if (!line.empty()) { + int delim_pos = line.find("|"); + + if (delim_pos != std::string::npos) { + int type = std::atoi(line.substr(0, delim_pos).c_str()); + InputHistory::iterator itr = input_history_tmp.find(type); + + if (itr != input_history_tmp.end()) { + std::string input_str = rak::trim(line.substr(delim_pos + 1)); + + if (!input_str.empty()) + itr->second.push_back(input_str); + } + } + } + } + + if (history_file.bad()) { + lt_log_print(torrent::LOG_DEBUG, "input history file corrupted, discarding (path:%s)", history_filename.c_str()); + return; + } else { + lt_log_print(torrent::LOG_DEBUG, "input history file read (path:%s)", history_filename.c_str()); + } + + for (InputHistory::const_iterator itr = input_history_tmp.begin(), last = input_history_tmp.end(); itr != last; itr++) { + int input_history_tmp_category_length = itr->second.size(); + InputHistory::iterator hitr = m_input_history.find(itr->first); + InputHistoryPointers::iterator pitr = m_input_history_pointers.find(itr->first); + + if (m_input_history_length < input_history_tmp_category_length) { + int pointer_offset = input_history_tmp_category_length - m_input_history_length; + + for (int i=0; i != m_input_history_length; i++) + hitr->second.at(i) = itr->second.at((pointer_offset + i) % input_history_tmp_category_length); + + pitr->second = 0; + } else { + hitr->second = itr->second; + hitr->second.resize(m_input_history_length); + + pitr->second = input_history_tmp_category_length % m_input_history_length; + } + } + } else { + lt_log_print(torrent::LOG_DEBUG, "could not open input history file (path:%s)", history_filename.c_str()); + } +} + +void +Root::save_input_history() { + if (!m_control->core()->download_store()->is_enabled()) + return; + + std::string history_filename = m_control->core()->download_store()->path() + "rtorrent.input_history"; + std::string history_filename_tmp = history_filename + ".new"; + std::fstream history_file(history_filename_tmp.c_str(), std::ios::out | std::ios::trunc); + + if (!history_file.is_open()) { + lt_log_print(torrent::LOG_DEBUG, "could not open input history file for writing (path:%s)", history_filename.c_str()); + return; + } + + for (InputHistory::const_iterator itr = m_input_history.begin(), last = m_input_history.end(); itr != last; itr++) { + InputHistoryPointers::const_iterator pitr = m_input_history_pointers.find(itr->first); + + for (int i=0; i != m_input_history_length; i++) + if (!itr->second.at((pitr->second + i) % m_input_history_length).empty()) + history_file << itr->first << "|" + itr->second.at((pitr->second + i) % m_input_history_length) + "\n"; + } + + if (!history_file.good()) { + lt_log_print(torrent::LOG_DEBUG, "input history file corrupted during writing, discarding (path:%s)", history_filename.c_str()); + return; + } else { + lt_log_print(torrent::LOG_DEBUG, "input history file written (path:%s)", history_filename.c_str()); + } + + history_file.close(); + + std::rename(history_filename_tmp.c_str(), history_filename.c_str()); +} + +void +Root::clear_input_history() { + for (int type = ui::DownloadList::INPUT_LOAD_DEFAULT; type != ui::DownloadList::INPUT_EOI; type++) { + InputHistory::iterator itr = m_input_history.find(type); + + for (int i=0; i != m_input_history_length; i++) + itr->second.at(i) = ""; + + m_input_history_pointers[type] = 0; + } +} + } diff --git a/src/ui/root.h b/src/ui/root.h index cbc5ff439..262de3789 100644 --- a/src/ui/root.h +++ b/src/ui/root.h @@ -39,6 +39,7 @@ #include #include "input/bindings.h" +#include "download_list.h" class Control; @@ -65,6 +66,10 @@ class Root { typedef display::WindowInput WInput; typedef display::WindowStatusbar WStatusbar; + typedef std::map InputHistoryPointers; + typedef std::vector InputHistoryCategory; + typedef std::map InputHistory; + Root(); void init(Control* c); @@ -88,11 +93,19 @@ class Root { const char* get_throttle_keys(); - void enable_input(const std::string& title, input::TextInput* input); + void enable_input(const std::string& title, input::TextInput* input, ui::DownloadList::Input type); void disable_input(); input::TextInput* current_input(); + int get_input_history_size() { return m_input_history_length; } + void set_input_history_size(int size); + void add_to_input_history(ui::DownloadList::Input type, std::string item); + + void load_input_history(); + void save_input_history(); + void clear_input_history(); + private: void setup_keys(); @@ -105,6 +118,17 @@ class Root { WStatusbar* m_windowStatusbar; input::Bindings m_bindings; + + int m_input_history_length; + std::string m_input_history_last_input; + int m_input_history_pointer_get; + InputHistory m_input_history; + InputHistoryPointers m_input_history_pointers; + + void prev_in_input_history(ui::DownloadList::Input type); + void next_in_input_history(ui::DownloadList::Input type); + + void reset_input_history_attributes(ui::DownloadList::Input type); }; }