# Copyright 1998-2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

__docformat__ = "epytext"

import errno
import itertools
import re
import socket
import subprocess
import sys
from typing import Optional

import portage

portage.proxy.lazyimport.lazyimport(
    globals(),
    "portage.process:spawn",
    "portage.util:writemsg",
    "portage.util.futures:asyncio",
)
import portage.util.formatter as formatter

from portage import os
from portage import _encodings
from portage import _unicode_encode
from portage import _unicode_decode
from portage.const import COLOR_MAP_FILE
from portage.exception import (
    CommandNotFound,
    FileNotFound,
    ParseError,
    PermissionDenied,
    PortageException,
)
from portage.localization import _

havecolor = 1
dotitles = 1

"""Maps style class to tuple of attribute names."""
_styles = {}

"""Maps attribute name to ansi code."""

esc_seq = "\x1b["

codes = {
    "normal": f"{esc_seq}0m",
    "reset": f"{esc_seq}39;49;00m",
    "bold": f"{esc_seq}01m",
    "faint": f"{esc_seq}02m",
    "standout": f"{esc_seq}03m",
    "underline": f"{esc_seq}04m",
    "blink": f"{esc_seq}05m",
    "overline": f"{esc_seq}06m",
    "reverse": f"{esc_seq}07m",
    "invisible": f"{esc_seq}08m",
    "no-attr": f"{esc_seq}22m",
    "no-standout": f"{esc_seq}23m",
    "no-underline": f"{esc_seq}24m",
    "no-blink": f"{esc_seq}25m",
    "no-overline": f"{esc_seq}26m",
    "no-reverse": f"{esc_seq}27m",
    "bg_black": f"{esc_seq}40m",
    "bg_darkred": f"{esc_seq}41m",
    "bg_darkgreen": f"{esc_seq}42m",
    "bg_brown": f"{esc_seq}43m",
    "bg_darkblue": f"{esc_seq}44m",
    "bg_purple": f"{esc_seq}45m",
    "bg_teal": f"{esc_seq}46m",
    "bg_lightgray": f"{esc_seq}47m",
    "bg_default": f"{esc_seq}49m",
    "bg_darkyellow": f"{esc_seq}43m",
}


def color(fg, bg="default", attr=["normal"]):
    myansicodechain = itertools.chain((codes[fg]), (codes[x] for x in [bg, *attr]))
    return "".join(myansicodechain)


ansi_codes = [y for x in range(30, 38) for y in (f"{x}m", f"{x};01m")]


rgb_ansi_colors = [
    "0x000000",
    "0x555555",
    "0xAA0000",
    "0xFF5555",
    "0x00AA00",
    "0x55FF55",
    "0xAA5500",
    "0xFFFF55",
    "0x0000AA",
    "0x5555FF",
    "0xAA00AA",
    "0xFF55FF",
    "0x00AAAA",
    "0x55FFFF",
    "0xAAAAAA",
    "0xFFFFFF",
]

for x in range(len(rgb_ansi_colors)):
    codes[rgb_ansi_colors[x]] = esc_seq + ansi_codes[x]

del x

codes["black"] = codes["0x000000"]
codes["darkgray"] = codes["0x555555"]

codes["red"] = codes["0xFF5555"]
codes["darkred"] = codes["0xAA0000"]

codes["green"] = codes["0x55FF55"]
codes["darkgreen"] = codes["0x00AA00"]

codes["yellow"] = codes["0xFFFF55"]
codes["brown"] = codes["0xAA5500"]

codes["blue"] = codes["0x5555FF"]
codes["darkblue"] = codes["0x0000AA"]

codes["fuchsia"] = codes["0xFF55FF"]
codes["purple"] = codes["0xAA00AA"]

codes["turquoise"] = codes["0x55FFFF"]
codes["teal"] = codes["0x00AAAA"]

codes["white"] = codes["0xFFFFFF"]
codes["lightgray"] = codes["0xAAAAAA"]

codes["darkteal"] = codes["turquoise"]
# Some terminals have darkyellow instead of brown.
codes["0xAAAA00"] = codes["brown"]
codes["darkyellow"] = codes["0xAAAA00"]


