#include #include #include #include #include #include #include #include #include #include #include namespace newsbeuter { itemview_formaction::itemview_formaction(view * vv, std::tr1::shared_ptr il, std::string formstr) : formaction(vv,formstr), show_source(false), quit(false), rxman(0), num_lines(0), itemlist(il), in_search(false) { valid_cmds.push_back("save"); std::sort(valid_cmds.begin(), valid_cmds.end()); } itemview_formaction::~itemview_formaction() { } void itemview_formaction::init() { f->set("msg",""); do_redraw = true; quit = false; links.clear(); num_lines = 0; if (!v->get_cfg()->get_configvalue_as_bool("display-article-progress")) { f->set("percentwidth", "0"); } else { f->set("percentwidth", utils::to_s(utils::max(6, utils::max(strlen(_("Top")), strlen(_("Bottom")))))); update_percent(); } set_keymap_hints(); } void itemview_formaction::prepare() { /* * whenever necessary, the item view is regenerated. This is done * by putting together the feed name, title, link, author, optional * flags and podcast download URL (enclosures) and then render the * HTML. The links extracted by the renderer are then appended, too. */ if (do_redraw) { { scope_measure("itemview::prepare: rendering"); f->run(-3); // XXX HACK: render once so that we get a proper widget width } std::vector lines; std::string widthstr = f->get("article:w"); unsigned int render_width = 80; unsigned int view_width = 0; if (widthstr.length() > 0) { view_width = render_width = utils::to_u(widthstr); if (render_width - 5 > 0) render_width -= 5; } std::tr1::shared_ptr item = feed->get_item_by_guid(guid); listformatter listfmt; std::tr1::shared_ptr feedptr = item->get_feedptr(); std::string title, feedtitle; if (feedptr.get() != NULL) { if (feedptr->title().length() > 0) { title = feedptr->title(); } else if (feedptr->link().length() > 0) { title = feedptr->link(); } else if (feedptr->rssurl().length() > 0) { title = feedptr->rssurl(); } } if (title.length() > 0) { feedtitle = utils::strprintf("%s%s", _("Feed: "), title.c_str()); listfmt.add_line(feedtitle, UINT_MAX, view_width); } if (item->title().length() > 0) { title = utils::strprintf("%s%s", _("Title: "), item->title().c_str()); listfmt.add_line(title, UINT_MAX, view_width); } if (item->author().length() > 0) { std::string author = utils::strprintf("%s%s", _("Author: "), item->author().c_str()); listfmt.add_line(author, UINT_MAX, view_width); } if (item->link().length() > 0) { std::string link = utils::strprintf("%s%s", _("Link: "), utils::censor_url(item->link()).c_str()); listfmt.add_line(link, UINT_MAX, view_width); } std::string date = utils::strprintf("%s%s", _("Date: "), item->pubDate().c_str()); listfmt.add_line(date, UINT_MAX, view_width); if (item->flags().length() > 0) { std::string flags = utils::strprintf("%s%s", _("Flags: "), item->flags().c_str()); listfmt.add_line(flags, UINT_MAX, view_width); } if (item->enclosure_url().length() > 0) { std::string enc_url = utils::strprintf("%s%s", _("Podcast Download URL: "), utils::censor_url(item->enclosure_url()).c_str()); if (item->enclosure_type() != "") { enc_url.append(utils::strprintf(" (%s%s)", _("type: "), item->enclosure_type().c_str())); } listfmt.add_line(enc_url, UINT_MAX, view_width); } listfmt.add_line(""); set_head(item->title()); unsigned int textwidth = v->get_cfg()->get_configvalue_as_int("text-width"); if (textwidth > 0) { render_width = textwidth; } if (show_source) { render_source(lines, utils::quote_for_stfl(item->description()), render_width); } else { std::string baseurl = item->get_base() != "" ? item->get_base() : item->feedurl(); lines = render_html(item->description(), links, baseurl, render_width); } listfmt.add_lines(lines, view_width); num_lines = listfmt.get_lines_count(); f->modify("article","replace_inner",listfmt.format_list(rxman, "article")); f->set("articleoffset","0"); if (in_search) { rxman->remove_last_regex("article"); in_search = false; } do_redraw = false; } } void itemview_formaction::process_operation(operation op, bool automatic, std::vector * args) { std::tr1::shared_ptr item = feed->get_item_by_guid(guid); bool hardquit = false; /* * whenever we process an operation, we mark the item * as read. Don't worry: when an item is already marked as * read, and then marked as read again, no database update * is done, since only _changes_ to the unread flag are * recorded in the database. */ try { bool old_unread = item->unread(); item->set_unread(false); if (old_unread) { v->get_ctrl()->mark_article_read(item->guid(), true); } } catch (const dbexception& e) { v->show_error(utils::strprintf(_("Error while marking article as read: %s"), e.what())); } switch (op) { case OP_TOGGLESOURCEVIEW: LOG(LOG_INFO, "view::run_itemview: toggling source view"); show_source = !show_source; do_redraw = true; break; case OP_ENQUEUE: { if (item->enclosure_url().length() > 0 && utils::is_http_url(item->enclosure_url())) { v->get_ctrl()->enqueue_url(item->enclosure_url(), feed); v->set_status(utils::strprintf(_("Added %s to download queue."), item->enclosure_url().c_str())); } else { v->set_status(utils::strprintf(_("Invalid URL: '%s'"), item->enclosure_url().c_str())); } } break; case OP_SAVE: { LOG(LOG_INFO, "view::run_itemview: saving article"); std::string filename; if (automatic) { if (args->size() > 0) filename = (*args)[0]; } else { filename = v->run_filebrowser(v->get_filename_suggestion(item->title())); } if (filename == "") { v->show_error(_("Aborted saving.")); } else { try { v->get_ctrl()->write_item(item, filename); v->show_error(utils::strprintf(_("Saved article to %s."), filename.c_str())); } catch (...) { v->show_error(utils::strprintf(_("Error: couldn't write article to file %s"), filename.c_str())); } } } break; case OP_OPENINBROWSER: LOG(LOG_INFO, "view::run_itemview: starting browser"); v->set_status(_("Starting browser...")); v->open_in_browser(item->link()); v->set_status(""); break; case OP_BOOKMARK: if (automatic) { qna_responses.clear(); qna_responses.push_back(item->title()); qna_responses.push_back(item->link()); qna_responses.push_back(args->size() > 0 ? (*args)[0] : ""); } else { this->start_bookmark_qna(item->title(), item->link(), ""); } break; case OP_SEARCH: { std::vector qna; if (automatic) { if (args->size() > 0) { qna_responses.clear(); qna_responses.push_back((*args)[0]); finished_qna(OP_INT_START_SEARCH); } } else { qna.push_back(qna_pair(_("Search for: "), "")); this->start_qna(qna, OP_INT_START_SEARCH, &searchhistory); } } break; case OP_PIPE_TO: { std::vector qna; if (automatic) { if (args->size() > 0) { qna_responses.clear(); qna_responses.push_back((*args)[0]); finished_qna(OP_PIPE_TO); } } else { qna.push_back(qna_pair(_("Pipe article to command: "), "")); this->start_qna(qna, OP_PIPE_TO, &cmdlinehistory); } } break; case OP_EDITFLAGS: if (automatic) { qna_responses.clear(); if (args->size() > 0) { qna_responses.push_back((*args)[0]); this->finished_qna(OP_INT_EDITFLAGS_END); } } else { std::vector qna; qna.push_back(qna_pair(_("Flags: "), item->flags())); this->start_qna(qna, OP_INT_EDITFLAGS_END); } break; case OP_SHOWURLS: LOG(LOG_DEBUG, "view::run_itemview: showing URLs"); if (links.size() > 0) { v->push_urlview(links); } else { v->show_error(_("URL list empty.")); } break; case OP_DELETE: LOG(LOG_INFO, "view::run_itemview: deleting current article"); item->set_deleted(true); v->get_ctrl()->mark_deleted(guid, true); /* fall-through! */ case OP_NEXTUNREAD: LOG(LOG_INFO, "view::run_itemview: jumping to next unread article"); if (v->get_next_unread(itemlist.get(), this)) { do_redraw = true; } else { v->pop_current_formaction(); v->show_error(_("No unread items.")); } break; case OP_PREVUNREAD: LOG(LOG_INFO, "view::run_itemview: jumping to previous unread article"); if (v->get_previous_unread(itemlist.get(), this)) { do_redraw = true; } else { v->pop_current_formaction(); v->show_error(_("No unread items.")); } break; case OP_NEXT: LOG(LOG_INFO, "view::run_itemview: jumping to next article"); if (v->get_next(itemlist.get(), this)) { do_redraw = true; } else { v->pop_current_formaction(); v->show_error(_("Already on last item.")); } break; case OP_PREV: LOG(LOG_INFO, "view::run_itemview: jumping to previous article"); if (v->get_previous(itemlist.get(), this)) { do_redraw = true; } else { v->pop_current_formaction(); v->show_error(_("Already on first item.")); } break; case OP_RANDOMUNREAD: LOG(LOG_INFO, "view::run_itemview: jumping to random unread article"); if (v->get_random_unread(itemlist.get(), this)) { do_redraw = true; } else { v->pop_current_formaction(); v->show_error(_("No unread items.")); } break; case OP_TOGGLEITEMREAD: LOG(LOG_INFO, "view::run_itemview: setting unread and quitting"); v->set_status(_("Toggling read flag for article...")); try { item->set_unread(true); v->get_ctrl()->mark_article_read(item->guid(), false); } catch (const dbexception& e) { v->show_error(utils::strprintf(_("Error while marking article as unread: %s"), e.what())); } v->set_status(""); quit = true; break; case OP_QUIT: LOG(LOG_INFO, "view::run_itemview: quitting"); quit = true; break; case OP_HARDQUIT: LOG(LOG_INFO, "view::run_itemview: hard quitting"); hardquit = true; break; case OP_HELP: v->push_help(); break; case OP_1: case OP_2: case OP_3: case OP_4: case OP_5: case OP_6: case OP_7: case OP_8: case OP_9: case OP_0: { unsigned int idx = op - OP_1; LOG(LOG_DEBUG, "itemview::run: OP_1 = %d op = %d idx = %u", OP_1, op, idx); if(idx < links.size()) { v->set_status(_("Starting browser...")); v->open_in_browser(links[idx].first); v->set_status(""); } } break; case OP_GOTO_URL: { std::vector qna; if (automatic) { if (args->size() > 0) { qna_responses.clear(); qna_responses.push_back((*args)[0]); finished_qna(OP_INT_GOTO_URL); } } else { qna.push_back(qna_pair(_("Goto URL #"), "")); this->start_qna(qna, OP_INT_GOTO_URL); } } break; default: break; } if (hardquit) { while (v->formaction_stack_size() > 0) { v->pop_current_formaction(); } } else if (quit) { v->pop_current_formaction(); } } keymap_hint_entry * itemview_formaction::get_keymap_hint() { static keymap_hint_entry hints[] = { { OP_QUIT, _("Quit") }, { OP_SAVE, _("Save") }, { OP_NEXTUNREAD, _("Next Unread") }, { OP_OPENINBROWSER, _("Open in Browser") }, { OP_ENQUEUE, _("Enqueue") }, { OP_HELP, _("Help") }, { OP_NIL, NULL } }; return hints; } void itemview_formaction::set_head(const std::string& s) { fmtstr_formatter fmt; fmt.register_fmt('N', PROGRAM_NAME); fmt.register_fmt('V', PROGRAM_VERSION); fmt.register_fmt('T', s); std::string listwidth = f->get("article:w"); std::istringstream is(listwidth); unsigned int width; is >> width; f->set("head",fmt.do_format(v->get_cfg()->get_configvalue("itemview-title-format"), width)); } void itemview_formaction::render_source(std::vector& lines, std::string desc, unsigned int width) { /* * this function is called instead of htmlrenderer::render() when the * user requests to have the source displayed instead of seeing the * rendered HTML. */ std::string line; do { std::string::size_type pos = desc.find_first_of("\r\n"); line = desc.substr(0,pos); if (pos == std::string::npos) desc.erase(); else desc.erase(0,pos+1); while (line.length() > width) { int i = width; while (i > 0 && line[i] != ' ' && line[i] != '<') --i; if (0 == i) { i = width; } std::string subline = line.substr(0, i); line.erase(0, i); pos = subline.find_first_not_of(" "); subline.erase(0,pos); lines.push_back(subline); } pos = line.find_first_not_of(" "); line.erase(0,pos); lines.push_back(line); } while (desc.length() > 0); } void itemview_formaction::handle_cmdline(const std::string& cmd) { std::vector tokens = utils::tokenize_quoted(cmd); if (tokens.size() > 0) { if (tokens[0] == "save" && tokens.size() >= 2) { std::string filename = utils::resolve_tilde(tokens[1]); std::tr1::shared_ptr item = feed->get_item_by_guid(guid); if (filename == "") { v->show_error(_("Aborted saving.")); } else { try { v->get_ctrl()->write_item(item, filename); v->show_error(utils::strprintf(_("Saved article to %s"), filename.c_str())); } catch (...) { v->show_error(utils::strprintf(_("Error: couldn't save article to %s"), filename.c_str())); } } } else { formaction::handle_cmdline(cmd); } } } void itemview_formaction::finished_qna(operation op) { formaction::finished_qna(op); // important! std::tr1::shared_ptr item = feed->get_item_by_guid(guid); switch (op) { case OP_INT_EDITFLAGS_END: item->set_flags(qna_responses[0]); v->get_ctrl()->update_flags(item); v->set_status(_("Flags updated.")); do_redraw = true; break; case OP_INT_START_SEARCH: do_search(); break; case OP_PIPE_TO: { std::string cmd = qna_responses[0]; std::ostringstream ostr; v->get_ctrl()->write_item(feed->get_item_by_guid(guid), ostr); v->push_empty_formaction(); stfl::reset(); FILE * f = popen(cmd.c_str(), "w"); if (f) { std::string data = ostr.str(); fwrite(data.c_str(), data.length(), 1, f); pclose(f); } v->pop_current_formaction(); } break; case OP_INT_GOTO_URL: { unsigned int idx = 0; sscanf(qna_responses[0].c_str(),"%u",&idx); if(idx && idx-1 < links.size()) { v->set_status(_("Starting browser...")); v->open_in_browser(links[idx-1].first); v->set_status(""); } } break; default: break; } } std::vector itemview_formaction::render_html(const std::string& source, std::vector& thelinks, const std::string& feedurl, unsigned int render_width) { std::vector lines; std::string renderer = v->get_cfg()->get_configvalue("html-renderer"); if (renderer == "internal") { htmlrenderer rnd(render_width); rnd.render(source, lines, thelinks, feedurl); } else { char * argv[4]; argv[0] = const_cast("/bin/sh"); argv[1] = const_cast("-c"); argv[2] = const_cast(renderer.c_str()); argv[3] = NULL; LOG(LOG_DEBUG, "itemview_formaction::render_html: source = %s", source.c_str()); LOG(LOG_DEBUG, "itemview_formaction::render_html: html-renderer = %s", argv[2]); std::string output = utils::run_program(argv, source); std::istringstream is(output); std::string line; getline(is, line); while (!is.eof()) { lines.push_back(utils::quote_for_stfl(line)); getline(is, line); } } return lines; } void itemview_formaction::set_regexmanager(regexmanager * r) { rxman = r; std::vector& attrs = r->get_attrs("article"); unsigned int i=0; std::string attrstr; for (std::vector::iterator it=attrs.begin();it!=attrs.end();++it,++i) { attrstr.append(utils::strprintf("@style_%u_normal:%s ", i, it->c_str())); } attrstr.append("@style_b_normal[color_bold]:attr=bold @style_u_normal[color_underline]:attr=underline "); std::string textview = utils::strprintf("{textview[article] style_normal[article]: style_end[styleend]:fg=blue,attr=bold %s .expand:vh offset[articleoffset]:0 richtext:1}", attrstr.c_str()); f->modify("article", "replace", textview); } void itemview_formaction::update_percent() { if (v->get_cfg()->get_configvalue_as_bool("display-article-progress")) { std::istringstream is(f->get("articleoffset")); unsigned int offset = 0; unsigned int percent = 0; is >> offset; if (num_lines > 0) percent = (100 * (offset + 1)) / num_lines; else percent = 0; LOG(LOG_DEBUG, "itemview_formaction::update_percent: offset = %u num_lines = %u percent = %u", offset, num_lines, percent); if (offset == 0 || percent == 0) { f->set("percent", _("Top")); } else if (offset == (num_lines - 1)) { f->set("percent", _("Bottom")); } else { f->set("percent", utils::strprintf("%3u %% ", percent)); } } } std::string itemview_formaction::title() { std::tr1::shared_ptr item = feed->get_item_by_guid(guid); return utils::strprintf(_("Article - %s"), item->title().c_str()); } void itemview_formaction::set_highlightphrase(const std::string& text) { highlight_text(text); } void itemview_formaction::do_search() { std::string searchphrase = qna_responses[0]; if (searchphrase.length() == 0) return; searchhistory.add_line(searchphrase); LOG(LOG_DEBUG, "itemview_formaction::do_search: searchphrase = %s", searchphrase.c_str()); highlight_text(searchphrase); } void itemview_formaction::highlight_text(const std::string& searchphrase) { std::vector params; params.push_back("article"); params.push_back(searchphrase); std::vector colors = utils::tokenize(v->get_cfg()->get_configvalue("search-highlight-colors"), " "); std::copy(colors.begin(), colors.end(), std::back_inserter(params)); try { rxman->handle_action("highlight", params); LOG(LOG_DEBUG, "itemview_formaction::highlight_text: configuration manipulation was successful"); set_regexmanager(rxman); in_search = true; do_redraw = true; } catch (const confighandlerexception& e) { LOG(LOG_ERROR, "itemview_formaction::highlight_text: handle_action failed, error = %s", e.what()); v->show_error(_("Error: invalid regular expression!")); } } }