1# Copyright (c) 2009, Google Inc. All rights reserved.
2#
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7#     * Redistributions of source code must retain the above copyright
8# notice, this list of conditions and the following disclaimer.
9#     * Redistributions in binary form must reproduce the above
10# copyright notice, this list of conditions and the following disclaimer
11# in the documentation and/or other materials provided with the
12# distribution.
13#     * Neither the name of Google Inc. nor the names of its
14# contributors may be used to endorse or promote products derived from
15# this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29import getpass
30import logging
31import os
32import platform
33import re
34import shlex
35import subprocess
36import sys
37import webbrowser
38
39from webkitpy.common.system.executive import Executive
40from webkitpy.common.system.platforminfo import PlatformInfo
41
42
43_log = logging.getLogger(__name__)
44
45
46try:
47    import readline
48except ImportError:
49    if sys.platform != "win32":
50        # There is no readline module for win32, not much to do except cry.
51        _log.warn("Unable to import readline.")
52
53
54class User(object):
55    DEFAULT_NO = 'n'
56    DEFAULT_YES = 'y'
57
58    def __init__(self, platforminfo=None):
59        # We cannot get the PlatformInfo object from a SystemHost because
60        # User is part of SystemHost itself.
61        self._platforminfo = platforminfo or PlatformInfo(sys, platform, Executive())
62
63    # FIXME: These are @classmethods because bugzilla.py doesn't have a Tool object (thus no User instance).
64    @classmethod
65    def prompt(cls, message, repeat=1, raw_input=raw_input):
66        response = None
67        while (repeat and not response):
68            repeat -= 1
69            response = raw_input(message)
70        return response
71
72    @classmethod
73    def prompt_password(cls, message, repeat=1):
74        return cls.prompt(message, repeat=repeat, raw_input=getpass.getpass)
75
76    @classmethod
77    def prompt_with_multiple_lists(cls, list_title, subtitles, lists, can_choose_multiple=False, raw_input=raw_input):
78        item_index = 0
79        cumulated_list = []
80        print list_title
81        for i in range(len(subtitles)):
82            print "\n" + subtitles[i]
83            for item in lists[i]:
84                item_index += 1
85                print "%2d. %s" % (item_index, item)
86            cumulated_list += lists[i]
87        return cls._wait_on_list_response(cumulated_list, can_choose_multiple, raw_input)
88
89    @classmethod
90    def _wait_on_list_response(cls, list_items, can_choose_multiple, raw_input):
91        while True:
92            if can_choose_multiple:
93                response = cls.prompt("Enter one or more numbers (comma-separated) or ranges (e.g. 3-7), or \"all\": ", raw_input=raw_input)
94                if not response.strip() or response == "all":
95                    return list_items
96
97                try:
98                    indices = []
99                    for value in re.split("\s*,\s*", response):
100                        parts = value.split('-')
101                        if len(parts) == 2:
102                            indices += range(int(parts[0]) - 1, int(parts[1]))
103                        else:
104                            indices.append(int(value) - 1)
105                except ValueError, err:
106                    continue
107
108                return [list_items[i] for i in indices]
109            else:
110                try:
111                    result = int(cls.prompt("Enter a number: ", raw_input=raw_input)) - 1
112                except ValueError, err:
113                    continue
114                return list_items[result]
115
116    @classmethod
117    def prompt_with_list(cls, list_title, list_items, can_choose_multiple=False, raw_input=raw_input):
118        print list_title
119        i = 0
120        for item in list_items:
121            i += 1
122            print "%2d. %s" % (i, item)
123        return cls._wait_on_list_response(list_items, can_choose_multiple, raw_input)
124
125    def edit(self, files):
126        editor = os.environ.get("EDITOR") or "vi"
127        args = shlex.split(editor)
128        # Note: Not thread safe: http://bugs.python.org/issue2320
129        subprocess.call(args + files)
130
131    def _warn_if_application_is_xcode(self, edit_application):
132        if "Xcode" in edit_application:
133            print "Instead of using Xcode.app, consider using EDITOR=\"xed --wait\"."
134
135    def edit_changelog(self, files):
136        edit_application = os.environ.get("CHANGE_LOG_EDIT_APPLICATION")
137        if edit_application and self._platforminfo.is_mac():
138            # On Mac we support editing ChangeLogs using an application.
139            args = shlex.split(edit_application)
140            print "Using editor in the CHANGE_LOG_EDIT_APPLICATION environment variable."
141            print "Please quit the editor application when done editing."
142            self._warn_if_application_is_xcode(edit_application)
143            subprocess.call(["open", "-W", "-n", "-a"] + args + files)
144            return
145        self.edit(files)
146
147    def page(self, message):
148        pager = os.environ.get("PAGER") or "less"
149        try:
150            # Note: Not thread safe: http://bugs.python.org/issue2320
151            child_process = subprocess.Popen([pager], stdin=subprocess.PIPE)
152            child_process.communicate(input=message)
153        except IOError, e:
154            pass
155
156    def confirm(self, message=None, default=DEFAULT_YES, raw_input=raw_input):
157        if not message:
158            message = "Continue?"
159        choice = {'y': 'Y/n', 'n': 'y/N'}[default]
160        response = raw_input("%s [%s]: " % (message, choice))
161        if not response:
162            response = default
163        return response.lower() == 'y'
164
165    def can_open_url(self):
166        try:
167            webbrowser.get()
168            return True
169        except webbrowser.Error, e:
170            return False
171
172    def open_url(self, url):
173        if not self.can_open_url():
174            _log.warn("Failed to open %s" % url)
175        webbrowser.open(url)
176