# Colors from /lib/gentoo/functions.sh
_styles["BAD"] = ("red",)
_styles["BRACKET"] = ("blue",)
_styles["ERR"] = ("red",)
_styles["GOOD"] = ("green",)
_styles["HILITE"] = ("teal",)
_styles["INFO"] = ("darkgreen",)
_styles["LOG"] = ("green",)
_styles["NORMAL"] = ("normal",)
_styles["QAWARN"] = ("brown",)
_styles["WARN"] = ("yellow",)

# Portage functions
_styles["INFORM"] = ("darkgreen",)
_styles["UNMERGE_WARN"] = ("red",)
_styles["SECURITY_WARN"] = ("red",)
_styles["MERGE_LIST_PROGRESS"] = ("yellow",)
_styles["PKG_BLOCKER"] = ("red",)
_styles["PKG_BLOCKER_SATISFIED"] = ("teal",)
_styles["PKG_MERGE"] = ("darkgreen",)
_styles["PKG_MERGE_SYSTEM"] = ("darkgreen",)
_styles["PKG_MERGE_WORLD"] = ("green",)
_styles["PKG_BINARY_MERGE"] = ("purple",)
_styles["PKG_BINARY_MERGE_SYSTEM"] = ("purple",)
_styles["PKG_BINARY_MERGE_WORLD"] = ("fuchsia",)
_styles["PKG_UNINSTALL"] = ("red",)
_styles["PKG_NOMERGE"] = ("teal",)
_styles["PKG_NOMERGE_SYSTEM"] = ("teal",)
_styles["PKG_NOMERGE_WORLD"] = ("blue",)
_styles["PROMPT_CHOICE_DEFAULT"] = ("green",)
_styles["PROMPT_CHOICE_OTHER"] = ("red",)


def _parse_color_map(config_root="/", onerror=None):
    """
    Parse /etc/portage/color.map and return a dict of error codes.

    @param onerror: an optional callback to handle any ParseError that would
            otherwise be raised
    @type onerror: callable
    @rtype: dict
    @return: a dictionary mapping color classes to color codes
    """
    global codes, _styles
    myfile = os.path.join(config_root, COLOR_MAP_FILE)
    ansi_code_pattern = re.compile("^[0-9;]*m$")
    quotes = "'\""

    def strip_quotes(token):
        if token[0] in quotes and token[0] == token[-1]:
            token = token[1:-1]
        return token

    try:
        with open(
            _unicode_encode(myfile, encoding=_encodings["fs"], errors="strict"),
            encoding=_encodings["content"],
            errors="replace",
        ) as f:
            lines = f.readlines()
        for lineno, line in enumerate(lines):
            commenter_pos = line.find("#")
            line = line[:commenter_pos].strip()

            if len(line) == 0:
                continue

            split_line = line.split("=")
            if len(split_line) != 2:
                e = ParseError(
                    _("'%s', line %s: expected exactly one occurrence of '=' operator")
                    % (myfile, lineno)
                )
                raise e
                if onerror:
                    onerror(e)
                else:
                    raise e
                continue

            k = strip_quotes(split_line[0].strip())
            v = strip_quotes(split_line[1].strip())
            if not k in _styles and not k in codes:
                e = ParseError(
                    _("'%s', line %s: Unknown variable: '%s'") % (myfile, lineno, k)
                )
                if onerror:
                    onerror(e)
                else:
                    raise e
                continue
            if ansi_code_pattern.match(v):
                if k in _styles:
                    _styles[k] = (esc_seq + v,)
                elif k in codes:
                    codes[k] = esc_seq + v
            else:
                code_list = []
                for x in v.split():
                    if x in codes:
                        if k in _styles:
                            code_list.append(x)
                        elif k in codes:
                            code_list.append(codes[x])
                    else:
                        e = ParseError(
                            _("'%s', line %s: Undefined: '%s'") % (myfile, lineno, x)
                        )
                        if onerror:
                            onerror(e)
                        else:
                            raise e
                if k in _styles:
                    _styles[k] = tuple(code_list)
                elif k in codes:
                    codes[k] = "".join(code_list)
    except OSError as e:
        if e.errno == errno.ENOENT:
            raise FileNotFound(myfile)
        elif e.errno == errno.EACCES:
            raise PermissionDenied(myfile)
        raise


def nc_len(mystr):
    tmp = re.sub(esc_seq + "^m]+m", "", mystr)
    return len(tmp)


