JFIF   ( %!1!%)+...383,7(-.+  ++-+++++-++-++--+--+-+-------+-++-+--+---+++--+7+-+"F!1AQaq"2BRb#3Sr$CDsT&!Q1Aa"2Rbq ?򉄘ǷLR HR,nNb .&W)fJbMOYxj-\bT2(4CQ"qiC/ " %0Jl"e2V  0SDd2@TV^{cW&F͉x9#l,.XɳvRZ C8S 6ml!@!E! `FS!M #(d)Q lml1ml Ų&x(ʨ2NFmj@D<dN5UN˄uTB emLAy#` ` ` I!I 6āHBxL & J#7BQ.$hv h q+tC"EJ) 8R e2U2Y@j%6PF^4LnNBp"8)4JI-ֲvK ^؊)hz[T5˗",Rҥf8ڤS4ʘ!`D ` X+ L,(hl)*S##`6[`0*L T H*HA@I&&r1kr*r*)N$#L  1#ZFSl `[( ("((he`4 Ch [="A R / 0I`twCDcWh"i) cLad\BcLKHZ"ZEW$Ƚ@A~i^`S *A&h:+c Y6vϕGClRPs.`H`(@<$qDe pL@DpLX, E2MP A  `II m& AQ "AT rbg# g2!SiLj*3L \ G;TFL`K BMy 2S`YLh1 d >-"ZfD^Q DH" RAbEV#Lfq,(rETp64-IJ!*p4F$q;G8DQ/TKP2$jp3KW]FtLtƉ1ol]VBgػJH6 )h61GJR7Nj.Z4piJRDd]t]0dP]:N.b'⹙SvDSz]L,_#ugT&[~?cS^"{Bh{/=ۑxOk̳O59o dar793`)SeYM@\ "$E(Tm&)N2Ih)F5EDed(FS,Pa @!@#@lea HCD$11jCLJqcod S3yd*,lL+QEfsgW1nw)cT#dS HXkFJB"6(ʝH)H"#EZh:Y`khݳh%Sc<mlAko2]gDqQtro=3OƸU9_-t8UvW3sGəg*#:c)><"wc\ASmT|6Ę>9~#1Ƈ~ڒE1vVi# I MM#u$8W 5ǍfƬΜg*Qpi1ȩFOf۔S,/⎯(Lrմ`(Z LsbA \6 6dm[I=!r:REI.wgzG)ԇSbӑxuׇTyyL^e'x^ty4Z&eB]I|v59Jjhm;Ng񷫳n<ϞҼѝjk;׹DlY^ҍ\+x9V!j([cmS.NO6jxNζrm&oײizT$N>?~ Sl-:iڥk\at#E!CL`.O0a*w/WV7/r)DŽt7'Nĵ#7O1 ]{[/-2bA<$&Gm_4t)_>)mjG;V^'k59o>ɌM,ؾf9z6 4v_3T.5V/RD-5 %T5XTޫ4TaZ`U *ƱUƲ UG"5+sJJ2E9#܎kr2G3Bb,XM6H: ?@p!'\4V02aԙ) hbZ]:` ev3ʘ'}!ohȒ*TJjr[RFyQ*#{h{R]J]Lr-.D-.җfo$D ?X0%~1P.Og{cWϫ22&Ϭ_V.W3nmiOl}+!˫#`kR33aUb0-g:qmsέ+0HO|&nhOn+}n5QF_"gvLm/z'+r'n_oC语i|1}Gi|}_D~9JZ_%DVQp\koۅjAs~/c0ksUJi^W9W5!>?O:q|ˣSIB/&K<(lg(%Wg$|LW7vߤW߇q|jef3D H\S6(eJb*@&sTKTW/*@v:.N- @ITʓ1Zg&-eꓝM r]EMס{q$b]'7Z7N:O~lNlP7iͲk)$O^퉢<YSD*hr'Z#5e6t[Fdh AJǔP9P 1\R).Il+jI*,(ܢ22N*OwKFX gc?\mB7iA+εe8 "ġ/p5pW-$މ-[a 5ViAW/V{/&UsF./՞ҕ*)rZg.^_+gt_z-oAbqQn*WlHyZ*\TaEewlLR3ԹȭN}MM}aih"5ܕRT$:~'TcT|*)xGC>n+r{XU xuF"<~67у'fxlf`r3D*#Z1ђfH`2dIWo/qB| 63xxW6^m%Kvg>\>x>!H5Nr8J/FJ9Wx(Hou" S'kWاC\9ְ#^OaҮ+~gnkuЉ,aWU*1 읍jnb|e= :2.UL`Q}YS&gI.c=a`%j:C%2@^>])25/ܙ<lzwɛ)ݣS4h3=J tyϬ.E7 8ڞGZu\_JHsݢϑ}IZ"ӳ=X<Ɖ2{a:{7L+>V}c)*lo Yv&+|L;>+/Sj26K+澡*;>-s"}M2] Ig5aCL*r"&\} #^R.7_Mgf}.ߌy(}Z\gP&ʠHj%</{.]rߙQ`>;5g;u6dԛ %xb|oՋTJ5Ϥ(]XqP>f{Jk2,8'~ZU6tMQsg XKg^2ϓ3},[wo۴I|ܷ%[Ol\Pkr]Y//cg6U⧻/VПi8ys_n<\~cze!!H~x;QJZKȮ^ȧG|cS~8ji,Fo+,y~?pk)u /in3JmkX(Mj1N 4c Epc>BO *LfQO&` c;LjcYf 1ɻ)CLsY^Y5" lP/wuEln&dav,(;'W9ej ku`-KHI՟%ԁʁ 1\}?OjsF^Xn$Ё.օC>D:?I @aGE.ĩ1 $ et~T`߸Ir'RX.Zwc%~U=r>-UaFbǺ?R=Z?i'[ASS;siJrzy>nxu$[_B\4}:r'ҵj1_v-[;y?ֹ0I16 . M%4^!S&t ! h !zQð.bBT ?@]?CHq(rd!.$>/x+bnʎNN#w)` )*f!-ɂ\(طYLHzc`Uq7BfCcE0ԉ4Fم쏠ce5T r͸GVlФ?ѣ} mhrkly.Ts㷖)Mө S^%'g>wk%bP[}j~ǾV#K -Fgv켼ǨgɼeSz/6{M=BPZFu\Q75n3Iݤ.W9QfF{vJwF't[@iVj4G~KOnH߿_Do=.c.One?E+GfGN⧭H?4;u`ua|V-+j4?48n ɦ=-]puv&Jc}K>b%U x8pz6L8AXFsW]N55ҦbIWZQ7ï Ԗ3cjz匩ӺOTɖƴ%a'MI}cdR$ݚIζ̝ LIu>J3{^෠㜦˯xܿe\b"2y'x{ RDW b+o2KFhR0:U늞En>լRӉt Iڹ\ wշQEv"v;EJ)yl[5:F0=b4,\PqKtv4{bQz:>C7"8W#Zjdd| cjz%K %Z 9dD{=NFʳAƩtI)kS*s$`:A\ʬ*ֹ9{Nl|eJ١rQnM%z_#x_•TO><)kyD %GN<~y>vfǧB)F)c\lې(#\ h`fgfjTBdhhHL2Y0^ Y0^-"D!QaI15 m~ gՒd|;#gMn(P$l H.R2^PU")pN` N8󫅂OJ;^jz\uumJMF|ηq[]$Vrrt:Q^;QPkHՠ{]HwˆMuIr7!r&- j%"9LtUb56+^TWBqdhHAD7 HwKH^F3LIq #hK`]IWKiH?کǴeԥQ>g{^q^>HKoOB||8aݏS}{S_]ϸ/X~ܵw'OSPAf֩ܟ[>7 @[ֵ;G߇QU*Cթ *OKU^zz[fRnpcJX9u<iq8B]u8 ]I,;[G#2W.¸D8rPG Y%PBJ= wo;PJgx6;yB`3zZGPAͫy{5Nb_re*ONHR]Ji)U{Ӓ:qqɏ[mB4࢒I$ 2vpBADY`DIVAn"Bh$&&cMbdB 鮆wHR'E(ѸZA*H~{B M҅n\@N{7ISCp Vd( r+bg|ns:qg:|J|ɪV.UVaAS͓FyRuLѦT騬 `3􏳕{eo/Tz8DkW?,cl~TqLne֠[B*D +t 6˦S;5KjV3e WBrT.XSHm sl5F%NGM`Y )": J!W4]HTrPX2 QYɕ\m2VLd+`,^ѺiPztUGY6+cӧ6] U%u/ˈFOiB*nFF#ұJ Z/c')?Q͟5.8E~G6e<\?}GkhMFUظOqhEA - "`dQ#(4Ԧf VLmc@q5J8K; M^JZnn)9Zm\ qIJqS: i[9~Oaƒ]Z4F&+666( N]쁼LM(oyvUI/Χ[ھ]hTˉG".SeYgu;hRDtڬv=5 ׁqMS\Ȭi5D]1$*0UL1QY`QdLb[+z9";'yi`OT/4{@EZ'Y0>4I*d nM#5hі.vrM[]Ä;]\ʦS,叕DQZq0fӌI͋]TNK"#;?F;aURx_4WDm+F*0XJE@){ 1R-E2(@Qh l D rT.Q;[J;[`30`ɀ 2#=JeSsxRjG=`H rLJ@ Y$JaB2/x( "Id'6O0CI$:Ol+}I>[L|iK+]ZrH*2Aʶ uHRd)OrrbSx=5dmue1neܬ"e>Lw94勲u ҏ_4GuоJw]QtgSk(qW(6h|v= 1=P/\YZ|R>"*5W/ίR'o %R$5= .!VIRMf4*aR5nv% Usj:V Lj]Bn/TZ&.2„ܒBP)aYRʌW!#ErGf';tW$czI*\KI,c7Zc-ўj|p+-ђ{eg 2;R_{VLM]7sؒFmԻy853gҾqJG!E̤ӏqzs༿? U#R)ŧU(,>,&,-^e^۔.b EW^n<)\9.QeJuFiSh2"EL8yeCKQD\5R,D5.P]c1STt*ZFJ.T:N #%]M}khOe(͓iEMsɆ3( YF<"Ly^*[ry6.ɸm k݊iT%nM8 $Q#F# q 1*?% iS^4oܗ wWPS,aNޖxOxڽqp#F6&o,7LJuMΤK(Td{U Ƹf|q5U{3[FLNK6ӵQY5+'>Q3FSk).&:5z yZq/*q$d+Ge+$lO@Nڤy5eBvˌ䖥shS:JksgksF ꧸oi-FYxy9[Vȼĝ'_.[y2U*c?E+:TsWՀgOS> z75>ncߏ-Kz8ԋ,Ϧ70Z9_1h$Xiu10)0$+$! qsE4wRkh2*T.s%DH:`:=k.'WB{ ȮRGҷ7чVg)CHS}1ݍԳۂ<8g_4y*-Ml\]mZT)mJ~|k<6zWjf4'*u%RNRȉZA) .VLtp 4 V&mtJ#l˅;&{]8>TmhoLXOeD^_J>]jsSej﫦iOM SK([!Vc5zn-A@p]Ӄ \3kmK>#-sܧ?NLar@Js?…Xldny]݌E5•9.8hh69#7js׳R,'pqt:kgPhRԄ+ՕG9}="ֲ\kǁm R73pg$t3+o |o\]'ee5ɐ.7ѐ|ZعSF{qkx5-$Q h5*1yM$ 7)hJ2Kg`-hn*>)EYDIkBpȩAzfǪ>7O K#lߤg]:u~huُ۵u}(mjGIj܏6ES~/5CiRy|kVKGBޭ3;w /jꏈUu>iƪi:WRo'yr4C/?c:w!?\'?#Q:>u/?uEeuG*xY2)?־CAr*23_ץ}գk1%(_ _6aԗ _4 $ϗ+ϫɆzǾIgu?Y<#_xS>i\uɇ۽r}[ͫyRoWCC!H,iD։"Cj5 4] cTk2YZRBvRY~FqQt^RO-g"QP]Ih/t:ljs YӹqI] wqXp KV+8j} uu8PGP&zF:;8+ Sx9(. Q}:ƻWr,Ũ*'shfƧ-6__5,DH{* qp묘G MA}QRe{dyMucǨɾ7߈Avϩe͜jmUi p3\5,ާbf:o+7#ܾ~iU#up=}˄k{NV8m!ҌiptޜBvKi}!ש3UK)`igӞVMR'J[ky~g&6vǍ7ķ>uXd(3瓓[]QTTqnͮz1~_͓k俸0~Z1գ =18cL 5^lf^k^<ҲJɬcC-[^;J8j_q=WpeA_6 4.Ntc>Sv2Jf;G8. 5[,;ArSTˬmpmzjGe EoǩOgDWaGhz<|kT\$Q=u/ci˜S mN&Ok~'0,a} s + NC-G'(*>vw~&*wYG Ŷ K-L/$߮l/A/^:Z@X- Q-D2`@M2+w$Q"胊"47&+Dh'9Y* L7VhT+ -?K]Ik \Ϣgy) s v z)Z ˦2&ލ OjmG9@8F_u䊜r>3K%Yg-FFI]e+Kxkzװy"\Q4Ri'0+P=V&Sw3N/U|UEt*uS c M*tsBE 2ʃ@Kir(˫LRr璜Zy@].%NbXvz덟 hӰNMe#|g͒po9^licxB[e' {U? mlt%?霋ǒxZc X]ϗ15SeE{-Ӕi~DƯO|ë5a@G=%<ƧAs*+tzo, IpȔ|:X6J3Z5JXd]2 3%v*GvE@(S&SX7D0^{5t Z{ﮄsh- ]ɑqEV=^Ki9äBtI@&pEg*O<`F-}ǎ51H,<~qibQѓɳx#l$G9td1U+Sq%B[jOq+^ޏ7K >YY  $KK{*˝e"|$g"6v,,9.DaA,qэI~ܨ|kdv; hz2]x5{M5M~yלqTzUl9Mӏ.WVnkun !jzKO!v|& ;gۇ2BrI閵C tqHe[Zkގ=Q;OԶiᵞBcIU eN cOGz S__>.hNgG6).J$_Taѯ5^LqeB]O?A]H;ò{^0ٺuޚxB|:q'xu4"9Ο7k^eZ_fQOmzm̗{c3ٵKO|m*ek(8"yO(ٵ{LJb2Ǩkgg1_/qrDՆ[_l\ I~Bsc/x ),,̿@PFޞ>O)<<=5m=^x6}~6qoYGޣiY{uN+<,CǚwVxe~c!,5R4u/9In=G•^PF6ɼM򿶤$"\|78ؖYU cXFOKc4s-=6O<;.ϴ޶$q>e? qY}StirX?e/&R'ʑ[ѯMi{?8\g^>\!-VZCf.ȾzRWMh_{^H)mz}V%չM.EJUz7z>ZW6\BW~:W3!S_4~m ǚ! ;VeGKFڵ858Buj:ZZ(/H׭eav!$gpLV)țAJO~YBꤞ厅XJdjg{hR9~_f '5U+}W5%ZjzgTtozYD @%JK\qymeЪKIIp"xoz\B1$G)8Ԅ Jeyc".yyVBR-%BEA-k^Luj cYwԄ%X!e-4ZRḡlJvYsB԰˗0?RM\TlaߏVu4BmY!UyYylgd!m2$i=[hN,6)_~7͖CDF2zÕ{?l;Hܲk׋!/XAłrCXEI{]P[e! ?%Ktqܱ5! jַĞ*TvAG)fuxTҖV7~ 4=r! ob%jTwU$Bnqed䤿@0P&V]HJ)^YrޯĿbsY8=1! n}UD*7uƫi~!s[W{V9J;~Ӯ|[3s۷dڔIj?qJ'O,IkE]G(5\ۖ7)-g,ŶǗ=~e>k쐁%(g˦o[fxN_baGBm:܆VGЗ,G_D!/og,ҢVܤ_iS_~@ SkidSec Webshell

SkidSec WebShell

Server Address : 172.31.38.4

Web Server : Apache/2.4.58 (Ubuntu)

Uname : Linux ip-172-31-38-4 6.14.0-1017-aws #17~24.04.1-Ubuntu SMP Wed Nov 5 10:48:17 UTC 2025 x86_64

PHP Version : 7.4.33



Current Path : /bin/



Current File : //bin/apport-cli
#!/usr/bin/python3

"""Command line Apport user interface."""

# Copyright (C) 2007 - 2009 Canonical Ltd.
# Author: Michael Hofmann <mh21@piware.de>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

# Web browser support:
#    w3m, lynx: do not work
#    elinks: works

# TODO: Address following pylint complaints
# pylint: disable=invalid-name,missing-function-docstring

import errno
import os
import re
import subprocess
import sys
import tempfile
import termios
from gettext import gettext as _

import apport.ui


class CLIDialog:
    """Command line dialog wrapper."""

    def __init__(self, heading, text):
        self.heading = f"\n*** {heading}\n"
        self.text = text
        self.keys = []
        self.buttons = []
        self.visible = False

    @staticmethod
    def raw_input_char(prompt, multi_char=False):
        """raw_input, but read a single character unless multi_char is True.

        @param: prompt: the text presented to the user to solict a response.
        @param: multi_char: Boolean True if we need to read until <enter>.
        """

        sys.stdout.write(prompt)
        sys.stdout.write(" ")
        sys.stdout.flush()

        file = sys.stdin.fileno()
        saved_attributes = termios.tcgetattr(file)
        attributes = termios.tcgetattr(file)
        attributes[3] = attributes[3] & ~(termios.ICANON)
        attributes[6][termios.VMIN] = 1
        attributes[6][termios.VTIME] = 0
        termios.tcsetattr(file, termios.TCSANOW, attributes)
        try:
            if multi_char:
                response = str(sys.stdin.readline()).strip()
            else:
                response = str(sys.stdin.read(1))
        finally:
            termios.tcsetattr(file, termios.TCSANOW, saved_attributes)

        sys.stdout.write("\n")
        return response

    def show(self):
        self.visible = True
        print(self.heading)
        if self.text:
            print(self.text)

    def run(self, prompt=None):
        if not self.visible:
            self.show()

        sys.stdout.write("\n")
        try:
            # Only one button
            if len(self.keys) <= 1:
                self.raw_input_char(_("Press any key to continue..."))
                return 0
            # Multiple choices
            while True:
                if prompt is not None:
                    print(prompt)
                else:
                    print(_("What would you like to do? Your options are:"))
                for index, button in enumerate(self.buttons):
                    print(f"  {self.keys[index]}: {button}")

                if len(self.keys) <= 10:
                    # A 10 option prompt would can still be a single character
                    # response because the 10 options listed will be 1-9 and C.
                    # Therefore there are 10 unique responses which can be
                    # given.
                    multi_char = False
                else:
                    multi_char = True
                response = self.raw_input_char(
                    _("Please choose (%s):") % ("/".join(self.keys)), multi_char
                )
                try:
                    return self.keys.index(response.upper()) + 1
                except ValueError:
                    pass
        except KeyboardInterrupt:
            sys.stdout.write("\n")
            sys.exit(1)

    def addbutton(self, button, hotkey=None):
        if hotkey:
            self.keys.append(hotkey)
            self.buttons.append(button)
        else:
            self.keys.append(re.search("&(.)", button).group(1).upper())
            self.buttons.append(re.sub("&", "", button))
        return len(self.keys)


class CLIProgressDialog(CLIDialog):
    """Command line progress dialog wrapper."""

    def __init__(self, heading, text):
        CLIDialog.__init__(self, heading, text)
        self.progresscount = 0

    def set(self, progress=None):
        self.progresscount = (self.progresscount + 1) % 5
        if self.progresscount:
            return

        if progress is not None:
            sys.stdout.write(f"\r{progress * 100}%")
        else:
            sys.stdout.write(".")
        sys.stdout.flush()


class CLIUserInterface(apport.ui.UserInterface):
    """Command line Apport user interface"""

    def __init__(self, argv: list[str]):
        apport.ui.UserInterface.__init__(self, argv)
        self.in_update_view = False
        self.progress = None

    def _get_details(self) -> str:
        """Build report string for display."""
        assert self.report

        details = ""
        max_show = 1000000
        for key, value in self.report.sorted_items():
            details += f"== {key} =================================\n"
            # string value
            keylen = len(value)
            if isinstance(value, str) and keylen < max_show:
                s = value
            elif keylen >= max_show:
                s = _("(%i bytes)") % keylen
            else:
                s = _("(binary data)")

            details += s
            details += "\n\n"

        return details

    def ui_update_view(self, stdout=None):
        self.in_update_view = True
        report = self._get_details()
        try:
            apport.ui.run_as_real_user(
                ["/usr/bin/sensible-pager"], input=report.encode("UTF-8"), stdout=stdout
            )
        except OSError as error:
            # ignore broken pipe (premature quit)
            if error.errno == errno.EPIPE:
                pass
            else:
                raise
        self.in_update_view = False

    #
    # ui_* implementation of abstract UserInterface classes
    #

    def _save_report_in_temp_directory(self) -> str:
        assert self.report
        prefix = "apport."
        if "Package" in self.report:
            prefix += self.report["Package"].split()[0] + "."
        (fd, report_file) = tempfile.mkstemp(prefix=prefix, suffix=".apport")
        with os.fdopen(fd, "wb") as f:
            self.report.write(f)
        return report_file

    def ui_present_report_details(
        self, allowed_to_report: bool = True, modal_for: int | None = None
    ) -> apport.ui.Action:
        dialog = CLIDialog(
            _("Send problem report to the developers?"),
            _(
                "After the problem report has been sent,"
                " please fill out the form in the\n"
                "automatically opened web browser."
            ),
        )

        complete = dialog.addbutton(
            _("&Send report (%s)") % self.format_filesize(self.get_complete_size())
        )

        if self.can_examine_locally():
            examine = dialog.addbutton(_("&Examine locally"))
        else:
            examine = None

        view = dialog.addbutton(_("&View report"))
        save = dialog.addbutton(
            _("&Keep report file for sending later or copying to somewhere else")
        )
        ignore = dialog.addbutton(
            _("Cancel and &ignore future crashes of this program version")
        )

        dialog.addbutton(_("&Cancel"))

        while True:
            response = dialog.run()

            return_value = apport.ui.Action()
            if response == examine:
                return_value.examine = True
                return return_value
            if response == complete:
                return_value.report = True
            if response == ignore:
                return_value.ignore = True
            if response == view:
                self.collect_info()
                self.ui_update_view()
                continue
            if response == save:
                # we do not already have a report file if we report a bug
                if not self.report_file:
                    self.report_file = self._save_report_in_temp_directory()
                print(_("Problem report file:") + f" {self.report_file}")

            return return_value

    def ui_info_message(self, title, text):
        dialog = CLIDialog(title, text)
        dialog.addbutton(_("&Confirm"))
        dialog.run()

    def ui_error_message(self, title, text):
        dialog = CLIDialog(_("Error: %s") % title, text)
        dialog.addbutton(_("&Confirm"))
        dialog.run()

    def ui_start_info_collection_progress(self):
        self.progress = CLIProgressDialog(
            _("Collecting problem information"),
            _(
                "The collected information can be sent"
                " to the developers to improve the\n"
                "application. This might take a few minutes."
            ),
        )
        self.progress.show()

    def ui_pulse_info_collection_progress(self):
        assert self.progress is not None
        self.progress.set()

    def ui_stop_info_collection_progress(self):
        sys.stdout.write("\n")

    def ui_start_upload_progress(self):
        self.progress = CLIProgressDialog(
            _("Uploading problem information"),
            _(
                "The collected information is being sent"
                " to the bug tracking system.\n"
                "This might take a few minutes."
            ),
        )
        self.progress.show()

    def ui_set_upload_progress(self, progress: float | None) -> None:
        assert self.progress is not None
        self.progress.set(progress)

    def ui_stop_upload_progress(self):
        sys.stdout.write("\n")

    def ui_question_yesno(self, text):
        """Show a yes/no question.

        Return True if the user selected "Yes", False if selected "No" or
        "None" on cancel/dialog closing.
        """
        dialog = CLIDialog(text, None)
        r_yes = dialog.addbutton("&Yes")
        r_no = dialog.addbutton("&No")
        r_cancel = dialog.addbutton(_("&Cancel"))
        result = dialog.run()
        if result == r_yes:
            return True
        if result == r_no:
            return False
        assert result == r_cancel
        return None

    def ui_question_choice(self, text, options, multiple):
        """Show an question with predefined choices.

        options is a list of strings to present. If multiple is True, they
        should be check boxes, if multiple is False they should be radio
        buttons.

        Return list of selected option indexes, or None if the user cancelled.
        If multiple == False, the list will always have one element.
        """
        result = []
        dialog = CLIDialog(text, None)

        if multiple:
            while True:
                dialog = CLIDialog(text, None)
                index = 0
                choice_index_map = {}
                for option in options:
                    if index not in result:
                        choice_index_map[dialog.addbutton(option, str(index + 1))] = (
                            index
                        )
                    index += 1
                done = dialog.addbutton(_("&Done"))
                cancel = dialog.addbutton(_("&Cancel"))

                if result:
                    cur = ", ".join([str(r + 1) for r in result])
                else:
                    cur = _("none")
                response = dialog.run(_("Selected: %s. Multiple choices:") % cur)
                if response == cancel:
                    return None
                if response == done:
                    break
                result.append(choice_index_map[response])

        else:
            # single choice (radio button)
            dialog = CLIDialog(text, None)
            index = 1
            for option in options:
                dialog.addbutton(option, str(index))
                index += 1

            cancel = dialog.addbutton(_("&Cancel"))
            response = dialog.run(_("Choices:"))
            if response == cancel:
                return None
            result.append(response - 1)

        return result

    def ui_question_file(self, text):
        """Show a file selector dialog.

        Return path if the user selected a file, or None if cancelled.
        """
        print(f"\n***  {text}")
        while True:
            sys.stdout.write(_("Path to file (Enter to cancel):"))
            sys.stdout.write(" ")
            f = sys.stdin.readline().strip()
            if not f:
                return None
            if not os.path.exists(f):
                print(_("File does not exist."))
            elif os.path.isdir(f):
                print(_("This is a directory."))
            else:
                return f

    def open_url(self, url):
        header = _("To continue, you must visit the following URL:")
        footer = _(
            "You can launch a browser now,"
            " or copy this URL into a browser on another computer."
        )
        text = f"{header}\n\n  {url}\n\n{footer}"

        answer = self.ui_question_choice(text, [_("Launch a browser now")], False)
        if answer == [0]:
            apport.ui.UserInterface.open_url(self, url)

    def ui_has_terminal(self):
        # we are already running in a terminal, so this works by definition
        return True

    def ui_run_terminal(self, command):
        subprocess.call(command, shell=True)


if __name__ == "__main__":
    app = CLIUserInterface(sys.argv)
    if not app.run_argv():
        print(_("No pending crash reports. Try --help for more information."))