# from [163f99ce43db2eeb334a47f9ce797a09e69509db] # to [f6acbe3512bdc457fd516c2633a62b3dc05ab121] # # patch "ui.cc" # from [568e30b266476dd1e77441b48db2e666b8e2faf2] # to [dcac1f79858287e11b486a94d9f1bc66fa25dfe9] # # patch "ui.hh" # from [8c46e815f378359bb3756605ad3771ee155089a7] # to [405d3582ac4c576a9ad0a6f8885eb1ac12be2576] --- +++ @@ -0,0 +1,649 @@ +// Copyright (C) 2002 Graydon Hoare +// 2007 Julio M. Merino Vidal +// +// This program is made available under the GNU GPL version 2.0 or +// greater. See the accompanying file COPYING for details. +// +// This program is distributed WITHOUT ANY WARRANTY; without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. + +#include "base.hh" +#include "cmd.hh" + +#include "lua.hh" +#include "app_state.hh" +#include "options_applicator.hh" +#include "work.hh" +#include "ui.hh" +#include "mt_version.hh" +#include "charset.hh" +#include "simplestring_xform.hh" +#include "vocab_cast.hh" + +#ifndef _WIN32 +#include +#endif + +using std::string; +using std::vector; +using std::ostream; +using std::make_pair; +using std::set; + +// +// Definition of top-level commands, used to classify the real commands +// in logical groups. +// +// These top level commands, while part of the final identifiers and defined +// as regular command groups, are handled separately. The user should not +// see them except through the help command. +// +// XXX This is to easily maintain compatibilty with older versions. But +// maybe this should be revised, because exposing the top level category +// (being optional, of course), may not be a bad idea. +// + +CMD_GROUP(__root__, "__root__", "", NULL, "", ""); + +CMD_GROUP_NO_COMPLETE(automation, "automation", "", CMD_REF(__root__), + N_("Commands that aid in scripted execution"), + ""); +CMD_GROUP(database, "database", "", CMD_REF(__root__), + N_("Commands that manipulate the database"), + ""); +CMD_GROUP(debug, "debug", "", CMD_REF(__root__), + N_("Commands that aid in program debugging"), + ""); +CMD_GROUP(informative, "informative", "", CMD_REF(__root__), + N_("Commands for information retrieval"), + ""); +CMD_GROUP(key_and_cert, "key_and_cert", "", CMD_REF(__root__), + N_("Commands to manage keys and certificates"), + ""); +CMD_GROUP(network, "network", "", CMD_REF(__root__), + N_("Commands that access the network"), + ""); +CMD_GROUP(packet_io, "packet_io", "", CMD_REF(__root__), + N_("Commands for packet reading and writing"), + ""); +CMD_GROUP(vcs, "vcs", "", CMD_REF(__root__), + N_("Commands for interaction with other version control systems"), + ""); +CMD_GROUP(review, "review", "", CMD_REF(__root__), + N_("Commands to review revisions"), + ""); +CMD_GROUP(tree, "tree", "", CMD_REF(__root__), + N_("Commands to manipulate the tree"), + ""); +CMD_GROUP(variables, "variables", "", CMD_REF(__root__), + N_("Commands to manage persistent variables"), + ""); +CMD_GROUP(workspace, "workspace", "", CMD_REF(__root__), + N_("Commands that deal with the workspace"), + ""); +CMD_GROUP(user, "user", "", CMD_REF(__root__), + N_("Commands defined by the user"), + ""); + +namespace commands { + + void remove_command_name_from_args(command_id const & ident, + args_vector & args, + size_t invisible_length) + { + MM(ident); + MM(args); + MM(invisible_length); + I(ident.empty() || args.size() >= ident.size() - invisible_length); + for (args_vector::size_type i = invisible_length; i < ident.size(); i++) + { + I(ident[i]().find(args[0]()) == 0); + args.erase(args.begin()); + } + } + + void reapply_options(app_state & app, + command const * cmd, + command_id const & cmd_ident, + command const * subcmd, + command_id const & subcmd_full_ident, + size_t subcmd_invisible_length, + args_vector const & subcmd_cmdline, + vector > const * const separate_params) + { + I(cmd); + options::opts::all_options().instantiate(&app.opts).reset(); + + cmd->preset_options(app.opts); + + option::concrete_option_set optset + = (options::opts::globals() | cmd->opts()) + .instantiate(&app.opts); + + optset.from_command_line(app.reset_info.default_args, + option::concrete_option_set::xargs_forbidden); + + if (subcmd) + { + args_vector subcmd_defaults; + app.lua.hook_get_default_command_options(subcmd_full_ident, + subcmd_defaults); + (options::opts::globals() | subcmd->opts()) + .instantiate(&app.opts) + .from_command_line(subcmd_defaults, + option::concrete_option_set::xargs_forbidden); + } + + // at this point we process the data from _MTN/options if + // the command needs it. + if ((subcmd ? subcmd : cmd)->use_workspace_options()) + { + workspace::check_format(); + workspace::get_options(app.opts); + } + + optset.from_command_line(app.reset_info.cmdline_args, + option::concrete_option_set::xargs_forbidden); + + if (subcmd) + { + app.opts.args.clear(); + option::concrete_option_set subcmd_optset + = (options::opts::globals() | subcmd->opts()) + .instantiate(&app.opts); + if (!separate_params) + { + /* the first argument here is only ever modified if the second is 'true' */ + subcmd_optset.from_command_line(const_cast(subcmd_cmdline), + option::concrete_option_set::xargs_forbidden); + } + else + { + subcmd_optset.from_key_value_pairs(*separate_params); + app.opts.args = subcmd_cmdline; + } + remove_command_name_from_args(subcmd_full_ident, app.opts.args, + subcmd_invisible_length); + } + else + { + remove_command_name_from_args(cmd_ident, app.opts.args); + } + } + + // monotone.cc calls this function after option processing. + void process(app_state & app, command_id const & ident, + args_vector const & args) + { + static bool process_called(false); + I(!process_called); + process_called = true; + + command const * cmd = CMD_REF(__root__)->find_command(ident); + app.reset_info.cmd = cmd; + + string visibleid = join_words(vector< utf8 >(ident.begin() + 1, + ident.end()))(); + + I(cmd->is_leaf() || cmd->is_group()); + E(!(cmd->is_group() && cmd->parent() == CMD_REF(__root__)), + origin::user, + F("command '%s' is invalid; it is a group") % join_words(ident)); + + if (!cmd->is_leaf()) + { + // args used in the command name have not been stripped yet + remove_command_name_from_args(ident, app.opts.args); + + E(!args.empty(), origin::user, + F("no subcommand specified for '%s'") % visibleid); + + E(false, origin::user, + F("could not match '%s' to a subcommand of '%s'") % + join_words(args) % visibleid); + } + + L(FL("executing command '%s'") % visibleid); + + reapply_options(app, cmd, ident); + + // intentional leak + // we don't want the options to be reset, so don't destruct this + new options_applicator(app.opts, options_applicator::for_primary_cmd); + + cmd->exec(app, ident, args); + } + + // Prints the abstract description of the given command or command group + // properly indented. The tag starts at column two. The description has + // to start, at the very least, two spaces after the tag's end position; + // this is given by the colabstract parameter. + static void describe(const string & tag, const string & abstract, + const string & subcommands, size_t colabstract, + ostream & out) + { + I(colabstract > 0); + + size_t col = 0; + out << " " << tag << " "; + col += display_width(utf8(tag + " ", origin::internal)); + + out << string(colabstract - col, ' '); + col = colabstract; + string desc(abstract); + if (!subcommands.empty()) + { + desc += " (" + subcommands + ')'; + } + out << format_text(desc, colabstract, col) << '\n'; + } + + class cmd_ptr_compare + { + public: + bool operator()(command const * const a, command const * const b) const + { + return a->primary_name()() < b->primary_name()(); + } + }; + + static void explain_children(command::children_set const & children, + bool show_hidden_commands, + ostream & out) + { + I(!children.empty()); + + vector< command const * > sorted; + + size_t colabstract = 0; + for (command::children_set::const_iterator i = children.begin(); + i != children.end(); i++) + { + command const * child = *i; + + if (child->hidden() && !show_hidden_commands) + continue; + + size_t len = display_width(join_words(child->names(), ", ")) + + display_width(utf8(" ")); + if (colabstract < len) + colabstract = len; + + sorted.push_back(child); + } + + sort(sorted.begin(), sorted.end(), cmd_ptr_compare()); + + for (vector< command const * >::const_iterator i = sorted.begin(); + i != sorted.end(); i++) + { + command const * child = *i; + describe(join_words(child->names(), ", ")(), child->abstract(), + join_words(child->subcommands(show_hidden_commands), ", ")(), + colabstract, out); + } + } + + static command const * + find_command(command_id const & ident) + { + command const * cmd = CMD_REF(__root__)->find_command(ident); + + // This function is only used internally with an identifier returned + // by complete_command. Therefore, it must always exist. + I(cmd != NULL); + + return cmd; + } + + static void explain_cmd_usage(command_id const & ident, + bool show_hidden_commands, + ostream & out) + { + I(ident.size() >= 1); + + vector< string > lines; + command const * cmd = find_command(ident); + + string visibleid = join_words(vector< utf8 >(ident.begin() + 1, + ident.end()))(); + + // Print command parameters. + string params = cmd->params(); + split_into_lines(params, lines); + + if (visibleid.empty()) + out << format_text(F("Commands in group '%s':") % + join_words(ident)()) + << "\n\n"; + else + { + if (!cmd->children().empty()) + out << format_text(F("Subcommands of '%s %s':") % + prog_name % visibleid) + << "\n\n"; + else if (!lines.empty()) + out << format_text(F("Syntax specific to '%s %s':") % + prog_name % visibleid) + << "\n\n"; + } + + // lines might be empty, but only when specific syntax is to be + // displayed, not in the other cases. + if (!lines.empty()) + { + for (vector::const_iterator j = lines.begin(); + j != lines.end(); ++j) + out << " " << visibleid << ' ' << *j << '\n'; + out << '\n'; + } + + // Explain children, if any. + if (!cmd->is_leaf()) + { + explain_children(cmd->children(), show_hidden_commands, out); + out << '\n'; + } + + // Print command description. + if (visibleid.empty()) + out << format_text(F("Purpose of group '%s':") % + join_words(ident)()) + << "\n\n"; + else + out << format_text(F("Description for '%s %s':") % + prog_name % visibleid) + << "\n\n"; + out << format_text(cmd->desc(), 2, 0, true) << "\n\n"; + + // Print all available aliases. + if (cmd->names().size() > 1) + { + command::names_set othernames = cmd->names(); + othernames.erase(ident[ident.size() - 1]); + out << format_text(F("Aliases: %s.") % + join_words(othernames, ", ")(), 2, 0, true) + << '\n'; + } + } + + void explain_usage(command_id const & ident, + bool show_hidden_commands, + ostream & out) + { + command const * cmd = find_command(ident); + + if (ident.empty()) + { + out << format_text(F("Command groups:")) << "\n\n"; + explain_children(CMD_REF(__root__)->children(), + show_hidden_commands, + out); + out << '\n' + << format_text(F("For information on a specific command, type " + "'mtn help [subcommand_name ...]'.")) + << "\n\n" + << format_text(F("To see more details about the commands of a " + "particular group, type 'mtn help '.")) + << "\n\n" + << format_text(F("Note that you can always abbreviate a command " + "name as long as it does not conflict with other " + "names.")) + << "\n"; + } + else + explain_cmd_usage(ident, show_hidden_commands, out); + } + + options::options_type command_options(command_id const & ident) + { + command const * cmd = find_command(ident); + return cmd->opts(); + } + + // Lua-defined user commands. + class cmd_lua : public command + { + lua_State *st; + std::string const f_name; + public: + cmd_lua(std::string const & primary_name, + std::string const & params, + std::string const & abstract, + std::string const & desc, + lua_State *L_st, + std::string const & func_name) : + command(primary_name, "", CMD_REF(user), false, false, params, + abstract, desc, true, + options::options_type() | options::opts::none, true), + st(L_st), f_name(func_name) + { + // because user commands are inserted after the normal + // initialisation process + CMD_REF(user)->children().insert(this); + } + + void exec(app_state & app, command_id const & execid, + args_vector const & args) const + { + I(st); + I(app.lua.check_lua_state(st)); + + app_state* app_p = get_app_state(st); + I(app_p == & app); + + Lua ll(st); + ll.func(f_name); + + for (args_vector::const_iterator it = args.begin(); + it != args.end(); ++it) + ll.push_str((*it)()); + + app.mtn_automate_allowed = true; + + ll.call(args.size(),0); + + app.mtn_automate_allowed = false; + + E(ll.ok(), origin::user, + F("Call to user command %s (lua command: %s) failed.") + % primary_name() % f_name); + } + }; +} + +LUAEXT(alias_command, ) +{ + const char *old_cmd = luaL_checkstring(LS, -2); + const char *new_cmd = luaL_checkstring(LS, -1); + E(old_cmd && new_cmd, origin::user, + F("%s called with an invalid parameter") % "alias_command"); + + args_vector args; + args.push_back(arg_type(old_cmd, origin::user)); + commands::command_id id = commands::complete_command(args); + commands::command *old_cmd_p = CMD_REF(__root__)->find_command(id); + + old_cmd_p->add_alias(utf8(new_cmd)); + + lua_pushboolean(LS, true); + return 1; +} + + +LUAEXT(register_command, ) +{ + const char *cmd_name = luaL_checkstring(LS, -5); + const char *cmd_params = luaL_checkstring(LS, -4); + const char *cmd_abstract = luaL_checkstring(LS, -3); + const char *cmd_desc = luaL_checkstring(LS, -2); + const char *cmd_func = luaL_checkstring(LS, -1); + + E(cmd_name && cmd_params && cmd_abstract && cmd_desc && cmd_func, + origin::user, + F("%s called with an invalid parameter") % "register_command"); + + // leak this - commands can't be removed anyway + new commands::cmd_lua(cmd_name, cmd_params, cmd_abstract, cmd_desc, + LS, cmd_func); + + lua_pushboolean(LS, true); + return 1; +} + +// Miscellaneous commands and related functions for which there is no +// better file. + +CMD_NO_WORKSPACE(help, "help", "", CMD_REF(informative), + N_("command [ARGS...]"), + N_("Displays help about commands and options"), + "", + options::opts::show_hidden_commands) +{ + if (args.size() < 1) + { + app.opts.help = true; + throw usage(command_id()); + } + + command_id id = commands::complete_command(args); + app.opts.help = true; + throw usage(id); +} + +CMD_NO_WORKSPACE(version, "version", "", CMD_REF(informative), "", + N_("Shows the program version"), + "", + options::opts::full) +{ + E(args.empty(), origin::user, + F("no arguments allowed")); + + if (global_sanity.get_verbosity() > 0) + print_full_version(); + else + print_version(); +} + +CMD_HIDDEN(crash, "crash", "", CMD_REF(debug), + "{ N | E | I | double-throw | exception | signal }", + N_("Triggers the specified kind of crash"), + "", + options::opts::none) +{ + if (args.size() != 1) + throw usage(execid); + bool spoon_exists(false); + if (idx(args,0)() == "N") + E(spoon_exists, origin::user, i18n_format("There is no spoon.")); + else if (idx(args,0)() == "E") + E(spoon_exists, origin::system, i18n_format("There is no spoon.")); + else if (idx(args,0)() == "I") + { + I(spoon_exists); + } + else if (idx(args,0)() == "double-throw") + { + // This code is rather picky, for example I(false) in the destructor + // won't always work like it should; see http://bugs.debian.org/516862 + class throwing_dtor + { + public: + throwing_dtor() {} + ~throwing_dtor() + { + throw std::exception(); + } + }; + throwing_dtor td; + throw std::exception(); + } +#define maybe_throw(ex) if(idx(args,0)()==#ex) throw ex("There is no spoon.") +#define maybe_throw_bare(ex) if(idx(args,0)()==#ex) throw ex() + else maybe_throw_bare(std::bad_alloc); + else maybe_throw_bare(std::bad_cast); + else maybe_throw_bare(std::bad_typeid); + else maybe_throw_bare(std::bad_exception); + else maybe_throw_bare(std::exception); + else maybe_throw(std::domain_error); + else maybe_throw(std::invalid_argument); + else maybe_throw(std::length_error); + else maybe_throw(std::out_of_range); + else maybe_throw(std::range_error); + else maybe_throw(std::overflow_error); + else maybe_throw(std::underflow_error); + else maybe_throw(std::logic_error); + else maybe_throw(std::runtime_error); + else + { +#ifndef _WIN32 + try + { + int signo = boost::lexical_cast(idx(args,0)()); + if (0 < signo && signo <= 15) + { + raise(signo); + // control should not get here... + I(!"crash: raise returned"); + } + } + catch (boost::bad_lexical_cast&) + { // fall through and throw usage + } +#endif + throw usage(execid); + } +#undef maybe_throw +#undef maybe_throw_bare +} + +// There isn't really a better place for this function. + +void +process_commit_message_args(options const & opts, + bool & given, + utf8 & log_message, + utf8 const & message_prefix) +{ + // can't have both a --message and a --message-file ... + E(!opts.message_given || !opts.msgfile_given, origin::user, + F("--message and --message-file are mutually exclusive")); + + if (opts.message_given) + { + string msg; + join_lines(opts.message, msg); + log_message = utf8(msg, origin::user); + if (!opts.no_prefix && message_prefix().length() != 0) + log_message = utf8(message_prefix() + "\n\n" + log_message(), + origin::user); + given = true; + } + else if (opts.msgfile_given) + { + data dat; + read_data_for_command_line(opts.msgfile, dat); + external dat2 = typecast_vocab(dat); + system_to_utf8(dat2, log_message); + if (!opts.no_prefix && message_prefix().length() != 0) + log_message = utf8(message_prefix() + "\n\n" + log_message(), + origin::user); + given = true; + } + else if (message_prefix().length() != 0) + { + log_message = message_prefix; + given = true; + } + else + given = false; +} + +// Local Variables: +// mode: C++ +// fill-column: 76 +// c-file-style: "gnu" +// indent-tabs-mode: nil +// End: +// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s: --- ui.cc +++ ui.cc @@ -0,0 +1,1133 @@ +// Copyright (C) 2002 Graydon Hoare +// +// This program is made available under the GNU GPL version 2.0 or +// greater. See the accompanying file COPYING for details. +// +// This program is distributed WITHOUT ANY WARRANTY; without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. + +// this file contains a couple utilities to deal with the user +// interface. the global user_interface object 'ui' owns clog, so no +// writing to it directly! + + +#include "base.hh" +#include "platform.hh" +#include "paths.hh" +#include "sanity.hh" +#include "ui.hh" +#include "charset.hh" +#include "simplestring_xform.hh" +#include "constants.hh" +#include "commands.hh" + +#include +#include +#include +#include +#include +#include +#include "lexical_cast.hh" +#include "safe_map.hh" + +#include + +#include "current_exception.hh" + +using std::clog; +using std::cout; +using std::endl; +using std::ios_base; +using std::locale; +using std::make_pair; +using std::map; +using std::max; +using std::ofstream; +using std::string; +using std::vector; + +using boost::lexical_cast; + +struct user_interface ui; + +struct user_interface::impl +{ + std::set issued_warnings; + + bool some_tick_is_dirty; // At least one tick needs being printed + bool last_write_was_a_tick; + map tickers; + tick_writer * t_writer; + string tick_trailer; + + impl() : some_tick_is_dirty(false), last_write_was_a_tick(false), + t_writer(0) {} +}; + +ticker::ticker(string const & tickname, string const & s, size_t mod, + bool kilocount, bool skip_display) : + ticks(0), + mod(mod), + total(0), + previous_total(0), + kilocount(kilocount), + use_total(false), + may_skip_display(skip_display), + keyname(tickname), + name(_(tickname.c_str())), + shortname(s), + count_size(0) +{ + I(ui.imp); + safe_insert(ui.imp->tickers, make_pair(keyname, this)); +} + +ticker::~ticker() +{ + I(ui.imp); + safe_erase(ui.imp->tickers, keyname); + + if (ui.imp->some_tick_is_dirty) + ui.write_ticks(); + ui.finish_ticking(); +} + +void +ticker::operator++() +{ + I(ui.imp); + I(ui.imp->tickers.find(keyname) != ui.imp->tickers.end()); + ticks++; + ui.imp->some_tick_is_dirty = true; + if (ticks % mod == 0) + ui.write_ticks(); +} + +void +ticker::operator+=(size_t t) +{ + I(ui.imp); + I(ui.imp->tickers.find(keyname) != ui.imp->tickers.end()); + size_t old = ticks; + + ticks += t; + if (t != 0) + { + ui.imp->some_tick_is_dirty = true; + if (ticks % mod == 0 || (ticks / mod) > (old / mod)) + ui.write_ticks(); + } +} + +// We would like to put these in an anonymous namespace but we can't because +// struct user_interface needs to make them friends. +struct tick_writer +{ +public: + tick_writer() {} + virtual ~tick_writer() {} + virtual void write_ticks() = 0; + virtual void clear_line() = 0; +}; + +struct tick_write_count : virtual public tick_writer +{ +public: + tick_write_count(); + ~tick_write_count(); + void write_ticks(); + void clear_line(); +private: + std::vector last_tick_widths; + size_t last_tick_len; +}; + +struct tick_write_dot : virtual public tick_writer +{ +public: + tick_write_dot(); + ~tick_write_dot(); + void write_ticks(); + void clear_line(); +private: + std::map last_ticks; + unsigned int chars_on_line; +}; + +struct tick_write_stdio : virtual public tick_writer +{ +public: + tick_write_stdio(); + ~tick_write_stdio(); + void write_ticks(); + void clear_line(); +private: + std::map last_ticks; +}; + +struct tick_write_nothing : virtual public tick_writer +{ +public: + void write_ticks() {} + void clear_line() {} +}; + +tick_write_count::tick_write_count() : last_tick_len(0) +{ +} + +tick_write_count::~tick_write_count() +{ +} + +static string compose_count(ticker *tick, size_t ticks=0) +{ + string count; + + if (ticks == 0) + { + ticks = tick->ticks; + } + + if (tick->kilocount && ticks) + { + // automatic unit conversion is enabled + float div = 1.0; + const char *message; + + if (ticks >= 1073741824) + { + div = 1073741824; + // xgettext: gibibytes (2^30 bytes) + message = N_("%.1f G"); + } + else if (ticks >= 1048576) + { + div = 1048576; + // xgettext: mebibytes (2^20 bytes) + message = N_("%.1f M"); + } + else if (ticks >= 1024) + { + div = 1024; + // xgettext: kibibytes (2^10 bytes) + message = N_("%.1f k"); + } + else + { + div = 1; + message = "%.0f"; + } + // We reset the mod to the divider, to avoid spurious screen updates. + tick->mod = max(static_cast(div / 10.0), 1); + count = (F(message) % (ticks / div)).str(); + } + else if (tick->use_total) + { + count = (F("%d/%d") % ticks % tick->total).str(); + } + else + { + // xgettext: bytes + count = (F("%d") % ticks).str(); + } + + return count; +} + +void tick_write_count::write_ticks() +{ + vector tick_widths; + vector tick_title_strings; + vector tick_count_strings; + + I(ui.imp); + for (map::const_iterator i = ui.imp->tickers.begin(); + i != ui.imp->tickers.end(); ++i) + { + ticker * tick = i->second; + + // if the display of this ticker has no great importance, i.e. multiple + // other tickers should be displayed at the same time, skip its display + // to save space on terminals + if (tick->may_skip_display) + continue; + + if ((tick->count_size == 0 && tick->kilocount) + || + (tick->use_total && tick->previous_total != tick->total)) + { + if (!tick->kilocount && tick->use_total) + { + // We know that we're going to eventually have 'total' + // displayed twice on screen, plus a slash. So we should + // pad out this field to that eventual size to avoid + // spurious re-issuing of the tick titles as we expand to + // the goal. + tick->set_count_size(display_width(utf8(compose_count(tick, + tick->total), + origin::internal))); + tick->previous_total = tick->total; + } + else + { + // To find out what the maximum size can be, choose one the + // the dividers from compose_count, subtract one and have + // compose_count create the count string for that. Use the + // size of the returned count string as an initial size for + // this tick. + tick->set_count_size(display_width(utf8(compose_count(tick, + 1048575), + origin::internal))); + } + } + + string count(compose_count(tick)); + + size_t title_width = display_width(utf8(tick->name, origin::internal)); + size_t count_width = display_width(utf8(count, origin::internal)); + + if (count_width > tick->count_size) + { + tick->set_count_size(count_width); + } + + size_t max_width = max(title_width, tick->count_size); + + string name; + name.append(max_width - title_width, ' '); + name.append(tick->name); + + string count2; + count2.append(max_width - count_width, ' '); + count2.append(count); + + tick_title_strings.push_back(name); + tick_count_strings.push_back(count2); + tick_widths.push_back(max_width); + } + + string tickline1; + bool write_tickline1 = !(ui.imp->last_write_was_a_tick + && (tick_widths == last_tick_widths)); + if (write_tickline1) + { + // Reissue the titles if the widths have changed. + tickline1 = ui.output_prefix(); + for (size_t i = 0; i < tick_widths.size(); ++i) + { + if (i != 0) + tickline1.append(" | "); + tickline1.append(idx(tick_title_strings, i)); + } + last_tick_widths = tick_widths; + write_tickline1 = true; + } + + // Always reissue the counts. + string tickline2 = ui.output_prefix(); + for (size_t i = 0; i < tick_widths.size(); ++i) + { + if (i != 0) + tickline2.append(" | "); + tickline2.append(idx(tick_count_strings, i)); + } + + if (!ui.imp->tick_trailer.empty()) + { + tickline2 += " "; + tickline2 += ui.imp->tick_trailer; + } + + size_t curr_sz = display_width(utf8(tickline2, origin::internal)); + if (curr_sz < last_tick_len) + tickline2.append(last_tick_len - curr_sz, ' '); + last_tick_len = curr_sz; + + unsigned int tw = terminal_width(); + if(write_tickline1) + { + if (ui.imp->last_write_was_a_tick) + clog << '\n'; + + if (tw && display_width(utf8(tickline1, origin::internal)) > tw) + { + // FIXME: may chop off more than necessary (because we chop by + // bytes, not by characters) + tickline1.resize(tw); + } + clog << tickline1 << '\n'; + } + if (tw && display_width(utf8(tickline2, origin::internal)) > tw) + { + // FIXME: may chop off more than necessary (because we chop by + // bytes, not by characters) + tickline2.resize(tw); + } + clog << '\r' << tickline2; + clog.flush(); +} + +void tick_write_count::clear_line() +{ + clog << endl; +} + + +tick_write_dot::tick_write_dot() +{ +} + +tick_write_dot::~tick_write_dot() +{ +} + +void tick_write_dot::write_ticks() +{ + I(ui.imp); + static const string tickline_prefix = ui.output_prefix(); + string tickline1, tickline2; + bool first_tick = true; + + if (ui.imp->last_write_was_a_tick) + { + tickline1 = ""; + tickline2 = ""; + } + else + { + tickline1 = ui.output_prefix() + "ticks: "; + tickline2 = "\n" + tickline_prefix; + chars_on_line = tickline_prefix.size(); + } + + for (map::const_iterator i = ui.imp->tickers.begin(); + i != ui.imp->tickers.end(); ++i) + { + map::const_iterator old = last_ticks.find(i->first); + + if (!ui.imp->last_write_was_a_tick) + { + if (!first_tick) + tickline1 += ", "; + + tickline1 += + i->second->shortname + "=\"" + i->second->name + "\"" + + "/" + lexical_cast(i->second->mod); + first_tick = false; + } + + if (old == last_ticks.end() + || ((i->second->ticks / i->second->mod) + > (old->second / i->second->mod))) + { + chars_on_line += i->second->shortname.size(); + if (chars_on_line > guess_terminal_width()) + { + chars_on_line = tickline_prefix.size() + i->second->shortname.size(); + tickline2 += "\n" + tickline_prefix; + } + tickline2 += i->second->shortname; + + if (old == last_ticks.end()) + last_ticks.insert(make_pair(i->first, i->second->ticks)); + else + last_ticks[i->first] = i->second->ticks; + } + } + + clog << tickline1 << tickline2; + clog.flush(); +} + +void tick_write_dot::clear_line() +{ + clog << endl; +} + + +tick_write_stdio::tick_write_stdio() +{} + +tick_write_stdio::~tick_write_stdio() +{} + +void tick_write_stdio::write_ticks() +{ + I(ui.imp); + string headers, sizes, tickline; + + for (map::const_iterator i = ui.imp->tickers.begin(); + i != ui.imp->tickers.end(); ++i) + { + std::map::iterator it = + last_ticks.find(i->second->shortname); + + // we output each explanation stanza just once and every time the + // total count has been changed + if (it == last_ticks.end()) + { + headers += i->second->shortname + ":" + i->second->name + ";"; + sizes += i->second->shortname + "=" + lexical_cast(i->second->total) + ";"; + last_ticks[i->second->shortname] = i->second->total; + } + else + if (it->second != i->second->total) + { + sizes += i->second->shortname + "=" + lexical_cast(i->second->total) + ";"; + last_ticks[i->second->shortname] = i->second->total; + } + + tickline += i->second->shortname + "#" + lexical_cast(i->second->ticks) + ";"; + } + + if (!headers.empty()) + { + global_sanity.maybe_write_to_out_of_band_handler('t', headers); + } + if (!sizes.empty()) + { + global_sanity.maybe_write_to_out_of_band_handler('t', sizes); + } + + I(!tickline.empty()); + global_sanity.maybe_write_to_out_of_band_handler('t', tickline); +} + +void tick_write_stdio::clear_line() +{ + std::map::iterator it; + std::string out; + + for (it = last_ticks.begin(); it != last_ticks.end(); it++) + { + out += it->first + ";"; + } + + global_sanity.maybe_write_to_out_of_band_handler('t', out); + last_ticks.clear(); +} + +// user_interface has both constructor/destructor and initialize/ +// deinitialize because there's only one of these objects, it's +// global, and we don't want global constructors/destructors doing +// any real work. see monotone.cc for how this is handled. + +void user_interface::initialize() +{ + imp = new user_interface::impl; + + cout.exceptions(ios_base::badbit); +#ifdef SYNC_WITH_STDIO_WORKS + clog.sync_with_stdio(false); +#endif + clog.unsetf(ios_base::unitbuf); + if (have_smart_terminal()) + set_tick_write_count(); + else + set_tick_write_dot(); + + timestamps_enabled = false; +} + +void user_interface::deinitialize() +{ + I(imp); + delete imp->t_writer; + delete imp; +} + +void +user_interface::finish_ticking() +{ + I(imp); + if (imp->tickers.empty() && imp->last_write_was_a_tick) + { + imp->tick_trailer = ""; + imp->t_writer->clear_line(); + imp->last_write_was_a_tick = false; + } +} + +void +user_interface::set_tick_trailer(string const & t) +{ + I(imp); + imp->tick_trailer = t; +} + +void +user_interface::set_tick_write_dot() +{ + I(imp); + if (tick_type == dot) + return; + if (imp->t_writer != 0) + delete imp->t_writer; + imp->t_writer = new tick_write_dot; + tick_type = dot; +} + +void +user_interface::set_tick_write_count() +{ + I(imp); + if (tick_type == count) + return; + if (imp->t_writer != 0) + delete imp->t_writer; + imp->t_writer = new tick_write_count; + tick_type = count; +} + +void +user_interface::set_tick_write_stdio() +{ + I(imp); + if (tick_type == stdio) + return; + if (imp->t_writer != 0) + delete imp->t_writer; + imp->t_writer = new tick_write_stdio; + tick_type = stdio; +} + +void +user_interface::set_tick_write_nothing() +{ + I(imp); + if (tick_type == none) + return; + if (imp->t_writer != 0) + delete imp->t_writer; + imp->t_writer = new tick_write_nothing; + tick_type = none; +} + +user_interface::ticker_type +user_interface::set_ticker_type(user_interface::ticker_type type) +{ + ticker_type ret = tick_type; + switch (type) + { + case count: set_tick_write_count(); break; + case dot: set_tick_write_dot(); break; + case stdio: set_tick_write_stdio(); break; + case none: set_tick_write_nothing(); break; + } + return ret; +} + +user_interface::ticker_type +user_interface::get_ticker_type() const +{ + return tick_type; +} + + +void +user_interface::write_ticks() +{ + I(imp); + imp->t_writer->write_ticks(); + imp->last_write_was_a_tick = true; + imp->some_tick_is_dirty = false; +} + +void +user_interface::warn(string const & warning) +{ + I(imp); + if (imp->issued_warnings.find(warning) == imp->issued_warnings.end()) + { + string message; + prefix_lines_with(_("warning: "), warning, message); + inform(message); + } + imp->issued_warnings.insert(warning); +} + +// this message should be kept consistent with unix/main.cc and +// win32/main.cc ::bug_report_message (it is not exactly the same) +void +user_interface::fatal(string const & fatal) +{ + inform(F("fatal: %s\n" + "this is almost certainly a bug in monotone.\n" + "please send this error message, the output of '%s version --verbose',\n" + "and a description of what you were doing to %s.") + % fatal % prog_name % PACKAGE_BUGREPORT); + global_sanity.dump_buffer(); +} +// just as above, but the error appears to have come from the database. +// Of course, since the monotone is the only thing that should be +// writing to the database, this still probably means there's a bug. +void +user_interface::fatal_db(string const & fatal) +{ + inform(F("fatal: %s\n" + "this is almost certainly a bug in monotone.\n" + "please send this error message, the output of '%s version --verbose',\n" + "and a description of what you were doing to %s.\n" + "This error appears to have been triggered by something in the\n" + "database you were using, so please preserve it in case it can\n" + "help in finding the bug.") + % fatal % prog_name % PACKAGE_BUGREPORT); + global_sanity.dump_buffer(); +} + +// Report what we can about a fatal exception (caught in the outermost catch +// handlers) which is from the std::exception hierarchy. In this case we +// can access the exception object, and we can try to figure out what it +// really is by typeinfo operations. +int +user_interface::fatal_exception(std::exception const & ex) +{ + char const * what = ex.what(); + unrecoverable_failure const * inf; + + if (dynamic_cast(&ex) + || dynamic_cast(&ex)) + { + this->inform(what); + return 1; + } + else if ((inf = dynamic_cast(&ex))) + { + if (inf->caused_by() == origin::database) + this->fatal_db(what); + else + this->fatal(what); + return 3; + } + else if (dynamic_cast(&ex)) + { + // an error has already been printed + return 1; + } + else if (dynamic_cast(&ex)) + { + this->inform(_("error: memory exhausted")); + return 1; + } + else // we can at least produce the class name and the what()... + { + using std::strcmp; + using std::strncmp; + char const * name = typeid(ex).name(); + char const * dem = demangle_typename(name); + + if (dem == 0) + dem = name; + + // some demanglers stick "class" at the beginning of their output, + // which looks dumb in this context + if (!strncmp(dem, "class ", 6)) + dem += 6; + + // only print what() if it's interesting, i.e. nonempty and different + // from the name (mangled or otherwise) of the exception type. + if (what == 0 || what[0] == 0 + || !strcmp(what, name) + || !strcmp(what, dem)) + this->fatal(dem); + else + this->fatal(i18n_format("%s: %s") % dem % what); + return 3; + } +} + +// Report what we can about a fatal exception (caught in the outermost catch +// handlers) which is of unknown type. If we have the interfaces, +// we can at least get the type_info object. +int +user_interface::fatal_exception() +{ + std::type_info *type = get_current_exception_type(); + if (type) + { + char const * name = type->name(); + char const * dem = demangle_typename(name); + if (dem == 0) + dem = name; + this->fatal(dem); + } + else + this->fatal(_("C++ exception of unknown type")); + return 3; +} + +string +user_interface::output_prefix() +{ + std::string prefix; + + if (timestamps_enabled) { + try { + // FIXME: with no app pointer around we have no access to + // app.lua.get_date_format_spec() here, so we use the same format + // which f.e. also Apache uses for its log output + prefix = "[" + + date_t::now().as_formatted_localtime("%a %b %d %H:%M:%S %Y") + + "] "; + } + // ensure that we do not throw an exception because we could not + // create the timestamp prefix above + catch (...) {} + } + + if (prog_name.empty()) { + prefix += "?: "; + } + else + { + prefix += prog_name + ": "; + } + + return prefix; +} + +static inline string +sanitize(string const & line) +{ + // FIXME: you might want to adjust this if you're using a charset + // which has safe values in the sub-0x20 range. ASCII, UTF-8, + // and most ISO8859-x sets do not. + string tmp; + tmp.reserve(line.size()); + for (size_t i = 0; i < line.size(); ++i) + { + if ((line[i] == '\n') + || (static_cast(line[i]) >= static_cast(0x20) + && line[i] != static_cast(0x7F))) + tmp += line[i]; + else + tmp += ' '; + } + return tmp; +} + +void +user_interface::ensure_clean_line() +{ + I(imp); + if (imp->last_write_was_a_tick) + { + write_ticks(); + imp->t_writer->clear_line(); + } + imp->last_write_was_a_tick = false; +} + +void +user_interface::redirect_log_to(system_path const & filename) +{ + static ofstream filestr; + if (filestr.is_open()) + filestr.close(); + filestr.open(filename.as_external().c_str(), ofstream::out | ofstream::app); + E(filestr.is_open(), origin::system, + F("failed to open log file '%s'") % filename); + clog.rdbuf(filestr.rdbuf()); +} + +void +user_interface::inform(string const & line) +{ + string prefixedLine; + prefix_lines_with(output_prefix(), line, prefixedLine); + ensure_clean_line(); + clog << sanitize(prefixedLine) << endl; // flushes +} + +unsigned int +guess_terminal_width() +{ + unsigned int w = terminal_width(); + if (!w) + w = constants::default_terminal_width; + return w; +} + +// A very simple class that adds an operator() to a string that returns +// the string itself. This is to make it compatible with, for example, +// the utf8 class, allowing it to be usable in other contexts without +// encoding conversions. +class string_adaptor : public string +{ +public: + string_adaptor(string const & str) : string(str) {} + string_adaptor(string const & str, origin::type) : string(str) {} + string const & operator()(void) const { return *this; } + origin::type made_from; +}; + +// See description for format_text below for more details. +static vector +wrap_paragraph(string const & text, size_t const line_length, + size_t const first_line_length) +{ + I(text.find('\n') == string::npos); + + vector wrapped; + size_t line_len = 0; + string this_line; + + vector< string_adaptor > words = split_into_words(string_adaptor(text)); + for (vector< string_adaptor >::const_iterator iter = words.begin(); + iter != words.end(); iter++) + { + string const & word = (*iter)(); + size_t word_len = display_width(utf8(word, origin::no_fault)); + size_t wanted_len = (wrapped.empty() ? first_line_length : line_length); + if (iter != words.begin() && line_len + word_len >= wanted_len) + { + wrapped.push_back(this_line); + line_len = 0; + this_line.clear(); + } + if (!this_line.empty()) + { + this_line += " "; + ++line_len; + } + line_len += word_len; + this_line += word; + } + if (!this_line.empty()) + wrapped.push_back(this_line); + + return wrapped; +} + +static string +format_paragraph(string const & text, size_t const col, + size_t curcol, bool indent_first_line) +{ + string ret; + size_t const maxcol = guess_terminal_width(); + vector wrapped = wrap_paragraph(text, maxcol - col, maxcol - curcol); + for (vector::iterator w = wrapped.begin(); w != wrapped.end(); ++w) + { + if (w != wrapped.begin()) + ret += "\n"; + if (w != wrapped.begin() || indent_first_line) + ret += string(col, ' '); + ret += *w; + } + return ret; +} + +// Reformats the given text so that it fits in the current screen with no +// wrapping. +// +// The input text is a series of words and sentences. Paragraphs may be +// separated with a '\n' character, which is taken into account to do the +// proper formatting. The text should not finish in '\n'. +// +// 'col' specifies the column where the text will start and 'curcol' +// specifies the current position of the cursor. +string +format_text(string const & text, size_t const col, + size_t curcol, bool indent_first_line) +{ + I(curcol <= col); + + string formatted; + + vector< string > lines; + split_into_lines(text, lines); + for (vector< string >::const_iterator iter = lines.begin(); + iter != lines.end(); iter++) + { + string const & line = *iter; + + formatted += format_paragraph(line, col, curcol, indent_first_line); + if (iter + 1 != lines.end()) + formatted += "\n\n"; + curcol = 0; + } + + return formatted; +} + +// See description for the other format_text above for more details. +string +format_text(i18n_format const & text, size_t const col, + size_t curcol, bool indent_first_line) +{ + return format_text(text.str(), col, curcol, indent_first_line); +} + +namespace { + class option_text + { + string names; + string desc; + vector formatted_names; + vector formatted_desc; + public: + option_text(string const & n, string const & d) + : names(n), desc(d) { } + size_t format_names(size_t const width) + { + size_t const full_len = display_width(utf8(names, origin::no_fault)); + size_t const slash = names.find('/'); + if (slash == string::npos || full_len <= width) + { + formatted_names.push_back(names); + return full_len; + } + + formatted_names.push_back(names.substr(0, slash-1)); + formatted_names.push_back(" " + names.substr(slash-1)); + + size_t ret = 0; + for (vector::const_iterator i = formatted_names.begin(); + i != formatted_names.end(); ++i) + { + ret = max(ret, display_width(utf8(*i, origin::no_fault))); + } + return ret; + } + void format_desc(size_t const width) + { + formatted_desc = wrap_paragraph(desc, width, width); + } + string formatted(size_t pre_indent, size_t space, size_t namelen) const + { + string ret; + string empty; + size_t const lines = max(formatted_names.size(), formatted_desc.size()); + for (size_t i = 0; i < lines; ++i) + { + string const * left = ∅ + if (i < formatted_names.size()) + left = &formatted_names.at(i); + string const * right = ∅ + if (i < formatted_desc.size()) + right = &formatted_desc.at(i); + + ret += string(pre_indent, ' ') + + *left + string(namelen - left->size(), ' ') + + string(space, ' ') + + *right + + "\n"; + } + return ret; + } + }; +} + +// Format a block of options and their descriptions. +static string +format_usage_strings(vector const & names, + vector const & descriptions) +{ + // " --long [ -s ] description goes here" + // ^ ^^ ^^ ^^ ^ + // | | \ namelen / | | \ descwidth /| <- edge of screen + // ^^^^ ^^^^ + // pre_indent space + string result; + + size_t const pre_indent = 2; // empty space on the left + size_t const space = 2; // space after the longest option, before the description + size_t const termwidth = guess_terminal_width(); + size_t const desired_namewidth = (termwidth - pre_indent - space) / 2; + size_t namelen = 0; + + vector texts; + I(names.size() == descriptions.size()); + for (vector::const_iterator n = names.begin(), d = descriptions.begin(); + n != names.end(); ++n, ++d) + { + if (n->empty()) + continue; + texts.push_back(option_text(*n, *d)); + + size_t my_name_len = texts.back().format_names(desired_namewidth); + if (my_name_len > namelen) + namelen = my_name_len; + } + + size_t const descindent = pre_indent + namelen + space; + size_t const descwidth = termwidth - descindent; + + for (vector::iterator i = texts.begin(); + i != texts.end(); ++i) + { + i->format_desc(descwidth); + + result += i->formatted(pre_indent, space, namelen); + } + + result += '\n'; + return result; +} + +static string +get_usage_str(options::options_type const & optset, options & opts) +{ + vector names; + vector descriptions; + unsigned int maxnamelen; + + optset.instantiate(&opts).get_usage_strings(names, descriptions, maxnamelen, + opts.show_hidden_commands); + return format_usage_strings(names, descriptions); +} + +void +user_interface::inform_usage(usage const & u, options & opts) +{ + // we send --help output to stdout, so that "mtn --help | less" works + // but we send error-triggered usage information to stderr, so that if + // you screw up in a script, you don't just get usage information sent + // merrily down your pipes. + std::ostream & usage_stream = (opts.help ? cout : clog); + + string visibleid; + if (!u.which.empty()) + visibleid = join_words(vector< utf8 >(u.which.begin() + 1, + u.which.end()))(); + + usage_stream << F("Usage: %s [OPTION...] command [ARG...]") % + prog_name << "\n\n"; + + if (u.which.empty()) + usage_stream << get_usage_str(options::opts::globals(), opts); + + // Make sure to hide documentation that's not part of + // the current command. + options::options_type cmd_options = + commands::command_options(u.which); + if (!cmd_options.empty()) + { + usage_stream + << F("Options specific to '%s %s' " + "(run '%s help' to see global options):") + % prog_name % visibleid % prog_name + << "\n\n"; + usage_stream << get_usage_str(cmd_options, opts); + } + + commands::explain_usage(u.which, opts.show_hidden_commands, usage_stream); +} + +bool +user_interface::enable_timestamps(bool enable) +{ + bool ret = timestamps_enabled; + timestamps_enabled = enable; + return ret; +} + +// Local Variables: +// mode: C++ +// fill-column: 76 +// c-file-style: "gnu" +// indent-tabs-mode: nil +// End: +// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s: --- ui.hh +++ ui.hh @@ -0,0 +1,128 @@ +// Copyright (C) 2002 Graydon Hoare +// +// This program is made available under the GNU GPL version 2.0 or +// greater. See the accompanying file COPYING for details. +// +// This program is distributed WITHOUT ANY WARRANTY; without even the +// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. + +#ifndef __UI_HH__ +#define __UI_HH__ + +// this file contains a couple utilities to deal with the user +// interface. the global user_interface object 'ui' owns cerr, so +// no writing to it directly! + +#include "vector.hh" + +struct i18n_format; +class system_path; +struct usage; +struct options; + +struct ticker +{ + size_t ticks; + size_t mod; + size_t total; + size_t previous_total; + bool kilocount; + bool use_total; + bool may_skip_display; + std::string keyname; + std::string name; // translated name + std::string shortname; + size_t count_size; + ticker(std::string const & n, std::string const & s, size_t mod = 64, + bool kilocount=false, bool skip_display=false); + void set_total(size_t tot) { use_total = true; total = tot; } + void set_count_size(size_t csiz) { count_size = csiz; } + void operator++(); + void operator+=(size_t t); + ~ticker(); +}; + +struct tick_writer; +struct tick_write_count; +struct tick_write_dot; +struct tick_write_stdio; + +struct user_interface +{ +public: + void initialize(); + void deinitialize(); + void warn(std::string const & warning); + void warn(format_base const & fmt) { warn(fmt.str()); } + void fatal(std::string const & fatal); + void fatal(format_base const & fmt) { fatal(fmt.str()); } + void fatal_db(std::string const & fatal); + void fatal_db(format_base const & fmt) { fatal_db(fmt.str()); } + void inform(std::string const & line); + void inform(format_base const & fmt) { inform(fmt.str()); } + void inform_usage(usage const & u, options & opts); + int fatal_exception(std::exception const & ex); + int fatal_exception(); + void set_tick_trailer(std::string const & trailer); + + enum ticker_type { count=1, dot, stdio, none }; + void set_tick_write_dot(); + void set_tick_write_count(); + void set_tick_write_stdio(); + void set_tick_write_nothing(); + ticker_type set_ticker_type(ticker_type type); + ticker_type get_ticker_type() const; + + void ensure_clean_line(); + void redirect_log_to(system_path const & filename); + bool enable_timestamps(bool enable); + + std::string output_prefix(); + +private: + void finish_ticking(); + void write_ticks(); + + struct impl; + impl * imp; + bool timestamps_enabled; + ticker_type tick_type; + + friend struct ticker; + friend struct tick_write_count; + friend struct tick_write_dot; + friend struct tick_write_stdio; +}; + +extern struct user_interface ui; + +// Wrapper class which ensures proper setup and teardown of the global ui +// object. (We do not want to use global con/destructors for this, as they +// execute outside the protection of main.cc's signal handlers.) +struct ui_library +{ + ui_library() { ui.initialize(); } + ~ui_library() { ui.deinitialize(); } +}; + +// like platform.hh's "terminal_width", but always returns a sensible value +// (even if there is no terminal) +unsigned int guess_terminal_width(); + +std::string format_text(std::string const & text, + size_t const col = 0, size_t curcol = 0, + bool indent_first_line = false); +std::string format_text(i18n_format const & text, + size_t const col = 0, size_t curcol = 0, + bool indent_first_line = false); + +#endif // __UI_HH__ + +// Local Variables: +// mode: C++ +// fill-column: 76 +// c-file-style: "gnu" +// indent-tabs-mode: nil +// End: +// vim: et:sw=2:sts=2:ts=2:cino=>2s,{s,\:s,+s,t0,g0,^-2,e-2,n-2,p2s,(0,=s: