diff --git a/README.md b/README.md index cc978ad..c36d333 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,12 @@ A GUI for basic tasks of package management using apt: * Check for and list all available upgrades * Download and install all available upgrades +![Screenshot](doc/screenshot.png "Screenshot: Updating the Package Cache") + ## Dependencies ```shell -apt install python3-gi +sudo apt install python3-gi ``` ## Installation diff --git a/bin/simple-apt-update b/bin/simple-apt-update index 7170658..15d9152 100755 --- a/bin/simple-apt-update +++ b/bin/simple-apt-update @@ -60,9 +60,8 @@ class UpdateWindow(Gtk.ApplicationWindow): self.buffer.set_text("") def scroll_to_bottom(self): - adj = self.scrolledwindow.get_vadjustment() - adj.set_value(adj.get_upper()) - self.scrolledwindow.set_vadjustment(adj) + self.buffer.get_end_iter() + self.text_view.scroll_to_mark(self.text_mark_end, 0, False, 0, 0) def level_to_color(self, level): if level == "INFO": @@ -72,47 +71,76 @@ class UpdateWindow(Gtk.ApplicationWindow): else: return "grey" - def prepend_mesg(self, level, text): - self.prepend(text) - self.prepend_color(level + ": ", self.level_to_color(level)) - def append_mesg(self, level, text): - self.append_color(level + ": ", self.level_to_color(level)) + self.append_color( + level + ": ", + self.level_to_color(level) + ) self.append(text) - def prepend_markup(self, markup): - self.insert_markup(markup, self.buffer.get_start_iter()) - def append_markup(self, markup): - self.insert_markup(markup, self.buffer.get_end_iter()) + self.insert_markup( + markup, + self.buffer.get_end_iter() + ) def insert_markup(self, markup, iter): self.buffer.insert_markup(iter, markup, -1) - def prepend_color(self, text, color): - self.insert_color(text, color, self.buffer.get_start_iter()) - def append_color(self, text, color): - self.insert_color(text, color, self.buffer.get_end_iter()) + self.insert_color( + text, + color, + self.buffer.get_end_iter() + ) def insert_color(self, text, color, iter): self.buffer.insert_markup( iter, - "%s" % (color, html.escape(text)), + "%s" % ( + color, + html.escape(text)), -1) - def prepend(self, text): - self.insert(text, self.buffer.get_start_iter()) - def append(self, text): - self.insert(text, self.buffer.get_end_iter()) + self.insert( + text, + self.buffer.get_end_iter() + ) self.scroll_to_bottom() def insert(self, text, iter): self.buffer.insert(iter, text + "\n") - def execute(self, args, ignore_stderr=False, output_msg=None, - empty_msg=None, env={}, clear=True): + def run_thread(self, args, env, prefix=None): + if self.thread is None: + do_run = True + elif not self.thread.is_alive(): + do_run = True + else: + do_run = False + + if do_run: + self.thread = threading.Thread( + target=self.run, + args=(args, env, prefix,)) + + self.thread.start() + + return False + else: + return True + + def execute( + self, + args, + ignore_stderr=False, + output_msg=None, + empty_msg=None, + prefix=None, + env={}, + clear=True + ): self.lock() if clear: @@ -123,15 +151,24 @@ class UpdateWindow(Gtk.ApplicationWindow): self.ignore_stderr = ignore_stderr self.stdout = '' self.stderr = '' - self.prepend_mesg( + self.append_mesg( "INFO", "Running command \"%s\" ..." % " ".join(args)) - thread = threading.Thread(target=self.run, args=(args, env,)) - thread.start() - def run(self, args, env={}): + GLib.timeout_add( + 250, + self.run_thread, + args, + env, + prefix + ) + + def run(self, args, env={}, prefix=None): p = subprocess.Popen( - args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, env=dict(os.environ, **env)) sel = selectors.DefaultSelector() @@ -145,7 +182,10 @@ class UpdateWindow(Gtk.ApplicationWindow): if data: if key.fileobj is p.stdout: - self.stdout_queue.put(data) + if prefix is None: + self.stdout_queue.put(data) + else: + self.stdout_queue.put(prefix + " " + data) else: self.stderr_queue.put(data) @@ -176,16 +216,15 @@ class UpdateWindow(Gtk.ApplicationWindow): env=env, empty_msg="No package upgrades were performed.") - def on_upgrade(self, *args): - self.upgrade() - def update(self, clear=True): args = ['/usr/bin/apt-get', '-y', 'update'] env = {'DEBIAN_FRONTEND': 'noninteractive'} - self.execute(args, env=env, clear=clear) - - def on_update(self, *args): - self.update() + self.execute( + args, + env=env, + clear=clear, + output_msg="The package cache was refreshed." + ) def list(self, clear=True): args = ['/usr/bin/apt', '-qq', 'list', '--upgradable'] @@ -193,8 +232,16 @@ class UpdateWindow(Gtk.ApplicationWindow): args, ignore_stderr=True, clear=clear, - output_msg="Found the following package upgrades:", - empty_msg="Currently there are no available package upgrades.") + prefix="UPDATE", + output_msg="Done listing available upgrades.", + empty_msg="No package upgrades found." + ) + + def on_update(self, *args): + self.update() + + def on_upgrade(self, *args): + self.upgrade() def on_list(self, *args): self.list() @@ -202,27 +249,39 @@ class UpdateWindow(Gtk.ApplicationWindow): def on_quit(self, *args): self.application.quit() + def process_exit(self, exit_code, empty_msg=None, output_msg=None): + self.thread.join() + + if exit_code != 0: + self.append_mesg( + "ERROR", + "Command exited with code %d" % exit_code + ) + elif self.stdout == '' and self.empty_msg is not None: + self.append_mesg("INFO", self.empty_msg) + elif self.stdout != '' and self.output_msg is not None: + self.append_mesg("INFO", self.output_msg) + + self.unlock() + def update_buffer(self): try: text = self.stdout_queue.get(block=False) - match = re.fullmatch(r'EXIT (\d+)', text) + match = re.fullmatch(r'(EXIT|UPDATE) (.*)', text) if match is None: self.stdout += text self.append_mesg("STDOUT", text) + elif match.group(1) == 'EXIT': + exit_code = int(match.group(2)) + self.process_exit(exit_code) + elif match.group(1) == 'UPDATE': + text = match.group(2) + self.stdout += text + self.append_mesg("UPDATE", text) else: - exit_code = int(match.group(1)) - - if exit_code != 0: - self.append_mesg( - "ERROR", - "Command exited with code %d" % exit_code) - elif self.stdout == '' and self.empty_msg is not None: - self.append_mesg("INFO", self.empty_msg) - elif self.stdout != '' and self.output_msg is not None: - self.append_mesg("INFO", self.empty_msg) - - self.unlock() + self.stdout += text + self.append_mesg("UPDATE", text) except queue.Empty: pass @@ -240,20 +299,31 @@ class UpdateWindow(Gtk.ApplicationWindow): return True def __init__(self, application): - super(UpdateWindow, self).__init__( + super( + UpdateWindow, + self + ).__init__( application=application, - title="Simple APT Update") + title="Simple APT Update" + ) + self.application = application + self.thread = None self.stdout_queue = queue.Queue() self.stderr_queue = queue.Queue() - GLib.timeout_add(100, self.update_buffer) + self.init_ui() + GLib.timeout_add(100, self.update_buffer) + def init_ui(self): self.set_border_width(10) self.set_default_size(630, 390) - hbox = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL) + hbox = Gtk.Box( + spacing=6, + orientation=Gtk.Orientation.VERTICAL + ) self.add(hbox) grid = Gtk.Grid() @@ -272,9 +342,12 @@ class UpdateWindow(Gtk.ApplicationWindow): grid.attach(self.list_button, 1, 0, 1, 1) self.upgrade_button = Gtk.Button.new_from_icon_name( - "gtk-apply", Gtk.IconSize.BUTTON) + "gtk-apply", + Gtk.IconSize.BUTTON + ) self.upgrade_button.set_tooltip_text( - "Download and install all available upgrades") + "Download and install all available upgrades" + ) self.upgrade_button.connect("clicked", self.on_upgrade) grid.attach(self.upgrade_button, 2, 0, 1, 1) @@ -283,7 +356,9 @@ class UpdateWindow(Gtk.ApplicationWindow): grid.attach(self.spinner, 3, 0, 1, 1) self.quit_button = Gtk.Button.new_from_icon_name( - "exit", Gtk.IconSize.BUTTON) + "exit", + Gtk.IconSize.BUTTON + ) self.quit_button.set_tooltip_text("Exit the program") self.quit_button.set_halign(Gtk.Align.END) self.quit_button.connect("clicked", self.on_quit) @@ -296,24 +371,32 @@ class UpdateWindow(Gtk.ApplicationWindow): self.scrolledwindow.set_max_content_height(300) self.buffer = Gtk.TextBuffer() - text_view = Gtk.TextView(buffer=self.buffer) - text_view.set_editable(False) - text_view.set_monospace(True) - text_view.set_cursor_visible(False) + self.text_view = Gtk.TextView(buffer=self.buffer) + self.text_view.set_editable(False) + self.text_view.set_monospace(True) + self.text_view.set_cursor_visible(False) - self.scrolledwindow.add(text_view) + text_buffer = self.text_view.get_buffer() + iter = text_buffer.get_end_iter() + self.text_mark_end = text_buffer.create_mark("", iter, False) + + self.scrolledwindow.add(self.text_view) hbox.pack_start(self.scrolledwindow, True, True, 0) class SimpleAptUpdate(Gtk.Application): def __init__(self): - super().__init__(application_id='de.linuxfoo.SimpleAptUpdate', - flags=Gio.ApplicationFlags.FLAGS_NONE) - self.connect('activate', self.on_activate) - signal.signal(signal.SIGINT, signal.SIG_DFL) + super( + SimpleAptUpdate, + self + ).__init__( + application_id='de.linuxfoo.SimpleAptUpdate', + flags=Gio.ApplicationFlags.FLAGS_NONE + ) - def do_command_line(self, cmdline): - pass + self.connect('activate', self.on_activate) + + signal.signal(signal.SIGINT, signal.SIG_DFL) def on_activate(self, application): self.window = UpdateWindow(application) @@ -321,7 +404,10 @@ class SimpleAptUpdate(Gtk.Application): action = Gio.SimpleAction.new("quit", None) action.connect("activate", self.window.on_quit) self.add_action(action) - self.set_accels_for_action('app.quit', ['q', 'w']) + self.set_accels_for_action( + 'app.quit', + ['q', 'w'] + ) action = Gio.SimpleAction.new("update", None) action.connect("activate", self.window.on_update) @@ -340,7 +426,6 @@ class SimpleAptUpdate(Gtk.Application): self.window.present() self.window.show_all() - self.window.update() self.window.list(clear=False) @@ -355,3 +440,5 @@ def main(): if __name__ == '__main__': main() + +# vim:fenc=utf-8:et:ts=4:sw=4 diff --git a/doc/screenshot.png b/doc/screenshot.png new file mode 100644 index 0000000..78d3b84 Binary files /dev/null and b/doc/screenshot.png differ diff --git a/res/icons/simple-apt-update.svg b/res/icons/simple-apt-update.svg index 5d60243..ebcd35d 100644 --- a/res/icons/simple-apt-update.svg +++ b/res/icons/simple-apt-update.svg @@ -7,19 +7,14 @@ xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="31.72216mm" height="36.307823mm" viewBox="0 0 31.72216 36.307823" version="1.1" - id="svg8" - inkscape:version="1.0.2 (e86c870879, 2021-01-15)" - sodipodi:docname="simple-apt-update.svg"> + id="svg8"> + orient="auto"> - - @@ -170,8 +127,6 @@ + ry="2.9184699" /> + id="path1321" /> + id="path1869" />