_legal_terms_re = re.compile(
    r"^(xterm|xterm-color|Eterm|aterm|rxvt|screen|kterm|rxvt-unicode|gnome|interix|tmux|st-256color|alacritty|konsole|foot)"
)
_disable_xtermTitle = None
_max_xtermTitle_len = 253


def xtermTitle(mystr, raw=False):
    global _disable_xtermTitle
    if _disable_xtermTitle is None:
        _disable_xtermTitle = not (
            sys.__stderr__.isatty()
            and "TERM" in os.environ
            and _legal_terms_re.match(os.environ["TERM"]) is not None
        )

    if dotitles and not _disable_xtermTitle:
        if not raw:
            hostname = socket.gethostname()
            mystr = f"{hostname}: {mystr}"
        # If the title string is too big then the terminal can
        # misbehave. Therefore, truncate it if it's too big.
        if len(mystr) > _max_xtermTitle_len:
            mystr = mystr[:_max_xtermTitle_len]
        if not raw:
            mystr = f"\x1b]0;{mystr}\x07"

        # avoid potential UnicodeEncodeError
        mystr = _unicode_encode(
            mystr, encoding=_encodings["stdio"], errors="backslashreplace"
        )
        f = sys.stderr.buffer
        f.write(mystr)
        f.flush()


default_xterm_title = None


def xtermTitleReset():
    global default_xterm_title
    if default_xterm_title is None:
        prompt_command = os.environ.get("PROMPT_COMMAND")
        if prompt_command == "":
            default_xterm_title = ""
        elif prompt_command is not None:
            if (
                dotitles
                and "TERM" in os.environ
                and _legal_terms_re.match(os.environ["TERM"]) is not None
                and sys.__stderr__.isatty()
            ):
                from portage.process import find_binary, spawn

                shell = os.environ.get("SHELL")
                if not shell or not os.access(shell, os.EX_OK):
                    shell = find_binary("sh")
                if shell:
                    spawn(
                        [shell, "-c", prompt_command],
                        env=os.environ,
                        fd_pipes={
                            0: portage._get_stdin().fileno(),
                            1: sys.__stderr__.fileno(),
                            2: sys.__stderr__.fileno(),
                        },
                    )
                else:
                    os.system(prompt_command)
            return
        else:
            pwd = os.environ.get("PWD", "")
            home = os.environ.get("HOME", "")
            if home != "" and pwd.startswith(home):
                pwd = "~" + pwd[len(home) :]
            default_xterm_title = "\x1b]0;{}@{}:{}\x07".format(
                os.environ.get("LOGNAME", ""),
                os.environ.get("HOSTNAME", "").split(".", 1)[0],
                pwd,
            )
    xtermTitle(default_xterm_title, raw=True)


def notitles():
    "turn off title setting"
    dotitles = 0


def nocolor():
    "turn off colorization"
    global havecolor
    havecolor = 0


def resetColor():
    return codes["reset"]


def style_to_ansi_code(style):
    """
    @param style: A style name
    @type style: String
    @rtype: String
    @return: A string containing one or more ansi escape codes that are
            used to render the given style.
    """
    ret = ""
    for attr_name in _styles[style]:
        # allow stuff that has found it's way through ansi_code_pattern
        ret += codes.get(attr_name, attr_name)
    return ret


def colormap():
    mycolors = []
    for c in (
        "BAD",
        "BRACKET",
        "ERR",
        "GOOD",
        "HILITE",
        "INFO",
        "LOG",
        "NORMAL",
        "QAWARN",
        "WARN",
    ):
        mycolors.append(f"PORTAGE_COLOR_{c}=$'{style_to_ansi_code(c)}'")
    return "\n".join(mycolors)


def colorize(color_key, text):
    global havecolor
    if havecolor:
        if color_key in codes:
            return codes[color_key] + text + codes["reset"]
        if color_key in _styles:
            return style_to_ansi_code(color_key) + text + codes["reset"]
    return text


compat_functions_colors = [
    "bold",
    "white",
    "teal",
    "turquoise",
    "darkteal",
    "fuchsia",
    "purple",
    "blue",
    "darkblue",
    "green",
    "darkgreen",
    "yellow",
    "brown",
    "darkyellow",
    "red",
    "darkred",
]


