Compare commits

..

No commits in common. "main" and "0.2" have entirely different histories.
main ... 0.2

4 changed files with 125 additions and 165 deletions

View File

@ -8,12 +8,10 @@ A GUI for basic tasks of package management using apt:
* Check for and list all available upgrades * Check for and list all available upgrades
* Download and install all available upgrades * Download and install all available upgrades
![Screenshot](doc/screenshot.png "Screenshot: Updating the Package Cache")
## Dependencies ## Dependencies
```shell ```shell
sudo apt install python3-gi apt install python3-gi
``` ```
## Installation ## Installation

View File

@ -60,8 +60,9 @@ class UpdateWindow(Gtk.ApplicationWindow):
self.buffer.set_text("") self.buffer.set_text("")
def scroll_to_bottom(self): def scroll_to_bottom(self):
self.buffer.get_end_iter() adj = self.scrolledwindow.get_vadjustment()
self.text_view.scroll_to_mark(self.text_mark_end, 0, False, 0, 0) adj.set_value(adj.get_upper())
self.scrolledwindow.set_vadjustment(adj)
def level_to_color(self, level): def level_to_color(self, level):
if level == "INFO": if level == "INFO":
@ -71,76 +72,47 @@ class UpdateWindow(Gtk.ApplicationWindow):
else: else:
return "grey" 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): def append_mesg(self, level, text):
self.append_color( self.append_color(level + ": ", self.level_to_color(level))
level + ": ",
self.level_to_color(level)
)
self.append(text) self.append(text)
def prepend_markup(self, markup):
self.insert_markup(markup, self.buffer.get_start_iter())
def append_markup(self, markup): def append_markup(self, markup):
self.insert_markup( self.insert_markup(markup, self.buffer.get_end_iter())
markup,
self.buffer.get_end_iter()
)
def insert_markup(self, markup, iter): def insert_markup(self, markup, iter):
self.buffer.insert_markup(iter, markup, -1) 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): def append_color(self, text, color):
self.insert_color( self.insert_color(text, color, self.buffer.get_end_iter())
text,
color,
self.buffer.get_end_iter()
)
def insert_color(self, text, color, iter): def insert_color(self, text, color, iter):
self.buffer.insert_markup( self.buffer.insert_markup(
iter, iter,
"<span color=\"%s\">%s</span>" % ( "<span color=\"%s\">%s</span>" % (color, html.escape(text)),
color,
html.escape(text)),
-1) -1)
def prepend(self, text):
self.insert(text, self.buffer.get_start_iter())
def append(self, text): def append(self, text):
self.insert( self.insert(text, self.buffer.get_end_iter())
text,
self.buffer.get_end_iter()
)
self.scroll_to_bottom() self.scroll_to_bottom()
def insert(self, text, iter): def insert(self, text, iter):
self.buffer.insert(iter, text + "\n") self.buffer.insert(iter, text + "\n")
def run_thread(self, args, env, prefix=None): def execute(self, args, ignore_stderr=False, output_msg=None,
if self.thread is None: empty_msg=None, env={}, clear=True):
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() self.lock()
if clear: if clear:
@ -151,24 +123,15 @@ class UpdateWindow(Gtk.ApplicationWindow):
self.ignore_stderr = ignore_stderr self.ignore_stderr = ignore_stderr
self.stdout = '' self.stdout = ''
self.stderr = '' self.stderr = ''
self.append_mesg( self.prepend_mesg(
"INFO", "INFO",
"Running command \"%s\" ..." % " ".join(args)) "Running command \"%s\" ..." % " ".join(args))
thread = threading.Thread(target=self.run, args=(args, env,))
thread.start()
GLib.timeout_add( def run(self, args, env={}):
250,
self.run_thread,
args,
env,
prefix
)
def run(self, args, env={}, prefix=None):
p = subprocess.Popen( p = subprocess.Popen(
args, args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False,
env=dict(os.environ, **env)) env=dict(os.environ, **env))
sel = selectors.DefaultSelector() sel = selectors.DefaultSelector()
@ -182,10 +145,7 @@ class UpdateWindow(Gtk.ApplicationWindow):
if data: if data:
if key.fileobj is p.stdout: if key.fileobj is p.stdout:
if prefix is None:
self.stdout_queue.put(data) self.stdout_queue.put(data)
else:
self.stdout_queue.put(prefix + " " + data)
else: else:
self.stderr_queue.put(data) self.stderr_queue.put(data)
@ -216,15 +176,16 @@ class UpdateWindow(Gtk.ApplicationWindow):
env=env, env=env,
empty_msg="No package upgrades were performed.") empty_msg="No package upgrades were performed.")
def on_upgrade(self, *args):
self.upgrade()
def update(self, clear=True): def update(self, clear=True):
args = ['/usr/bin/apt-get', '-y', 'update'] args = ['/usr/bin/apt-get', '-y', 'update']
env = {'DEBIAN_FRONTEND': 'noninteractive'} env = {'DEBIAN_FRONTEND': 'noninteractive'}
self.execute( self.execute(args, env=env, clear=clear)
args,
env=env, def on_update(self, *args):
clear=clear, self.update()
output_msg="The package cache was refreshed."
)
def list(self, clear=True): def list(self, clear=True):
args = ['/usr/bin/apt', '-qq', 'list', '--upgradable'] args = ['/usr/bin/apt', '-qq', 'list', '--upgradable']
@ -232,16 +193,8 @@ class UpdateWindow(Gtk.ApplicationWindow):
args, args,
ignore_stderr=True, ignore_stderr=True,
clear=clear, clear=clear,
prefix="UPDATE", output_msg="Found the following package upgrades:",
output_msg="Done listing available upgrades.", empty_msg="Currently there are no available package 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): def on_list(self, *args):
self.list() self.list()
@ -249,39 +202,27 @@ class UpdateWindow(Gtk.ApplicationWindow):
def on_quit(self, *args): def on_quit(self, *args):
self.application.quit() 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): def update_buffer(self):
try: try:
text = self.stdout_queue.get(block=False) text = self.stdout_queue.get(block=False)
match = re.fullmatch(r'(EXIT|UPDATE) (.*)', text) match = re.fullmatch(r'EXIT (\d+)', text)
if match is None: if match is None:
self.stdout += text self.stdout += text
self.append_mesg("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: else:
self.stdout += text exit_code = int(match.group(1))
self.append_mesg("UPDATE", text)
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()
except queue.Empty: except queue.Empty:
pass pass
@ -299,31 +240,20 @@ class UpdateWindow(Gtk.ApplicationWindow):
return True return True
def __init__(self, application): def __init__(self, application):
super( super(UpdateWindow, self).__init__(
UpdateWindow,
self
).__init__(
application=application, application=application,
title="Simple APT Update" title="Simple APT Update")
)
self.application = application self.application = application
self.thread = None
self.stdout_queue = queue.Queue() self.stdout_queue = queue.Queue()
self.stderr_queue = queue.Queue() self.stderr_queue = queue.Queue()
self.init_ui()
GLib.timeout_add(100, self.update_buffer) GLib.timeout_add(100, self.update_buffer)
self.init_ui()
def init_ui(self): def init_ui(self):
self.set_border_width(10) self.set_border_width(10)
self.set_default_size(630, 390) self.set_default_size(630, 390)
hbox = Gtk.Box( hbox = Gtk.Box(spacing=6, orientation=Gtk.Orientation.VERTICAL)
spacing=6,
orientation=Gtk.Orientation.VERTICAL
)
self.add(hbox) self.add(hbox)
grid = Gtk.Grid() grid = Gtk.Grid()
@ -342,12 +272,9 @@ class UpdateWindow(Gtk.ApplicationWindow):
grid.attach(self.list_button, 1, 0, 1, 1) grid.attach(self.list_button, 1, 0, 1, 1)
self.upgrade_button = Gtk.Button.new_from_icon_name( self.upgrade_button = Gtk.Button.new_from_icon_name(
"gtk-apply", "gtk-apply", Gtk.IconSize.BUTTON)
Gtk.IconSize.BUTTON
)
self.upgrade_button.set_tooltip_text( 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) self.upgrade_button.connect("clicked", self.on_upgrade)
grid.attach(self.upgrade_button, 2, 0, 1, 1) grid.attach(self.upgrade_button, 2, 0, 1, 1)
@ -356,9 +283,7 @@ class UpdateWindow(Gtk.ApplicationWindow):
grid.attach(self.spinner, 3, 0, 1, 1) grid.attach(self.spinner, 3, 0, 1, 1)
self.quit_button = Gtk.Button.new_from_icon_name( self.quit_button = Gtk.Button.new_from_icon_name(
"exit", "exit", Gtk.IconSize.BUTTON)
Gtk.IconSize.BUTTON
)
self.quit_button.set_tooltip_text("Exit the program") self.quit_button.set_tooltip_text("Exit the program")
self.quit_button.set_halign(Gtk.Align.END) self.quit_button.set_halign(Gtk.Align.END)
self.quit_button.connect("clicked", self.on_quit) self.quit_button.connect("clicked", self.on_quit)
@ -371,43 +296,32 @@ class UpdateWindow(Gtk.ApplicationWindow):
self.scrolledwindow.set_max_content_height(300) self.scrolledwindow.set_max_content_height(300)
self.buffer = Gtk.TextBuffer() self.buffer = Gtk.TextBuffer()
self.text_view = Gtk.TextView(buffer=self.buffer) text_view = Gtk.TextView(buffer=self.buffer)
self.text_view.set_editable(False) text_view.set_editable(False)
self.text_view.set_monospace(True) text_view.set_monospace(True)
self.text_view.set_cursor_visible(False) text_view.set_cursor_visible(False)
text_buffer = self.text_view.get_buffer() self.scrolledwindow.add(text_view)
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) hbox.pack_start(self.scrolledwindow, True, True, 0)
class SimpleAptUpdate(Gtk.Application): class SimpleAptUpdate(Gtk.Application):
def __init__(self): def __init__(self):
super( super().__init__(application_id='de.linuxfoo.SimpleAptUpdate',
SimpleAptUpdate, flags=Gio.ApplicationFlags.FLAGS_NONE)
self
).__init__(
application_id='de.linuxfoo.SimpleAptUpdate',
flags=Gio.ApplicationFlags.FLAGS_NONE
)
self.connect('activate', self.on_activate) self.connect('activate', self.on_activate)
signal.signal(signal.SIGINT, signal.SIG_DFL) signal.signal(signal.SIGINT, signal.SIG_DFL)
def do_command_line(self, cmdline):
pass
def on_activate(self, application): def on_activate(self, application):
self.window = UpdateWindow(application) self.window = UpdateWindow(application)
action = Gio.SimpleAction.new("quit", None) action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.window.on_quit) action.connect("activate", self.window.on_quit)
self.add_action(action) self.add_action(action)
self.set_accels_for_action( self.set_accels_for_action('app.quit', ['<Primary>q', '<Primary>w'])
'app.quit',
['<Primary>q', '<Primary>w']
)
action = Gio.SimpleAction.new("update", None) action = Gio.SimpleAction.new("update", None)
action.connect("activate", self.window.on_update) action.connect("activate", self.window.on_update)
@ -426,6 +340,7 @@ class SimpleAptUpdate(Gtk.Application):
self.window.present() self.window.present()
self.window.show_all() self.window.show_all()
self.window.update()
self.window.list(clear=False) self.window.list(clear=False)
@ -440,5 +355,3 @@ def main():
if __name__ == '__main__': if __name__ == '__main__':
main() main()
# vim:fenc=utf-8:et:ts=4:sw=4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@ -7,14 +7,19 @@
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" 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" width="31.72216mm"
height="36.307823mm" height="36.307823mm"
viewBox="0 0 31.72216 36.307823" viewBox="0 0 31.72216 36.307823"
version="1.1" version="1.1"
id="svg8"> id="svg8"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
sodipodi:docname="simple-apt-update.svg">
<defs <defs
id="defs2"> id="defs2">
<linearGradient <linearGradient
inkscape:collect="always"
id="linearGradient4117"> id="linearGradient4117">
<stop <stop
style="stop-color:#000000;stop-opacity:1;" style="stop-color:#000000;stop-opacity:1;"
@ -50,6 +55,7 @@
id="stop4093" /> id="stop4093" />
</linearGradient> </linearGradient>
<linearGradient <linearGradient
inkscape:collect="always"
id="linearGradient1878" id="linearGradient1878"
osb:paint="gradient"> osb:paint="gradient">
<stop <stop
@ -66,14 +72,24 @@
id="Arrow2Lend" id="Arrow2Lend"
refX="0" refX="0"
refY="0" refY="0"
orient="auto"> orient="auto"
inkscape:stockid="Arrow2Lend"
inkscape:isstock="true">
<path <path
transform="matrix(-1.1,0,0,-1.1,-1.1,0)" transform="matrix(-1.1,0,0,-1.1,-1.1,0)"
d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z" d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1" style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.625;stroke-linejoin:round;stroke-opacity:1"
id="path892" /> id="path892" />
</marker> </marker>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="177.41502 : 38.534923 : 1"
inkscape:vp_y="57.01026 : -606.09557 : 0"
inkscape:vp_z="-849.50931 : -58.059119 : 1"
inkscape:persp3d-origin="-338.86915 : 20.239614 : 1"
id="perspective833" />
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4095" xlink:href="#linearGradient4095"
id="linearGradient4087" id="linearGradient4087"
x1="41.938935" x1="41.938935"
@ -82,6 +98,7 @@
y2="60.562257" y2="60.562257"
gradientUnits="userSpaceOnUse" /> gradientUnits="userSpaceOnUse" />
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4095" xlink:href="#linearGradient4095"
id="linearGradient4089" id="linearGradient4089"
x1="22.932867" x1="22.932867"
@ -90,6 +107,7 @@
y2="48.843493" y2="48.843493"
gradientUnits="userSpaceOnUse" /> gradientUnits="userSpaceOnUse" />
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4095" xlink:href="#linearGradient4095"
id="linearGradient4097" id="linearGradient4097"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
@ -98,6 +116,7 @@
x2="51.607597" x2="51.607597"
y2="48.390368" /> y2="48.390368" />
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4117" xlink:href="#linearGradient4117"
id="linearGradient4121" id="linearGradient4121"
x1="22.932867" x1="22.932867"
@ -106,6 +125,7 @@
y2="48.843493" y2="48.843493"
gradientUnits="userSpaceOnUse" /> gradientUnits="userSpaceOnUse" />
<linearGradient <linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4117" xlink:href="#linearGradient4117"
id="linearGradient4123" id="linearGradient4123"
x1="19.885436" x1="19.885436"
@ -114,6 +134,29 @@
y2="48.390368" y2="48.390368"
gradientUnits="userSpaceOnUse" /> gradientUnits="userSpaceOnUse" />
</defs> </defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="3.959798"
inkscape:cx="81.30198"
inkscape:cy="102.74857"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1885"
inkscape:window-height="1236"
inkscape:window-x="2101"
inkscape:window-y="111"
inkscape:window-maximized="0"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata <metadata
id="metadata5"> id="metadata5">
<rdf:RDF> <rdf:RDF>
@ -127,6 +170,8 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<g <g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(-19.885436,-34.013894)"> transform="translate(-19.885436,-34.013894)">
<rect <rect
@ -136,14 +181,18 @@
height="27.752951" height="27.752951"
x="20.385435" x="20.385435"
y="34.513893" y="34.513893"
ry="2.9184699" /> ry="2.9184699"
inkscape:transform-center-x="13.051673"
inkscape:transform-center-y="11.996269" />
<path <path
style="fill:url(#linearGradient4121);stroke:url(#linearGradient4089);stroke-width:3.765;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1" style="fill:url(#linearGradient4121);stroke:url(#linearGradient4089);stroke-width:3.765;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;fill-opacity:1"
d="m 24.815338,34.950563 c 0.145105,26.252732 17.704313,25.913942 17.704313,25.913942" d="m 24.815338,34.950563 c 0.145105,26.252732 17.704313,25.913942 17.704313,25.913942"
id="path1321" /> id="path1321"
sodipodi:nodetypes="cc" />
<path <path
style="fill:#bebebe;fill-opacity:1;stroke:url(#linearGradient4087);stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" style="fill:#bebebe;fill-opacity:1;stroke:url(#linearGradient4087);stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 42.10017,59.891406 -0.02493,-8.714785 8.491092,10.446869 -8.494225,8.380984 z" d="m 42.10017,59.891406 -0.02493,-8.714785 8.491092,10.446869 -8.494225,8.380984 z"
id="path1869" /> id="path1869"
sodipodi:nodetypes="ccccc" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB