1"""The xonsh configuration (xonfig) utility."""
2import os
3import re
4import ast
5import json
6import shutil
7import random
8import pprint
9import textwrap
10import builtins
11import argparse
12import functools
13import itertools
14import contextlib
15import collections
16
17try:
18    import ply
19except ImportError:
20    from xonsh.ply import ply
21
22import xonsh.wizard as wiz
23from xonsh import __version__ as XONSH_VERSION
24from xonsh.prompt.base import is_template_string
25from xonsh.platform import (
26    is_readline_available,
27    ptk_version,
28    PYTHON_VERSION_INFO,
29    pygments_version,
30    ON_POSIX,
31    ON_LINUX,
32    linux_distro,
33    ON_DARWIN,
34    ON_WINDOWS,
35    ON_CYGWIN,
36    DEFAULT_ENCODING,
37    ON_MSYS,
38    githash,
39)
40from xonsh.tools import (
41    to_bool,
42    is_string,
43    print_exception,
44    is_superuser,
45    color_style_names,
46    print_color,
47    color_style,
48)
49from xonsh.foreign_shells import CANON_SHELL_NAMES
50from xonsh.xontribs import xontrib_metadata, find_xontrib
51from xonsh.lazyasd import lazyobject
52
53HR = "'`-.,_,.-*'`-.,_,.-*'`-.,_,.-*'`-.,_,.-*'`-.,_,.-*'`-.,_,.-*'`-.,_,.-*'"
54WIZARD_HEAD = """
55          {{BOLD_WHITE}}Welcome to the xonsh configuration wizard!{{NO_COLOR}}
56          {{YELLOW}}------------------------------------------{{NO_COLOR}}
57This will present a guided tour through setting up the xonsh static
58config file. Xonsh will automatically ask you if you want to run this
59wizard if the configuration file does not exist. However, you can
60always rerun this wizard with the xonfig command:
61
62    $ xonfig wizard
63
64This wizard will load an existing configuration, if it is available.
65Also never fear when this wizard saves its results! It will create
66a backup of any existing configuration automatically.
67
68This wizard has two main phases: foreign shell setup and environment
69variable setup. Each phase may be skipped in its entirety.
70
71For the configuration to take effect, you will need to restart xonsh.
72
73{hr}
74""".format(
75    hr=HR
76)
77
78WIZARD_FS = """
79{hr}
80
81                      {{BOLD_WHITE}}Foreign Shell Setup{{NO_COLOR}}
82                      {{YELLOW}}-------------------{{NO_COLOR}}
83The xonsh shell has the ability to interface with foreign shells such
84as Bash, zsh, or fish.
85
86For configuration, this means that xonsh can load the environment,
87aliases, and functions specified in the config files of these shells.
88Naturally, these shells must be available on the system to work.
89Being able to share configuration (and source) from foreign shells
90makes it easier to transition to and from xonsh.
91""".format(
92    hr=HR
93)
94
95WIZARD_ENV = """
96{hr}
97
98                  {{BOLD_WHITE}}Environment Variable Setup{{NO_COLOR}}
99                  {{YELLOW}}--------------------------{{NO_COLOR}}
100The xonsh shell also allows you to setup environment variables from
101the static configuration file. Any variables set in this way are
102superseded by the definitions in the xonshrc or on the command line.
103Still, setting environment variables in this way can help define
104options that are global to the system or user.
105
106The following lists the environment variable name, its documentation,
107the default value, and the current value. The default and current
108values are presented as pretty repr strings of their Python types.
109
110{{BOLD_GREEN}}Note:{{NO_COLOR}} Simply hitting enter for any environment variable
111will accept the default value for that entry.
112""".format(
113    hr=HR
114)
115
116WIZARD_ENV_QUESTION = "Would you like to set env vars now, " + wiz.YN
117
118WIZARD_XONTRIB = """
119{hr}
120
121                           {{BOLD_WHITE}}Xontribs{{NO_COLOR}}
122                           {{YELLOW}}--------{{NO_COLOR}}
123No shell is complete without extensions, and xonsh is no exception. Xonsh
124extensions are called {{BOLD_GREEN}}xontribs{{NO_COLOR}}, or xonsh contributions.
125Xontribs are dynamically loadable, either by importing them directly or by
126using the 'xontrib' command. However, you can also configure xonsh to load
127xontribs automatically on startup prior to loading the run control files.
128This allows the xontrib to be used immediately in your xonshrc files.
129
130The following describes all xontribs that have been registered with xonsh.
131These come from users, 3rd party developers, or xonsh itself!
132""".format(
133    hr=HR
134)
135
136WIZARD_XONTRIB_QUESTION = "Would you like to enable xontribs now, " + wiz.YN
137
138WIZARD_TAIL = """
139Thanks for using the xonsh configuration wizard!"""
140
141
142_XONFIG_SOURCE_FOREIGN_SHELL_COMMAND = collections.defaultdict(
143    lambda: "source-foreign", bash="source-bash", cmd="source-cmd", zsh="source-zsh"
144)
145
146
147def _dump_xonfig_foreign_shell(path, value):
148    shell = value["shell"]
149    shell = CANON_SHELL_NAMES.get(shell, shell)
150    cmd = [_XONFIG_SOURCE_FOREIGN_SHELL_COMMAND.get(shell)]
151    interactive = value.get("interactive", None)
152    if interactive is not None:
153        cmd.extend(["--interactive", str(interactive)])
154    login = value.get("login", None)
155    if login is not None:
156        cmd.extend(["--login", str(login)])
157    envcmd = value.get("envcmd", None)
158    if envcmd is not None:
159        cmd.extend(["--envcmd", envcmd])
160    aliascmd = value.get("aliasmd", None)
161    if aliascmd is not None:
162        cmd.extend(["--aliascmd", aliascmd])
163    extra_args = value.get("extra_args", None)
164    if extra_args:
165        cmd.extend(["--extra-args", repr(" ".join(extra_args))])
166    safe = value.get("safe", None)
167    if safe is not None:
168        cmd.extend(["--safe", str(safe)])
169    prevcmd = value.get("prevcmd", "")
170    if prevcmd:
171        cmd.extend(["--prevcmd", repr(prevcmd)])
172    postcmd = value.get("postcmd", "")
173    if postcmd:
174        cmd.extend(["--postcmd", repr(postcmd)])
175    funcscmd = value.get("funcscmd", None)
176    if funcscmd:
177        cmd.extend(["--funcscmd", repr(funcscmd)])
178    sourcer = value.get("sourcer", None)
179    if sourcer:
180        cmd.extend(["--sourcer", sourcer])
181    if cmd[0] == "source-foreign":
182        cmd.append(shell)
183    cmd.append('"echo loading xonsh foreign shell"')
184    return " ".join(cmd)
185
186
187def _dump_xonfig_env(path, value):
188    name = os.path.basename(path.rstrip("/"))
189    ensurer = builtins.__xonsh_env__.get_ensurer(name)
190    dval = ensurer.detype(value)
191    return "${name} = {val!r}".format(name=name, val=dval)
192
193
194def _dump_xonfig_xontribs(path, value):
195    return "xontrib load {0}".format(" ".join(value))
196
197
198@lazyobject
199def XONFIG_DUMP_RULES():
200    return {
201        "/": None,
202        "/env/": None,
203        "/foreign_shells/*/": _dump_xonfig_foreign_shell,
204        "/env/*": _dump_xonfig_env,
205        "/env/*/[0-9]*": None,
206        "/xontribs/": _dump_xonfig_xontribs,
207    }
208
209
210def make_fs_wiz():
211    """Makes the foreign shell part of the wizard."""
212    cond = wiz.create_truefalse_cond(prompt="Add a new foreign shell, " + wiz.YN)
213    fs = wiz.While(
214        cond=cond,
215        body=[
216            wiz.Input("shell name (e.g. bash): ", path="/foreign_shells/{idx}/shell"),
217            wiz.StoreNonEmpty(
218                "interactive shell [bool, default=True]: ",
219                converter=to_bool,
220                show_conversion=True,
221                path="/foreign_shells/{idx}/interactive",
222            ),
223            wiz.StoreNonEmpty(
224                "login shell [bool, default=False]: ",
225                converter=to_bool,
226                show_conversion=True,
227                path="/foreign_shells/{idx}/login",
228            ),
229            wiz.StoreNonEmpty(
230                "env command [str, default='env']: ",
231                path="/foreign_shells/{idx}/envcmd",
232            ),
233            wiz.StoreNonEmpty(
234                "alias command [str, default='alias']: ",
235                path="/foreign_shells/{idx}/aliascmd",
236            ),
237            wiz.StoreNonEmpty(
238                ("extra command line arguments [list of str, " "default=[]]: "),
239                converter=ast.literal_eval,
240                show_conversion=True,
241                path="/foreign_shells/{idx}/extra_args",
242            ),
243            wiz.StoreNonEmpty(
244                "safely handle exceptions [bool, default=True]: ",
245                converter=to_bool,
246                show_conversion=True,
247                path="/foreign_shells/{idx}/safe",
248            ),
249            wiz.StoreNonEmpty(
250                "pre-command [str, default='']: ", path="/foreign_shells/{idx}/prevcmd"
251            ),
252            wiz.StoreNonEmpty(
253                "post-command [str, default='']: ", path="/foreign_shells/{idx}/postcmd"
254            ),
255            wiz.StoreNonEmpty(
256                "foreign function command [str, default=None]: ",
257                path="/foreign_shells/{idx}/funcscmd",
258            ),
259            wiz.StoreNonEmpty(
260                "source command [str, default=None]: ",
261                path="/foreign_shells/{idx}/sourcer",
262            ),
263            wiz.Message(message="Foreign shell added.\n"),
264        ],
265    )
266    return fs
267
268
269def _wrap_paragraphs(text, width=70, **kwargs):
270    """Wraps paragraphs instead."""
271    pars = text.split("\n")
272    pars = ["\n".join(textwrap.wrap(p, width=width, **kwargs)) for p in pars]
273    s = "\n".join(pars)
274    return s
275
276
277ENVVAR_MESSAGE = """
278{{BOLD_CYAN}}${name}{{NO_COLOR}}
279{docstr}
280{{RED}}default value:{{NO_COLOR}} {default}
281{{RED}}current value:{{NO_COLOR}} {current}"""
282
283ENVVAR_PROMPT = "{BOLD_GREEN}>>>{NO_COLOR} "
284
285
286def make_exit_message():
287    """Creates a message for how to exit the wizard."""
288    shell_type = builtins.__xonsh_shell__.shell_type
289    keyseq = "Ctrl-D" if shell_type == "readline" else "Ctrl-C"
290    msg = "To exit the wizard at any time, press {BOLD_UNDERLINE_CYAN}"
291    msg += keyseq + "{NO_COLOR}.\n"
292    m = wiz.Message(message=msg)
293    return m
294
295
296def make_envvar(name):
297    """Makes a StoreNonEmpty node for an environment variable."""
298    env = builtins.__xonsh_env__
299    vd = env.get_docs(name)
300    if not vd.configurable:
301        return
302    default = vd.default
303    if "\n" in default:
304        default = "\n" + _wrap_paragraphs(default, width=69)
305    curr = env.get(name)
306    if is_string(curr) and is_template_string(curr):
307        curr = curr.replace("{", "{{").replace("}", "}}")
308    curr = pprint.pformat(curr, width=69)
309    if "\n" in curr:
310        curr = "\n" + curr
311    msg = ENVVAR_MESSAGE.format(
312        name=name,
313        default=default,
314        current=curr,
315        docstr=_wrap_paragraphs(vd.docstr, width=69),
316    )
317    mnode = wiz.Message(message=msg)
318    ens = env.get_ensurer(name)
319    path = "/env/" + name
320    pnode = wiz.StoreNonEmpty(
321        ENVVAR_PROMPT,
322        converter=ens.convert,
323        show_conversion=True,
324        path=path,
325        retry=True,
326        store_raw=vd.store_as_str,
327    )
328    return mnode, pnode
329
330
331def _make_flat_wiz(kidfunc, *args):
332    kids = map(kidfunc, *args)
333    flatkids = []
334    for k in kids:
335        if k is None:
336            continue
337        flatkids.extend(k)
338    wizard = wiz.Wizard(children=flatkids)
339    return wizard
340
341
342def make_env_wiz():
343    """Makes an environment variable wizard."""
344    w = _make_flat_wiz(make_envvar, sorted(builtins.__xonsh_env__._docs.keys()))
345    return w
346
347
348XONTRIB_PROMPT = "{BOLD_GREEN}Add this xontrib{NO_COLOR}, " + wiz.YN
349
350
351def _xontrib_path(visitor=None, node=None, val=None):
352    # need this to append only based on user-selected size
353    return ("xontribs", len(visitor.state.get("xontribs", ())))
354
355
356def make_xontrib(xontrib, package):
357    """Makes a message and StoreNonEmpty node for a xontrib."""
358    name = xontrib.get("name", "<unknown-xontrib-name>")
359    msg = "\n{BOLD_CYAN}" + name + "{NO_COLOR}\n"
360    if "url" in xontrib:
361        msg += "{RED}url:{NO_COLOR} " + xontrib["url"] + "\n"
362    if "package" in xontrib:
363        msg += "{RED}package:{NO_COLOR} " + xontrib["package"] + "\n"
364    if "url" in package:
365        if "url" in xontrib and package["url"] != xontrib["url"]:
366            msg += "{RED}package-url:{NO_COLOR} " + package["url"] + "\n"
367    if "license" in package:
368        msg += "{RED}license:{NO_COLOR} " + package["license"] + "\n"
369    msg += "{PURPLE}installed?{NO_COLOR} "
370    msg += ("no" if find_xontrib(name) is None else "yes") + "\n"
371    desc = xontrib.get("description", "")
372    if not isinstance(desc, str):
373        desc = "".join(desc)
374    msg += _wrap_paragraphs(desc, width=69)
375    if msg.endswith("\n"):
376        msg = msg[:-1]
377    mnode = wiz.Message(message=msg)
378    convert = lambda x: name if to_bool(x) else wiz.Unstorable
379    pnode = wiz.StoreNonEmpty(XONTRIB_PROMPT, converter=convert, path=_xontrib_path)
380    return mnode, pnode
381
382
383def make_xontribs_wiz():
384    """Makes a xontrib wizard."""
385    md = xontrib_metadata()
386    pkgs = [md["packages"].get(d.get("package", None), {}) for d in md["xontribs"]]
387    w = _make_flat_wiz(make_xontrib, md["xontribs"], pkgs)
388    return w
389
390
391def make_xonfig_wizard(default_file=None, confirm=False, no_wizard_file=None):
392    """Makes a configuration wizard for xonsh config file.
393
394    Parameters
395    ----------
396    default_file : str, optional
397        Default filename to save and load to. User will still be prompted.
398    confirm : bool, optional
399        Confirm that the main part of the wizard should be run.
400    no_wizard_file : str, optional
401        Filename for that will flag to future runs that the wizard should not be
402        run again. If None (default), this defaults to default_file.
403    """
404    w = wiz.Wizard(
405        children=[
406            wiz.Message(message=WIZARD_HEAD),
407            make_exit_message(),
408            wiz.Message(message=WIZARD_FS),
409            make_fs_wiz(),
410            wiz.Message(message=WIZARD_ENV),
411            wiz.YesNo(question=WIZARD_ENV_QUESTION, yes=make_env_wiz(), no=wiz.Pass()),
412            wiz.Message(message=WIZARD_XONTRIB),
413            wiz.YesNo(
414                question=WIZARD_XONTRIB_QUESTION, yes=make_xontribs_wiz(), no=wiz.Pass()
415            ),
416            wiz.Message(message="\n" + HR + "\n"),
417            wiz.FileInserter(
418                prefix="# XONSH WIZARD START",
419                suffix="# XONSH WIZARD END",
420                dump_rules=XONFIG_DUMP_RULES,
421                default_file=default_file,
422                check=True,
423            ),
424            wiz.Message(message=WIZARD_TAIL),
425        ]
426    )
427    if confirm:
428        q = (
429            "Would you like to run the xonsh configuration wizard now?\n\n"
430            "1. Yes (You can abort at any time)\n"
431            "2. No, but ask me next time.\n"
432            "3. No, and don't ask me again.\n\n"
433            "1, 2, or 3 [default: 2]? "
434        )
435        no_wizard_file = default_file if no_wizard_file is None else no_wizard_file
436        passer = wiz.Pass()
437        saver = wiz.SaveJSON(
438            check=False, ask_filename=False, default_file=no_wizard_file
439        )
440        w = wiz.Question(
441            q, {1: w, 2: passer, 3: saver}, converter=lambda x: int(x) if x != "" else 2
442        )
443    return w
444
445
446def _wizard(ns):
447    env = builtins.__xonsh_env__
448    shell = builtins.__xonsh_shell__.shell
449    fname = env.get("XONSHRC")[-1] if ns.file is None else ns.file
450    no_wiz = os.path.join(env.get("XONSH_CONFIG_DIR"), "no-wizard")
451    w = make_xonfig_wizard(
452        default_file=fname, confirm=ns.confirm, no_wizard_file=no_wiz
453    )
454    tempenv = {"PROMPT": "", "XONSH_STORE_STDOUT": False}
455    pv = wiz.PromptVisitor(w, store_in_history=False, multiline=False)
456
457    @contextlib.contextmanager
458    def force_hide():
459        if env.get("XONSH_STORE_STDOUT") and hasattr(shell, "_force_hide"):
460            orig, shell._force_hide = shell._force_hide, False
461            yield
462            shell._force_hide = orig
463        else:
464            yield
465
466    with force_hide(), env.swap(tempenv):
467        try:
468            pv.visit()
469        except (KeyboardInterrupt, Exception):
470            print()
471            print_exception()
472
473
474def _xonfig_format_human(data):
475    wcol1 = wcol2 = 0
476    for key, val in data:
477        wcol1 = max(wcol1, len(key))
478        wcol2 = max(wcol2, len(str(val)))
479    hr = "+" + ("-" * (wcol1 + 2)) + "+" + ("-" * (wcol2 + 2)) + "+\n"
480    row = "| {key!s:<{wcol1}} | {val!s:<{wcol2}} |\n"
481    s = hr
482    for key, val in data:
483        s += row.format(key=key, wcol1=wcol1, val=val, wcol2=wcol2)
484    s += hr
485    return s
486
487
488def _xonfig_format_json(data):
489    data = {k.replace(" ", "_"): v for k, v in data}
490    s = json.dumps(data, sort_keys=True, indent=1) + "\n"
491    return s
492
493
494def _info(ns):
495    env = builtins.__xonsh_env__
496    try:
497        ply.__version__ = ply.__version__
498    except AttributeError:
499        ply.__version__ = "3.8"
500    data = [("xonsh", XONSH_VERSION)]
501    hash_, date_ = githash()
502    if hash_:
503        data.append(("Git SHA", hash_))
504        data.append(("Commit Date", date_))
505    data.extend(
506        [
507            ("Python", "{}.{}.{}".format(*PYTHON_VERSION_INFO)),
508            ("PLY", ply.__version__),
509            ("have readline", is_readline_available()),
510            ("prompt toolkit", ptk_version() or None),
511            ("shell type", env.get("SHELL_TYPE")),
512            ("pygments", pygments_version()),
513            ("on posix", bool(ON_POSIX)),
514            ("on linux", bool(ON_LINUX)),
515        ]
516    )
517    if ON_LINUX:
518        data.append(("distro", linux_distro()))
519    data.extend(
520        [
521            ("on darwin", ON_DARWIN),
522            ("on windows", ON_WINDOWS),
523            ("on cygwin", ON_CYGWIN),
524            ("on msys2", ON_MSYS),
525            ("is superuser", is_superuser()),
526            ("default encoding", DEFAULT_ENCODING),
527            ("xonsh encoding", env.get("XONSH_ENCODING")),
528            ("encoding errors", env.get("XONSH_ENCODING_ERRORS")),
529        ]
530    )
531    formatter = _xonfig_format_json if ns.json else _xonfig_format_human
532    s = formatter(data)
533    return s
534
535
536def _styles(ns):
537    env = builtins.__xonsh_env__
538    curr = env.get("XONSH_COLOR_STYLE")
539    styles = sorted(color_style_names())
540    if ns.json:
541        s = json.dumps(styles, sort_keys=True, indent=1)
542        print(s)
543        return
544    lines = []
545    for style in styles:
546        if style == curr:
547            lines.append("* {GREEN}" + style + "{NO_COLOR}")
548        else:
549            lines.append("  " + style)
550    s = "\n".join(lines)
551    print_color(s)
552
553
554def _str_colors(cmap, cols):
555    color_names = sorted(cmap.keys(), key=(lambda s: (len(s), s)))
556    grper = lambda s: min(cols // (len(s) + 1), 8)
557    lines = []
558    for n, group in itertools.groupby(color_names, key=grper):
559        width = cols // n
560        line = ""
561        for i, name in enumerate(group):
562            buf = " " * (width - len(name))
563            line += "{" + name + "}" + name + "{NO_COLOR}" + buf
564            if (i + 1) % n == 0:
565                lines.append(line)
566                line = ""
567        if len(line) != 0:
568            lines.append(line)
569    return "\n".join(lines)
570
571
572def _tok_colors(cmap, cols):
573    from xonsh.style_tools import Color
574
575    nc = Color.NO_COLOR
576    names_toks = {}
577    for t in cmap.keys():
578        name = str(t)
579        if name.startswith("Token.Color."):
580            _, _, name = name.rpartition(".")
581        names_toks[name] = t
582    color_names = sorted(names_toks.keys(), key=(lambda s: (len(s), s)))
583    grper = lambda s: min(cols // (len(s) + 1), 8)
584    toks = []
585    for n, group in itertools.groupby(color_names, key=grper):
586        width = cols // n
587        for i, name in enumerate(group):
588            toks.append((names_toks[name], name))
589            buf = " " * (width - len(name))
590            if (i + 1) % n == 0:
591                buf += "\n"
592            toks.append((nc, buf))
593        if not toks[-1][1].endswith("\n"):
594            toks[-1] = (nc, toks[-1][1] + "\n")
595    return toks
596
597
598def _colors(args):
599    columns, _ = shutil.get_terminal_size()
600    columns -= int(ON_WINDOWS)
601    style_stash = builtins.__xonsh_env__["XONSH_COLOR_STYLE"]
602
603    if args.style is not None:
604        if args.style not in color_style_names():
605            print("Invalid style: {}".format(args.style))
606            return
607        builtins.__xonsh_env__["XONSH_COLOR_STYLE"] = args.style
608
609    color_map = color_style()
610    akey = next(iter(color_map))
611    if isinstance(akey, str):
612        s = _str_colors(color_map, columns)
613    else:
614        s = _tok_colors(color_map, columns)
615    print_color(s)
616    builtins.__xonsh_env__["XONSH_COLOR_STYLE"] = style_stash
617
618
619def _tutorial(args):
620    import webbrowser
621
622    webbrowser.open("http://xon.sh/tutorial.html")
623
624
625@functools.lru_cache(1)
626def _xonfig_create_parser():
627    p = argparse.ArgumentParser(
628        prog="xonfig", description="Manages xonsh configuration."
629    )
630    subp = p.add_subparsers(title="action", dest="action")
631    info = subp.add_parser(
632        "info", help=("displays configuration information, " "default action")
633    )
634    info.add_argument(
635        "--json", action="store_true", default=False, help="reports results as json"
636    )
637    wiz = subp.add_parser("wizard", help="displays configuration information")
638    wiz.add_argument(
639        "--file", default=None, help="config file location, default=$XONSHRC"
640    )
641    wiz.add_argument(
642        "--confirm",
643        action="store_true",
644        default=False,
645        help="confirm that the wizard should be run.",
646    )
647    sty = subp.add_parser("styles", help="prints available xonsh color styles")
648    sty.add_argument(
649        "--json", action="store_true", default=False, help="reports results as json"
650    )
651    colors = subp.add_parser("colors", help="preview color style")
652    colors.add_argument(
653        "style", nargs="?", default=None, help="style to preview, default: <current>"
654    )
655    subp.add_parser("tutorial", help="Launch tutorial in browser.")
656    return p
657
658
659_XONFIG_MAIN_ACTIONS = {
660    "info": _info,
661    "wizard": _wizard,
662    "styles": _styles,
663    "colors": _colors,
664    "tutorial": _tutorial,
665}
666
667
668def xonfig_main(args=None):
669    """Main xonfig entry point."""
670    if not args or (
671        args[0] not in _XONFIG_MAIN_ACTIONS and args[0] not in {"-h", "--help"}
672    ):
673        args.insert(0, "info")
674    parser = _xonfig_create_parser()
675    ns = parser.parse_args(args)
676    if ns.action is None:  # apply default action
677        ns = parser.parse_args(["info"] + args)
678    return _XONFIG_MAIN_ACTIONS[ns.action](ns)
679
680
681@lazyobject
682def STRIP_COLOR_RE():
683    return re.compile("{.*?}")
684
685
686def _align_string(string, align="<", fill=" ", width=80):
687    """ Align and pad a color formatted string """
688    linelen = len(STRIP_COLOR_RE.sub("", string))
689    padlen = max(width - linelen, 0)
690    if align == "^":
691        return fill * (padlen // 2) + string + fill * (padlen // 2 + padlen % 2)
692    elif align == ">":
693        return fill * padlen + string
694    elif align == "<":
695        return string + fill * padlen
696    else:
697        return string
698
699
700@lazyobject
701def TAGLINES():
702    return [
703        "Exofrills in the shell",
704        "No frills in the shell",
705        "Become the Lord of the Files",
706        "Break out of your shell",
707        "The only shell that is also a shell",
708        "All that is and all that shell be",
709        "It cannot be that hard",
710        "Pass the xonsh, Piggy",
711        "Piggy glanced nervously into hell and cradled the xonsh",
712        "The xonsh is a symbol",
713        "It is pronounced conch",
714        "The shell, bourne again",
715        "Snailed it",
716        "Starfish loves you",
717        "Come snail away",
718        "This is Major Tom to Ground Xonshtrol",
719        "Sally sells csh and keeps xonsh to herself",
720        "Nice indeed. Everything's accounted for, except your old shell.",
721        "I wanna thank you for putting me back in my snail shell",
722        "Crustaceanly Yours",
723        "With great shell comes great reproducibility",
724        "None shell pass",
725        "You shell not pass!",
726        "The x-on shell",
727        "Ever wonder why there isn't a Taco Shell? Because it is a corny idea.",
728        "The carcolh will catch you!",
729        "People xonshtantly mispronounce these things",
730        "WHAT...is your favorite shell?",
731        "Conches for the xonsh god!",
732        "Python-powered, cross-platform, Unix-gazing shell",
733        "Tab completion in Alderaan places",
734        "This fix was trickier than expected",
735        "The unholy cross of Bash/Python",
736    ]
737
738
739# list of strings or tuples (string, align, fill)
740WELCOME_MSG = [
741    "",
742    ("{{INTENSE_WHITE}}Welcome to the xonsh shell ({version}){{NO_COLOR}}", "^", " "),
743    "",
744    ("{{INTENSE_RED}}~{{NO_COLOR}} {tagline} {{INTENSE_RED}}~{{NO_COLOR}}", "^", " "),
745    "",
746    ("{{INTENSE_BLACK}}", "<", "-"),
747    "{{GREEN}}xonfig{{NO_COLOR}} tutorial    {{INTENSE_WHITE}}->    Launch the tutorial in "
748    "the browser{{NO_COLOR}}",
749    "{{GREEN}}xonfig{{NO_COLOR}} wizard      {{INTENSE_WHITE}}->    Run the configuration "
750    "wizard and claim your shell {{NO_COLOR}}",
751    "{{INTENSE_BLACK}}(Note: Run the Wizard or create a {{RED}}~/.xonshrc{{INTENSE_BLACK}} file "
752    "to suppress the welcome screen)",
753    "",
754]
755
756
757def print_welcome_screen():
758    subst = dict(tagline=random.choice(list(TAGLINES)), version=XONSH_VERSION)
759    for elem in WELCOME_MSG:
760        if isinstance(elem, str):
761            elem = (elem, "", "")
762        line = elem[0].format(**subst)
763        termwidth = os.get_terminal_size().columns
764        line = _align_string(line, elem[1], elem[2], width=termwidth)
765        print_color(line)
766