class create_color_func:
    __slots__ = ("_color_key",)

    def __init__(self, color_key):
        self._color_key = color_key

    def __call__(self, text):
        return colorize(self._color_key, text)


for c in compat_functions_colors:
    globals()[c] = create_color_func(c)


class ConsoleStyleFile:
    """
    A file-like object that behaves something like
    the colorize() function. Style identifiers
    passed in via the new_styles() method will be used to
    apply console codes to output.
    """

    def __init__(self, f):
        self._file = f
        self._styles = None
        self.write_listener = None

    def new_styles(self, styles):
        self._styles = styles

    def write(self, s):
        # In python-2.6, DumbWriter.send_line_break() can write
        # non-unicode '\n' which fails with TypeError if self._file
        # is a text stream such as io.StringIO. Therefore, make sure
        # input is converted to unicode when necessary.
        s = _unicode_decode(s)
        global havecolor
        if havecolor and self._styles:
            styled_s = []
            for style in self._styles:
                styled_s.append(style_to_ansi_code(style))
            styled_s.append(s)
            styled_s.append(codes["reset"])
            self._write(self._file, "".join(styled_s))
        else:
            self._write(self._file, s)
        if self.write_listener:
            self._write(self.write_listener, s)

    def _write(self, f, s):
        # avoid potential UnicodeEncodeError
        if f in (sys.stdout, sys.stderr):
            s = _unicode_encode(
                s, encoding=_encodings["stdio"], errors="backslashreplace"
            )
            f = f.buffer
        f.write(s)

    def writelines(self, lines):
        for s in lines:
            self.write(s)

    def flush(self):
        self._file.flush()

    def close(self):
        self._file.close()


class StyleWriter(formatter.DumbWriter):
    """
    This is just a DumbWriter with a hook in the new_styles() method
    that passes a styles tuple as a single argument to a callable
    style_listener attribute.
    """

    def __init__(self, **kwargs):
        formatter.DumbWriter.__init__(self, **kwargs)
        self.style_listener = None

    def new_styles(self, styles):
        formatter.DumbWriter.new_styles(self, styles)
        if self.style_listener:
            self.style_listener(styles)


def get_term_size(fd=None):
    """
    Get the number of lines and columns of the tty that is connected to
    fd.  Returns a tuple of (lines, columns) or (0, 0) if an error
    occurs. The curses module is used if available, otherwise the output of
    `stty size` is parsed. The lines and columns values are guaranteed to be
    greater than or equal to zero, since a negative COLUMNS variable is
    known to prevent some commands from working (see bug #394091).
    """
    if fd is None:
        fd = sys.stdout
    if not hasattr(fd, "isatty") or not fd.isatty():
        return (0, 0)

    # Do not use curses.tigetnum("lines") or curses.tigetnum("cols") because it
    # returns stale values after terminal resize. Do not use curses.initscr().getmaxyx()
    # since that has unwanted side-effects, requiring a call to `stty sane` to restore a
    # sane state.
    if not sys.stdin.isatty():
        # Use curses if stdin is not a tty since stty fails in that case.
        try:
            import curses

            try:
                curses.setupterm(term=os.environ.get("TERM", "unknown"), fd=fd.fileno())
                return curses.tigetnum("lines"), curses.tigetnum("cols")
            except curses.error:
                pass
        except ImportError:
            pass

    try:
        proc = subprocess.Popen(["stty", "size"], stdout=subprocess.PIPE, stderr=fd)
    except OSError as e:
        if e.errno != errno.ENOENT:
            raise
        # stty command not found
        return (0, 0)

    out = _unicode_decode(proc.communicate()[0])
    if proc.wait() == os.EX_OK:
        out = out.split()
        if len(out) == 2:
            try:
                val = (int(out[0]), int(out[1]))
            except ValueError:
                pass
            else:
                if val[0] >= 0 and val[1] >= 0:
                    return val
    return (0, 0)


