mirror of https://gitee.com/openkylin/apport.git
651 lines
24 KiB
Python
Executable File
651 lines
24 KiB
Python
Executable File
#!/usr/bin/python3
|
|
|
|
"""GTK Apport user interface."""
|
|
|
|
# Copyright (C) 2007-2016 Canonical Ltd.
|
|
# Author: Martin Pitt <martin.pitt@ubuntu.com>
|
|
#
|
|
# 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.
|
|
|
|
# pylint: disable=invalid-name
|
|
# pylint: enable=invalid-name
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from gettext import gettext as _
|
|
|
|
import apport
|
|
import apport.ui
|
|
|
|
try:
|
|
import gi
|
|
|
|
gi.require_version("Gdk", "3.0") # noqa: E402, pylint: disable=C0413
|
|
gi.require_version("GdkX11", "3.0") # noqa: E402, pylint: disable=C0413
|
|
gi.require_version("Gtk", "3.0") # noqa: E402, pylint: disable=C0413
|
|
gi.require_version("Wnck", "3.0") # noqa: E402, pylint: disable=C0413
|
|
from gi.repository import GdkX11, GLib, Gtk, Wnck
|
|
except (AssertionError, ImportError, RuntimeError) as error:
|
|
# probably distribution upgrade or session just closing down?
|
|
sys.stderr.write(f"Cannot start: {str(error)}\n")
|
|
sys.exit(1)
|
|
|
|
have_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
|
|
|
|
|
def find_xid_for_pid(pid: int) -> int | None:
|
|
"""Return the X11 Window (xid) for the supplied process ID."""
|
|
screen = Wnck.Screen.get_default()
|
|
screen.force_update()
|
|
for window in screen.get_windows():
|
|
if window.get_pid() == pid:
|
|
return window.get_xid()
|
|
return None
|
|
|
|
|
|
class GTKUserInterface(apport.ui.UserInterface):
|
|
# TODO: Address following pylint complaints
|
|
# pylint: disable=invalid-name,missing-function-docstring
|
|
"""GTK UserInterface."""
|
|
|
|
def w(self, widget: str) -> Gtk.Widget:
|
|
"""Shortcut for getting a widget."""
|
|
widget = self.widgets.get_object(widget)
|
|
assert widget is not None
|
|
return widget
|
|
|
|
def __init__(self, argv: list[str]):
|
|
apport.ui.UserInterface.__init__(self, argv)
|
|
|
|
# load UI
|
|
Gtk.Window.set_default_icon_name("apport")
|
|
self.widgets = Gtk.Builder()
|
|
self.widgets.set_translation_domain(self.gettext_domain)
|
|
self.widgets.add_from_file(
|
|
os.path.join(os.path.dirname(argv[0]), "apport-gtk.ui")
|
|
)
|
|
|
|
# connect signal handlers
|
|
assert self.widgets.connect_signals(self) is None
|
|
|
|
# initialize tree model and view
|
|
self.tree_model = self.w("details_treestore")
|
|
|
|
column = Gtk.TreeViewColumn("Report", Gtk.CellRendererText(), text=0)
|
|
self.w("details_treeview").append_column(column)
|
|
self.spinner = self.add_spinner_over_treeview(self.w("details_overlay"))
|
|
|
|
self.argv = argv
|
|
self.md = None
|
|
|
|
self.desktop_info = None
|
|
self.allowed_to_report = True
|
|
self.collect_called = False
|
|
|
|
#
|
|
# ui_* implementation of abstract UserInterface classes
|
|
#
|
|
|
|
@staticmethod
|
|
def add_spinner_over_treeview(overlay):
|
|
"""Reparents a treeview in a GtkOverlay, then layers a GtkSpinner
|
|
centered on top."""
|
|
# TODO handle the expose event of the spinner so that we can draw on
|
|
# the treeview's viewport's window instead.
|
|
spinner = Gtk.Spinner()
|
|
spinner.set_size_request(42, 42)
|
|
align = Gtk.Alignment()
|
|
align.set_valign(Gtk.Align.CENTER)
|
|
align.set_halign(Gtk.Align.CENTER)
|
|
align.add(spinner)
|
|
overlay.add_overlay(align)
|
|
overlay.show()
|
|
align.show()
|
|
spinner.hide()
|
|
return spinner
|
|
|
|
def ui_update_view(self, shown_keys: list[str] | None = None) -> None:
|
|
assert self.report
|
|
|
|
# do nothing if the dialog is already destroyed when the data
|
|
# collection finishes
|
|
if not self.w("details_treeview").get_property("visible"):
|
|
return
|
|
|
|
self.tree_model.clear()
|
|
for key, value in self.report.sorted_items(shown_keys):
|
|
keyiter = self.tree_model.insert_before(None, None)
|
|
self.tree_model.set_value(keyiter, 0, key)
|
|
|
|
valiter = self.tree_model.insert_before(keyiter, None)
|
|
if isinstance(value, str):
|
|
v = value
|
|
if len(v) > 4000:
|
|
v = v[:4000] + "\n[...]"
|
|
self.tree_model.set_value(valiter, 0, v)
|
|
# expand the row if the value has less than 5 lines
|
|
if len(list(filter(lambda c: c == "\n", value))) < 4:
|
|
self.w("details_treeview").expand_row(
|
|
self.tree_model.get_path(keyiter), False
|
|
)
|
|
else:
|
|
self.tree_model.set_value(valiter, 0, _("(binary data)"))
|
|
|
|
def get_system_application_title(self):
|
|
"""Get dialog title for a non-.desktop application.
|
|
|
|
If the system application was started from the console, assume a
|
|
developer who would appreciate the application name having a more
|
|
prominent placement. Otherwise, provide a simple explanation for
|
|
more novice users.
|
|
"""
|
|
assert self.report
|
|
env = self.report.get("ProcEnviron", "")
|
|
from_console = "TERM=" in env and "SHELL=" in env
|
|
|
|
if from_console:
|
|
if "ExecutablePath" in self.report:
|
|
t = _(
|
|
"Sorry, the application %s has stopped unexpectedly."
|
|
) % os.path.basename(self.report["ExecutablePath"])
|
|
else:
|
|
t = _("Sorry, %s has closed unexpectedly.") % self.cur_package
|
|
else:
|
|
if "DistroRelease" not in self.report:
|
|
self.report.add_os_info()
|
|
t = (
|
|
_("Sorry, %s has experienced an internal error.")
|
|
% self.report["DistroRelease"]
|
|
)
|
|
return t
|
|
|
|
def setup_bug_report(self):
|
|
# This is a bug generated through `apport-bug $package`, or
|
|
# `apport-collect $id`.
|
|
assert self.report
|
|
|
|
# avoid collecting information again, in this mode we already have it
|
|
if "DistroRelease" in self.report:
|
|
self.collect_called = True
|
|
self.ui_update_view()
|
|
self.w("title_label").set_label(
|
|
f"<big><b>{_('Send problem report to the developers?')}</b></big>"
|
|
)
|
|
self.w("title_label").show()
|
|
self.w("subtitle_label").hide()
|
|
self.w("ignore_future_problems").hide()
|
|
self.w("show_details").clicked()
|
|
self.w("show_details").hide()
|
|
self.w("dont_send_button").show()
|
|
self.w("continue_button").set_label(_("Send"))
|
|
|
|
def set_modal_for(self, xid):
|
|
gdk_window = self.w("dialog_crash_new")
|
|
gdk_window.realize()
|
|
gdk_window = gdk_window.get_window()
|
|
gdk_display = GdkX11.X11Display.get_default()
|
|
foreign = GdkX11.X11Window.foreign_new_for_display(gdk_display, xid)
|
|
gdk_window.set_transient_for(foreign)
|
|
gdk_window.set_modal_hint(True)
|
|
|
|
def ui_present_report_details(
|
|
self, allowed_to_report: bool = True, modal_for: int | None = None
|
|
) -> apport.ui.Action:
|
|
# TODO: Split into smaller functions/methods
|
|
# pylint: disable=too-many-branches,too-many-locals,too-many-statements
|
|
assert self.report
|
|
icon = None
|
|
self.collect_called = False
|
|
report_type = self.report.get("ProblemType")
|
|
self.w("details_scrolledwindow").hide()
|
|
self.w("show_details").set_label(_("Show Details"))
|
|
self.tree_model.clear()
|
|
|
|
self.allowed_to_report = allowed_to_report
|
|
if self.allowed_to_report:
|
|
self.w("remember_send_report_choice").show()
|
|
self.w("send_problem_notice_label").set_label(
|
|
f"<b>{self.w('send_problem_notice_label').get_label()}</b>"
|
|
)
|
|
self.w("send_problem_notice_label").show()
|
|
self.w("dont_send_button").grab_focus()
|
|
else:
|
|
self.w("dont_send_button").hide()
|
|
self.w("continue_button").set_label(_("Continue"))
|
|
self.w("continue_button").grab_focus()
|
|
|
|
self.w("examine").set_visible(self.can_examine_locally())
|
|
|
|
if modal_for is not None and "DISPLAY" in os.environ:
|
|
xid = find_xid_for_pid(modal_for)
|
|
if xid:
|
|
self.set_modal_for(xid)
|
|
|
|
if report_type == "Hang" and self.offer_restart:
|
|
self.w("ignore_future_problems").set_active(False)
|
|
self.w("ignore_future_problems").hide()
|
|
self.w("relaunch_app").set_active(True)
|
|
self.w("relaunch_app").show()
|
|
self.w("subtitle_label").show()
|
|
self.w("subtitle_label").set_label(
|
|
"You can wait to see if it wakes up, or close or relaunch it."
|
|
)
|
|
self.desktop_info = self.get_desktop_entry()
|
|
if self.desktop_info:
|
|
icon = self.desktop_info.get("icon")
|
|
name = self.desktop_info["name"]
|
|
name = GLib.markup_escape_text(name)
|
|
title = _("The application %s has stopped responding.") % name
|
|
else:
|
|
icon = "distributor-logo"
|
|
name = os.path.basename(self.report["ExecutablePath"])
|
|
title = _('The program "%s" has stopped responding.') % name
|
|
self.w("title_label").set_label(f"<big><b>{title}</b></big>")
|
|
elif not self.report_file or report_type == "Bug":
|
|
self.w("remember_send_report_choice").hide()
|
|
self.w("send_problem_notice_label").hide()
|
|
self.setup_bug_report()
|
|
elif report_type in {"KernelCrash", "KernelOops"}:
|
|
self.w("ignore_future_problems").set_active(False)
|
|
self.w("ignore_future_problems").hide()
|
|
self.w("title_label").set_label(
|
|
f"<big><b>{self.get_system_application_title()}</b></big>"
|
|
)
|
|
self.w("subtitle_label").hide()
|
|
icon = "distributor-logo"
|
|
elif report_type == "Package":
|
|
package = self.report.get("Package")
|
|
if package:
|
|
self.w("subtitle_label").set_label(_("Package: %s") % package)
|
|
self.w("subtitle_label").show()
|
|
else:
|
|
self.w("subtitle_label").hide()
|
|
self.w("ignore_future_problems").hide()
|
|
self.w("title_label").set_label(
|
|
_("Sorry, a problem occurred while installing software.")
|
|
)
|
|
else:
|
|
# Regular crash.
|
|
self.desktop_info = self.get_desktop_entry()
|
|
if self.desktop_info:
|
|
icon = self.desktop_info.get("icon")
|
|
n = self.desktop_info["name"]
|
|
n = GLib.markup_escape_text(n)
|
|
if report_type == "RecoverableProblem":
|
|
t = _("The application %s has experienced an internal error.") % n
|
|
else:
|
|
t = _("The application %s has closed unexpectedly.") % n
|
|
self.w("title_label").set_label(f"<big><b>{t}</b></big>")
|
|
self.w("subtitle_label").hide()
|
|
|
|
pid = apport.ui.get_pid(self.report)
|
|
still_running = pid and apport.ui.still_running(pid)
|
|
if (
|
|
"ProcCmdline" in self.report
|
|
and not still_running
|
|
and self.offer_restart
|
|
):
|
|
self.w("relaunch_app").set_active(True)
|
|
self.w("relaunch_app").show()
|
|
else:
|
|
icon = "distributor-logo"
|
|
if report_type == "RecoverableProblem":
|
|
title_text = (
|
|
_("The application %s has experienced an internal error.")
|
|
% self.cur_package
|
|
)
|
|
else:
|
|
title_text = self.get_system_application_title()
|
|
self.w("title_label").set_label(f"<big><b>{title_text}</b></big>")
|
|
self.w("subtitle_label").show()
|
|
self.w("subtitle_label").set_label(
|
|
_("If you notice further problems, try restarting the computer.")
|
|
)
|
|
self.w("ignore_future_problems").set_label(
|
|
_("Ignore future problems of this type")
|
|
)
|
|
if self.report.get("CrashCounter"):
|
|
self.w("ignore_future_problems").show()
|
|
else:
|
|
self.w("ignore_future_problems").hide()
|
|
|
|
if report_type == "RecoverableProblem":
|
|
body = self.report.get("DialogBody", "")
|
|
if body:
|
|
del self.report["DialogBody"]
|
|
self.w("subtitle_label").show()
|
|
# Set a maximum size for the dialog body, so developers do
|
|
# not try to shove entire log files into this dialog.
|
|
self.w("subtitle_label").set_label(body[:1024])
|
|
|
|
if icon:
|
|
# pylint: disable=import-outside-toplevel
|
|
from gi.repository import GdkPixbuf
|
|
|
|
builtin = Gtk.IconLookupFlags.USE_BUILTIN
|
|
app_icon = self.w("application_icon")
|
|
theme = Gtk.IconTheme.get_default()
|
|
try:
|
|
pb = theme.load_icon(icon, 42, builtin).copy()
|
|
overlay = theme.load_icon("dialog-error", 16, builtin)
|
|
overlay_w = overlay.get_width()
|
|
overlay_h = overlay.get_height()
|
|
off_x = pb.get_width() - overlay_w
|
|
off_y = pb.get_height() - overlay_h
|
|
overlay.composite(
|
|
pb,
|
|
off_x,
|
|
off_y,
|
|
overlay_w,
|
|
overlay_h,
|
|
off_x,
|
|
off_y,
|
|
1,
|
|
1,
|
|
GdkPixbuf.InterpType.BILINEAR,
|
|
255,
|
|
)
|
|
if app_icon.get_parent(): # work around LP#938090
|
|
app_icon.set_from_pixbuf(pb)
|
|
except GLib.GError:
|
|
self.w("application_icon").set_from_icon_name(
|
|
"dialog-error", Gtk.IconSize.DIALOG
|
|
)
|
|
else:
|
|
self.w("application_icon").set_from_icon_name(
|
|
"dialog-error", Gtk.IconSize.DIALOG
|
|
)
|
|
|
|
d = self.w("dialog_crash_new")
|
|
if "DistroRelease" in self.report:
|
|
d.set_title(self.report["DistroRelease"].split()[0])
|
|
d.set_resizable(self.w("details_scrolledwindow").get_property("visible"))
|
|
d.show()
|
|
# don't steal focus when being called without arguments (i. e.
|
|
# automatically launched)
|
|
if len(self.argv) == 1:
|
|
d.set_focus_on_map(False)
|
|
|
|
return_value = apport.ui.Action()
|
|
|
|
def dialog_crash_dismissed(widget):
|
|
self.w("dialog_crash_new").hide()
|
|
if widget is self.w("dialog_crash_new"):
|
|
Gtk.main_quit()
|
|
return
|
|
if widget is self.w("examine"):
|
|
return_value.examine = True
|
|
Gtk.main_quit()
|
|
return
|
|
|
|
# Force close or leave close app are the default actions
|
|
# with no specifier in case of hangs or crash
|
|
if (
|
|
self.w("relaunch_app").get_active()
|
|
and self.desktop_info
|
|
and self.offer_restart
|
|
):
|
|
return_value.restart = True
|
|
|
|
if self.w("ignore_future_problems").get_active():
|
|
return_value.ignore = True
|
|
|
|
return_value.remember = self.w("remember_send_report_choice").get_active()
|
|
|
|
if widget == self.w("continue_button"):
|
|
return_value.report = self.allowed_to_report
|
|
|
|
Gtk.main_quit()
|
|
|
|
self.w("dialog_crash_new").connect("destroy", dialog_crash_dismissed)
|
|
self.w("continue_button").connect("clicked", dialog_crash_dismissed)
|
|
self.w("dont_send_button").connect("clicked", dialog_crash_dismissed)
|
|
self.w("examine").connect("clicked", dialog_crash_dismissed)
|
|
Gtk.main()
|
|
return return_value
|
|
|
|
def _ui_message_dialog(self, title, text, _type, buttons=Gtk.ButtonsType.CLOSE):
|
|
self.md = Gtk.MessageDialog(message_type=_type, buttons=buttons)
|
|
if "http://" in text or "https://" in text:
|
|
self.md.set_markup(text_to_markup(text))
|
|
else:
|
|
# work around gnome #620579
|
|
self.md.set_property("text", text)
|
|
self.md.set_title(title)
|
|
result = self.md.run()
|
|
self.md.hide()
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
self.md = None
|
|
return result
|
|
|
|
def ui_info_message(self, title, text):
|
|
self._ui_message_dialog(title, text, Gtk.MessageType.INFO)
|
|
|
|
def ui_error_message(self, title, text):
|
|
self._ui_message_dialog(title, text, Gtk.MessageType.ERROR)
|
|
|
|
def ui_shutdown(self):
|
|
Gtk.main_quit()
|
|
|
|
def ui_start_upload_progress(self):
|
|
"""Open a window with an definite progress bar, telling the user to
|
|
wait while debug information is being uploaded."""
|
|
|
|
self.w("progressbar_upload").set_fraction(0)
|
|
self.w("window_report_upload").show()
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
|
|
def ui_set_upload_progress(self, progress: float | None) -> None:
|
|
"""Set the progress bar in the debug data upload progress
|
|
window to the given ratio (between 0 and 1, or None for indefinite
|
|
progress).
|
|
|
|
This function is called every 100 ms."""
|
|
|
|
if progress:
|
|
self.w("progressbar_upload").set_fraction(progress)
|
|
else:
|
|
self.w("progressbar_upload").set_pulse_step(0.1)
|
|
self.w("progressbar_upload").pulse()
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
|
|
def ui_stop_upload_progress(self):
|
|
"""Close debug data upload progress window."""
|
|
|
|
self.w("window_report_upload").hide()
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
|
|
def ui_start_info_collection_progress(self):
|
|
# show a spinner if we already have the main window
|
|
if self.w("dialog_crash_new").get_property("visible"):
|
|
self.spinner.show()
|
|
self.spinner.start()
|
|
elif self.crashdb.accepts(self.report):
|
|
# show a progress dialog if our DB accepts the crash
|
|
self.w("progressbar_information_collection").set_fraction(0)
|
|
self.w("window_information_collection").show()
|
|
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
|
|
def ui_pulse_info_collection_progress(self):
|
|
if self.w("window_information_collection").get_property("visible"):
|
|
self.w("progressbar_information_collection").pulse()
|
|
|
|
# for a spinner we just need to handle events
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
|
|
def ui_stop_info_collection_progress(self):
|
|
if self.w("window_information_collection").get_property("visible"):
|
|
self.w("window_information_collection").hide()
|
|
else:
|
|
self.spinner.hide()
|
|
self.spinner.stop()
|
|
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
|
|
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.
|
|
"""
|
|
result = self._ui_message_dialog(
|
|
"", text, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO
|
|
)
|
|
if result == Gtk.ResponseType.YES:
|
|
return True
|
|
if result == Gtk.ResponseType.NO:
|
|
return False
|
|
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.
|
|
"""
|
|
d = self.w("dialog_choice")
|
|
d.set_default_size(400, -1)
|
|
self.w("label_choice_text").set_label(text)
|
|
|
|
# remove previous choices
|
|
for child in self.w("vbox_choices").get_children():
|
|
child.destroy()
|
|
|
|
b = None
|
|
for option in options:
|
|
if multiple:
|
|
b = Gtk.CheckButton.new_with_label(option)
|
|
# use previous radio button as group; work around GNOME#635253
|
|
elif b:
|
|
b = Gtk.RadioButton.new_with_label_from_widget(b, option)
|
|
else:
|
|
b = Gtk.RadioButton.new_with_label([], option)
|
|
self.w("vbox_choices").pack_start(b, True, True, 0)
|
|
self.w("vbox_choices").show_all()
|
|
|
|
result = d.run()
|
|
d.hide()
|
|
if result != Gtk.ResponseType.OK:
|
|
return None
|
|
|
|
index = 0
|
|
result = []
|
|
for c in self.w("vbox_choices").get_children():
|
|
if c.get_active():
|
|
result.append(index)
|
|
index += 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.
|
|
"""
|
|
md = Gtk.FileChooserDialog(
|
|
text,
|
|
parent=self.w("window_information_collection"),
|
|
buttons=(
|
|
Gtk.STOCK_CANCEL,
|
|
Gtk.ResponseType.CANCEL,
|
|
Gtk.STOCK_OPEN,
|
|
Gtk.ResponseType.OK,
|
|
),
|
|
)
|
|
result = md.run()
|
|
md.hide()
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration_do(False)
|
|
if result == Gtk.ResponseType.OK:
|
|
return md.get_filenames()[0]
|
|
return None
|
|
|
|
@staticmethod
|
|
def _get_terminal():
|
|
terminals = [
|
|
"x-terminal-emulator",
|
|
"gnome-terminal",
|
|
"terminator",
|
|
"xfce4-terminal",
|
|
"xterm",
|
|
]
|
|
|
|
for t in terminals:
|
|
program = GLib.find_program_in_path(t)
|
|
if program:
|
|
return program
|
|
return None
|
|
|
|
def ui_has_terminal(self):
|
|
return have_display and self._get_terminal() is not None
|
|
|
|
def ui_run_terminal(self, command):
|
|
program = self._get_terminal()
|
|
assert program is not None
|
|
subprocess.call([program, "-e", command])
|
|
|
|
#
|
|
# Event handlers
|
|
#
|
|
|
|
def on_show_details_clicked(self, widget):
|
|
sw = self.w("details_scrolledwindow")
|
|
if sw.get_property("visible"):
|
|
self.w("dialog_crash_new").set_resizable(False)
|
|
sw.hide()
|
|
widget.set_label(_("Show Details"))
|
|
else:
|
|
self.w("dialog_crash_new").set_resizable(True)
|
|
sw.show()
|
|
widget.set_label(_("Hide Details"))
|
|
if not self.collect_called:
|
|
self.collect_called = True
|
|
self.ui_update_view(["ExecutablePath"])
|
|
GLib.idle_add(
|
|
lambda: self.collect_info(on_finished=self.ui_update_view)
|
|
)
|
|
return True
|
|
|
|
def on_progress_window_close_event(self, widget, event=None):
|
|
# pylint: disable=unused-argument
|
|
self.w("window_information_collection").hide()
|
|
self.w("window_report_upload").hide()
|
|
sys.exit(0)
|
|
|
|
|
|
def text_to_markup(text: str) -> str:
|
|
"""Turn URLs into links"""
|
|
escaped_text = GLib.markup_escape_text(text)
|
|
return re.sub(
|
|
"(https?://[a-zA-Z0-9._-]+[a-zA-Z0-9_#?%+=./-]*[a-zA-Z0-9_#?%+=/-])",
|
|
r'<a href="\1">\1</a>',
|
|
escaped_text,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if not have_display:
|
|
apport.fatal(
|
|
"This program needs a running X session. Please see"
|
|
' "man apport-cli" for a command line version of Apport.'
|
|
)
|
|
app = GTKUserInterface(sys.argv)
|
|
app.run_argv()
|