def set_term_size(lines: int, columns: int, fd: int) -> Optional[asyncio.Future]:
    """
    Set the number of lines and columns for the tty that is connected to fd.
    For portability, this simply calls `stty rows $lines columns $columns`.

    If spawn succeeds and the event loop is running then an instance of
    asyncio.Future is returned and the caller should wait for it in order
    to prevent possible error messages like this:

    [ERROR] Task was destroyed but it is pending!
    """

    cmd = ["stty", "rows", str(lines), "columns", str(columns)]
    try:
        proc = spawn(cmd, env=os.environ, fd_pipes={0: fd}, returnproc=True)
    except CommandNotFound:
        writemsg(_("portage: stty: command not found\n"), noiselevel=-1)
    else:
        loop = asyncio.get_event_loop()
        if loop.is_running():
            return asyncio.ensure_future(proc.wait(), loop=loop)
        else:
            loop.run_until_complete(proc.wait())


class EOutput:
    """
    Performs fancy terminal formatting for status and informational messages.

    The provided methods produce identical terminal output to the eponymous
    functions in the shell script C{/sbin/functions.sh} and also accept
    identical parameters.

    This is not currently a drop-in replacement however, as the output-related
    functions in C{/sbin/functions.sh} are oriented for use mainly by system
    init scripts and ebuilds and their output can be customized via certain
    C{RC_*} environment variables (see C{/etc/conf.d/rc}). B{EOutput} is not
    customizable in this manner since it's intended for more general uses.
    Likewise, no logging is provided.

    @ivar quiet: Specifies if output should be silenced.
    @type quiet: BooleanType
    @ivar term_columns: Width of terminal in characters. Defaults to the value
            specified by the shell's C{COLUMNS} variable, else to the queried tty
            size, else to C{80}.
    @type term_columns: IntType
    """

    def __init__(self, quiet=False):
        self.__last_e_cmd = ""
        self.__last_e_len = 0
        self.quiet = quiet
        lines, columns = get_term_size()
        if columns <= 0:
            columns = 80
        self.term_columns = columns
        sys.stdout.flush()
        sys.stderr.flush()

    def _write(self, f, s):
        # avoid potential UnicodeEncodeError
        writemsg(s, noiselevel=-1, fd=f)

    def __eend(self, caller, errno, msg):
        if errno == 0:
            status_brackets = (
                colorize("BRACKET", "[ ")
                + colorize("GOOD", "ok")
                + colorize("BRACKET", " ]")
            )
        else:
            status_brackets = (
                colorize("BRACKET", "[ ")
                + colorize("BAD", "!!")
                + colorize("BRACKET", " ]")
            )
            if msg:
                if caller == "eend":
                    self.eerror(msg[0])
                elif caller == "ewend":
                    self.ewarn(msg[0])
        if self.__last_e_cmd != "ebegin":
            self.__last_e_len = 0
        if not self.quiet:
            out = sys.stdout
            self._write(
                out,
                "%*s%s\n"
                % ((self.term_columns - self.__last_e_len - 7), "", status_brackets),
            )

    def ebegin(self, msg):
        """
        Shows a message indicating the start of a process.

        @param msg: A very brief (shorter than one line) description of the
                starting process.
        @type msg: StringType
        """
        msg += " ..."
        if not self.quiet:
            self.einfon(msg)
        self.__last_e_len = len(msg) + 3
        self.__last_e_cmd = "ebegin"

    def eend(self, errno, *msg):
        """
        Indicates the completion of a process, optionally displaying a message
        via L{eerror} if the process's exit status isn't C{0}.

        @param errno: A standard UNIX C{errno} code returned by processes upon
                exit.
        @type errno: IntType
        @param msg: I{(optional)} An error message, typically a standard UNIX
                error string corresponding to C{errno}.
        @type msg: StringType
        """
        if not self.quiet:
            self.__eend("eend", errno, msg)
        self.__last_e_cmd = "eend"

    def eerror(self, msg):
        """
        Shows an error message.

        @param msg: A very brief (shorter than one line) error message.
        @type msg: StringType
        """
        out = sys.stderr
        if not self.quiet:
            if self.__last_e_cmd == "ebegin":
                self._write(out, "\n")
            self._write(out, colorize("ERR", " * ") + msg + "\n")
        self.__last_e_cmd = "eerror"

    def einfo(self, msg):
        """
        Shows an informative message terminated with a newline.

        @param msg: A very brief (shorter than one line) informative message.
        @type msg: StringType
        """
        out = sys.stdout
        if not self.quiet:
            if self.__last_e_cmd == "ebegin":
                self._write(out, "\n")
            self._write(out, colorize("INFO", " * ") + msg + "\n")
        self.__last_e_cmd = "einfo"

    def einfon(self, msg):
        """
        Shows an informative message terminated without a newline.

        @param msg: A very brief (shorter than one line) informative message.
        @type msg: StringType
        """
        out = sys.stdout
        if not self.quiet:
            if self.__last_e_cmd == "ebegin":
                self._write(out, "\n")
            self._write(out, colorize("INFO", " * ") + msg)
        self.__last_e_cmd = "einfon"

    def eqawarn(self, msg):
        """
        Shows a QA warning.

        @param msg: A very brief (shorter than one line) warning message.
        @type msg: StringType
        """
        out = sys.stderr
        if not self.quiet:
            if self.__last_e_cmd == "ebegin":
                self._write(out, "\n")
            self._write(out, colorize("QAWARN", " * ") + msg + "\n")
        self.__last_e_cmd = "ewarn"

    def elog(self, msg):
        """
        Shows a logged informative message terminated with a newline.

        @param msg: A very brief (shorter than one line) informative message.
        @type msg: StringType
        """
        out = sys.stdout
        if not self.quiet:
            if self.__last_e_cmd == "ebegin":
                self._write(out, "\n")
            self._write(out, colorize("LOG", " * ") + msg + "\n")
        self.__last_e_cmd = "elog"

    def ewarn(self, msg):
        """
        Shows a warning message.

        @param msg: A very brief (shorter than one line) warning message.
        @type msg: StringType
        """
        out = sys.stderr
        if not self.quiet:
            if self.__last_e_cmd == "ebegin":
                self._write(out, "\n")
            self._write(out, colorize("WARN", " * ") + msg + "\n")
        self.__last_e_cmd = "ewarn"

    def ewend(self, errno, *msg):
        """
        Indicates the completion of a process, optionally displaying a message
        via L{ewarn} if the process's exit status isn't C{0}.

        @param errno: A standard UNIX C{errno} code returned by processes upon
                exit.
        @type errno: IntType
        @param msg: I{(optional)} A warning message, typically a standard UNIX
                error string corresponding to C{errno}.
        @type msg: StringType
        """
        if not self.quiet:
            self.__eend("ewend", errno, msg)
        self.__last_e_cmd = "ewend"


class ProgressBar:
    """The interface is copied from the ProgressBar class from the EasyDialogs
    module (which is Mac only)."""

    def __init__(self, title=None, maxval=0, label=None, max_desc_length=25):
        self._title = title or ""
        self._maxval = maxval
        self._label = label or ""
        self._curval = 0
        self._desc = ""
        self._desc_max_length = max_desc_length
        self._set_desc()

    @property
    def curval(self):
        """
        The current value (of type integer or long integer) of the progress
        bar. The normal access methods coerce curval between 0 and maxval. This
        attribute should not be altered directly.
        """
        return self._curval

    @property
    def maxval(self):
        """
        The maximum value (of type integer or long integer) of the progress
        bar; the progress bar (thermometer style) is full when curval equals
        maxval. If maxval is 0, the bar will be indeterminate (barber-pole).
        This attribute should not be altered directly.
        """
        return self._maxval

    def title(self, newstr):
        """Sets the text in the title bar of the progress dialog to newstr."""
        self._title = newstr
        self._set_desc()

    def label(self, newstr):
        """Sets the text in the progress box of the progress dialog to newstr."""
        self._label = newstr
        self._set_desc()

    def _set_desc(self):
        self._desc = "{}{}".format(
            f"{self._title}: " if self._title else "",
            f"{self._label}" if self._label else "",
        )
        if len(self._desc) > self._desc_max_length:  # truncate if too long
            self._desc = f"{self._desc[:self._desc_max_length - 3]}..."
        if len(self._desc):
            self._desc = self._desc.ljust(self._desc_max_length)

    def set(self, value, maxval=None):
        """
        Sets the progress bar's curval to value, and also maxval to max if the
        latter is provided. value is first coerced between 0 and maxval. The
        thermometer bar is updated to reflect the changes, including a change
        from indeterminate to determinate or vice versa.
        """
        if maxval is not None:
            self._maxval = maxval
        if value < 0:
            value = 0
        elif value > self._maxval:
            value = self._maxval
        self._curval = value

    def inc(self, n=1):
        """Increments the progress bar's curval by n, or by 1 if n is not
        provided. (Note that n may be negative, in which case the effect is a
        decrement.) The progress bar is updated to reflect the change. If the
        bar is indeterminate, this causes one ``spin'' of the barber pole. The
        resulting curval is coerced between 0 and maxval if incrementing causes
        it to fall outside this range.
        """
        self.set(self._curval + n)


class TermProgressBar(ProgressBar):
    """A tty progress bar similar to wget's."""

    def __init__(self, fd=sys.stdout, **kwargs):
        ProgressBar.__init__(self, **kwargs)
        lines, self.term_columns = get_term_size(fd)
        self.file = fd
        self._min_columns = 11
        self._max_columns = 80
        # for indeterminate mode, ranges from 0.0 to 1.0
        self._position = 0.0

    def set(self, value, maxval=None):
        ProgressBar.set(self, value, maxval=maxval)
        self._display_image(self._create_image())

    def _display_image(self, image):
        self.file.write("\r")
        self.file.write(image)
        self.file.flush()

    def _create_image(self):
        cols = self.term_columns
        if cols > self._max_columns:
            cols = self._max_columns
        min_columns = self._min_columns
        curval = self._curval
        maxval = self._maxval
        position = self._position
        percentage_str_width = 5
        square_brackets_width = 2
        if cols < percentage_str_width:
            return ""
        bar_space = cols - percentage_str_width - square_brackets_width - 1
        if self._desc:
            bar_space -= self._desc_max_length

        if maxval == 0:
            max_bar_width = bar_space - 3
            _percent = "".ljust(percentage_str_width)
            if cols < min_columns:
                return ""
            if position <= 0.5:
                offset = 2 * position
            else:
                offset = 2 * (1 - position)
            delta = 0.5 / max_bar_width
            position += delta
            if position >= 1.0:
                position = 0.0
            # make sure it touches the ends
            if 1.0 - position < delta:
                position = 1.0
            if position < 0.5 and 0.5 - position < delta:
                position = 0.5
            self._position = position
            bar_width = int(offset * max_bar_width)
            image = "{}{}{}".format(
                self._desc,
                _percent,
                "["
                + (bar_width * " ")
                + "<=>"
                + ((max_bar_width - bar_width) * " ")
                + "]",
            )
            return image

        percentage = 100 * curval // maxval
        max_bar_width = bar_space - 1
        _percent = ("%d%% " % percentage).rjust(percentage_str_width)
        image = f"{self._desc}{_percent}"

        if cols < min_columns:
            return image
        offset = curval / maxval
        bar_width = int(offset * max_bar_width)
        image = (
            image
            + "["
            + (bar_width * "=")
            + ">"
            + ((max_bar_width - bar_width) * " ")
            + "]"
        )
        return image


_color_map_loaded = False


def _init(config_root="/"):
    """
    Load color.map from the given config_root. This is called automatically
    on first access of the codes or _styles attributes (unless it has already
    been called for some other reason).
    """

    global _color_map_loaded, codes, _styles
    if _color_map_loaded:
        return

    _color_map_loaded = True
    codes = object.__getattribute__(codes, "_attr")
    _styles = object.__getattribute__(_styles, "_attr")

    for k, v in codes.items():
        codes[k] = _unicode_decode(v)

    for k, v in _styles.items():
        _styles[k] = _unicode_decode(v)

    try:
        _parse_color_map(
            config_root=config_root,
            onerror=lambda e: writemsg(f"{str(e)}\n", noiselevel=-1),
        )
    except FileNotFound:
        pass
    except PermissionDenied as e:
        writemsg(_("Permission denied: '%s'\n") % str(e), noiselevel=-1)
        del e
    except PortageException as e:
        writemsg(f"{str(e)}\n", noiselevel=-1)
        del e


class _LazyInitColorMap(portage.proxy.objectproxy.ObjectProxy):
    __slots__ = ("_attr",)

    def __init__(self, attr):
        portage.proxy.objectproxy.ObjectProxy.__init__(self)
        object.__setattr__(self, "_attr", attr)

    def _get_target(self):
        _init()
        return object.__getattribute__(self, "_attr")


codes = _LazyInitColorMap(codes)
_styles = _LazyInitColorMap(_styles)
