diff --git a/README.md b/README.md
index 2dbb4f4..0663efd 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
software-station
================
-Software Station is the future software manager using pkgng.
+Software Station is the software manager for GhostBSD.
diff --git a/pkg_info.py b/pkg_info.py
new file mode 100644
index 0000000..5867dbb
--- /dev/null
+++ b/pkg_info.py
@@ -0,0 +1,92 @@
+"""Package information loader for installed and available FreeBSD packages.
+
+This module defines the PkgInfo class, which loads installed and available packages
+using the system's `pkg` tool, and allows basic search/filtering operations.
+"""
+
+import subprocess
+from software_station.search_index import Package, PkgSearchIndex
+
+
+class PkgInfo:
+ """Handles retrieval of installed and available packages on the system."""
+
+
+ def __init__(self):
+ """Initializes the package lists and loads data from the package system."""
+ self.available: list[Package] = []
+ self.installed_names: set[str] = set()
+ self.index = None
+ self.load()
+
+
+ def load(self):
+ """Loads both installed and available package data."""
+ self.load_installed()
+ self.load_available()
+
+
+ def load_installed(self):
+ """Retrieves the names of currently installed packages."""
+ try:
+ result = subprocess.run(
+ ['pkg', 'query', '%n'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ text=True,
+ timeout=10
+ )
+ self.installed_names = {name.strip() for name in result.stdout.splitlines()}
+ except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
+ self.installed_names = set()
+
+
+ def load_available(self):
+ """Retrieves all available packages with name, version, and description."""
+ try:
+ result = subprocess.run(
+ ['pkg', 'query', '-a', '%n %v %e'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ text=True,
+ timeout=10
+ )
+ self.available = []
+ for line in result.stdout.splitlines():
+ parts = line.strip().split(' ', 2)
+ if len(parts) >= 2:
+ name, version = parts[0], parts[1]
+ desc = parts[2] if len(parts) == 3 else ''
+ self.available.append(Package(
+ name=name,
+ version=version,
+ description=desc,
+ installed=name in self.installed_names
+ ))
+ self.index = PkgSearchIndex(self.available)
+ except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
+ self.available = []
+ self.index = None
+
+
+ def search(self, prefix: str) -> list[Package]:
+ """Searches available packages by prefix.
+
+ Args:
+ prefix (str): The string to match at the beginning of package names.
+
+ Returns:
+ list[Package]: Matching package list.
+ """
+ return self.available if self.index is None else self.index.search_prefix(prefix)
+
+
+ def get_installed(self) -> list[Package]:
+ """Returns all packages that are currently installed.
+
+ Returns:
+ list[Package]: Installed packages.
+ """
+ return [pkg for pkg in self.available if pkg.installed]
diff --git a/software-station b/software-station
index 2f6bfcf..7a64355 100755
--- a/software-station
+++ b/software-station
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
-Copyright (c) 2017-2018, GhostBSD. All rights reserved.
+Copyright (c) 2017-2025, GhostBSD. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
@@ -28,20 +28,25 @@ ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
-import gi
-gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk, GdkPixbuf, GLib, Gdk
+# Standard library imports
import threading
-import crypt
import pwd
import os
import gettext
+import crypt
from time import sleep
+# Third-party imports
+import gi
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk, GdkPixbuf, GLib, Gdk
+
+# Configure gettext for internationalization
gettext.bindtextdomain('software-station', '/usr/local/share/locale')
gettext.textdomain('software-station')
_ = gettext.gettext
+# Local imports
from software_station_pkg import (
search_packages,
available_package_origin,
@@ -58,11 +63,11 @@ from software_station_pkg import (
start_update_station,
repository_is_syncing
)
-
from software_station_xpm import xpm_package_category
__VERSION__ = '2.0'
+# pylint: disable=global-at-module-level
global pkg_to_install
pkg_to_install = []
@@ -70,19 +75,19 @@ global pkg_to_uninstall
pkg_to_uninstall = []
-class TableWindow(Gtk.Window):
+class TableWindow(Gtk.Window): # pylint: disable=too-many-instance-attributes,too-many-public-methods
+ """Main window for the Software Station GUI."""
def __init__(self):
- Gtk.Window.__init__(self)
+ super().__init__()
self.set_title(_("Software Station"))
self.connect("delete-event", Gtk.main_quit)
self.set_size_request(850, 500)
self.set_default_icon_name('system-software-install')
# Creating the toolbar
toolbar = Gtk.Toolbar()
- # toolbar.set_style(Gtk.ToolbarStyle.BOTH)
self.box1 = Gtk.VBox(homogeneous=False, spacing=0)
- self.add(self.box1)
+ self.add(self.box1) # pylint: disable=no-member
self.box1.show()
self.box1.pack_start(toolbar, False, False, 0)
self.previousbutton = Gtk.ToolButton()
@@ -90,18 +95,15 @@ class TableWindow(Gtk.Window):
self.previousbutton.set_is_important(True)
self.previousbutton.set_icon_name("go-previous")
self.previousbutton.set_sensitive(False)
- #toolbar.insert(self.previousbutton, 1)
self.nextbutton = Gtk.ToolButton()
self.nextbutton.set_label(_("Forward"))
self.nextbutton.set_is_important(True)
self.nextbutton.set_icon_name("go-next")
self.nextbutton.set_sensitive(False)
- #toolbar.insert(self.nextbutton, 2)
self.desc_is_shown = False
self.desc_toggle = Gtk.ToggleToolButton()
self.desc_toggle.set_property("tooltip-text", _("Show Description"))
- # self.desc_toggle.set_label(_("Show Description"))
self.desc_toggle.set_icon_name("view-list")
self.desc_toggle.connect("toggled", self.toggle_description)
self.desc_toggle.set_sensitive(False)
@@ -136,34 +138,32 @@ class TableWindow(Gtk.Window):
self.search_entry.connect("key-release-event", self.search_release)
self.search_entry.set_property("tooltip-text", _("Search Software"))
self.search_entry.set_sensitive(False)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- toolitem.add(hBox)
- hBox.show()
- hBox.pack_start(self.search_entry, False, False, 0)
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ toolitem.add(hbox)
+ hbox.show()
+ hbox.pack_start(self.search_entry, False, False, 0)
self.description_search = False
self.search_description = Gtk.CheckButton(
label=_("Search Description"))
self.search_description.connect("toggled",
- self.search_description_toggled)
+ self.search_description_toggled)
self.search_description.set_sensitive(False)
- hBox.pack_start(self.search_description, False, False, 0)
+ hbox.pack_start(self.search_description, False, False, 0)
self.apply_button = Gtk.Button()
self.apply_button.set_label(_("Apply"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-apply', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-apply', 1)
self.apply_button.set_image(apply_img)
self.apply_button.set_property(
"tooltip-text", _("Apply change on the system"))
self.apply_button.set_sensitive(False)
- hBox.pack_end(self.apply_button, False, False, 0)
+ hbox.pack_end(self.apply_button, False, False, 0)
self.cancel_button = Gtk.Button()
self.cancel_button.set_label(_("Cancel"))
- cancel_img = Gtk.Image()
- cancel_img.set_from_icon_name('gtk-cancel', 1)
+ cancel_img = Gtk.Image.new_from_icon_name('gtk-cancel', 1)
self.cancel_button.set_image(cancel_img)
self.cancel_button.set_sensitive(False)
self.cancel_button.set_property("tooltip-text", _("Cancel changes"))
- hBox.pack_end(self.cancel_button, False, False, 0)
+ hbox.pack_end(self.cancel_button, False, False, 0)
self.apply_button.connect("clicked", self.confirm_packages)
self.cancel_button.connect("clicked", self.cancel_change)
# Creating a notebook to switch
@@ -171,7 +171,7 @@ class TableWindow(Gtk.Window):
self.mainstack.show()
self.mainstack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT)
self.box1.pack_start(self.mainstack, True, True, 0)
- mainwin = self.MainBook()
+ mainwin = self.main_book()
self.mainstack.add_named(mainwin, "mainwin")
self.pkg_statistic = Gtk.Label()
self.pkg_statistic.set_use_markup(True)
@@ -188,7 +188,7 @@ class TableWindow(Gtk.Window):
context = Gtk.StyleContext()
screen = Gdk.Screen.get_default()
context.add_provider_for_screen(
- screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+ screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) # pylint: disable=no-member
grid = Gtk.Grid()
grid.set_column_spacing(10)
grid.set_margin_start(10)
@@ -201,38 +201,42 @@ class TableWindow(Gtk.Window):
grid.attach(self.progress, 4, 0, 6, 1)
grid.show()
self.box1.pack_start(grid, False, False, 0)
- self.show_all()
+ self.show_all() # pylint: disable=no-member
self.initial_thread('initial')
- def search_description_toggled(self, widget):
+ def search_description_toggled(self, widget): # pylint: disable=unused-argument
+ """Toggle whether to search package descriptions."""
if widget.get_active():
self.description_search = True
else:
self.description_search = False
- def toggle_description(self, widget):
+ def toggle_description(self, widget): # pylint: disable=unused-argument
+ """Toggle the display of package descriptions in the UI."""
self.desc_is_shown = not self.desc_is_shown
if self.desc_is_shown:
self.table.remove(self.pkg_sw)
self.table.attach(self.pkg_sw, 2, 12, 0, 8)
self.table.attach(self.description_sw, 2, 12, 8, 12)
self.pkg_sw.show()
- self.description_sw.show_all()
+ self.description_sw.show_all() # pylint: disable=no-member
else:
self.table.remove(self.pkg_sw)
self.table.remove(self.description_sw)
self.table.attach(self.pkg_sw, 2, 12, 0, 12)
self.pkg_sw.show()
- def cancel_change(self, widget):
+ def cancel_change(self, widget): # pylint: disable=unused-argument
+ """Cancel pending package changes and reset the UI."""
+ # pylint: disable=global-statement
global pkg_to_uninstall
global pkg_to_install
pkg_to_install = []
pkg_to_uninstall = []
pkg_selection = self.pkgtreeview.get_selection()
- (model, iter) = pkg_selection.get_selected()
+ model, selected_iter = pkg_selection.get_selected()
try:
- pkg_path = model.get_path(iter)
+ pkg_path = model.get_path(selected_iter)
except KeyError:
nopath = True
else:
@@ -249,17 +253,21 @@ class TableWindow(Gtk.Window):
self.apply_button.set_sensitive(False)
self.cancel_button.set_sensitive(False)
- def apply_change(self, widget):
+ def apply_change(self, widget): # pylint: disable=unused-argument
+ """Apply selected package changes."""
self.confirm_window.hide()
self.apply_thr = threading.Thread(target=self.apply_package_change,
- args=())
+ args=())
self.apply_thr.start()
def stop_apply_tread(self):
+ """Stop the package change thread."""
self.apply_thr.join()
def apply_package_change(self):
+ """Process package installation and uninstallation."""
self.progress.show()
+ # pylint: disable=global-statement
global pkg_to_uninstall
global pkg_to_install
un_num = len(pkg_to_uninstall)
@@ -297,8 +305,7 @@ class TableWindow(Gtk.Window):
fraction += increment
for pkg in pkg_to_install:
- msg = _("Installing")
- msg += f" {pkg}"
+ msg = f"{_('Installing')} {pkg}"
GLib.idle_add(self.update_progress, self.progress, fraction, msg)
dpkg = install_packages(pkg)
while True:
@@ -328,6 +335,7 @@ class TableWindow(Gtk.Window):
GLib.idle_add(self.unlock_ui)
def all_or_installed(self, widget, data):
+ """Switch between displaying all or installed packages."""
if widget.get_active():
self.available_or_installed = data
if data == 'available':
@@ -345,20 +353,24 @@ class TableWindow(Gtk.Window):
self.update_pkg_store()
def sync_available_packages(self):
+ """Sync available package data."""
self.available_category = available_package_origin()
self.available_pkg = available_package_dictionary(
self.available_category)
def sync_installed_packages(self):
+ """Sync installed package data."""
self.installed_category = installed_package_origin()
self.installed_pkg = installed_package_dictionary(
self.installed_category)
def update_progress(self, progress, fraction, msg):
+ """Update the progress bar with the given fraction and message."""
progress.set_fraction(fraction)
progress.set_text(msg)
def initial_sync(self):
+ """Perform initial synchronization of package data."""
self.pkg_statistic.set_text(_('Syncing statistic'))
self.pkg_statistic.set_use_markup(True)
self.network = network_stat()
@@ -408,18 +420,21 @@ class TableWindow(Gtk.Window):
elif self.need_upgrade is True:
GLib.idle_add(self.confirm_upgrade)
- def hide_window(self, button, window):
+ def hide_window(self, button, window): # pylint: disable=unused-argument
+ """Hide the specified window."""
window.hide()
def confirm_offline(self):
+ """Show a dialog for offline or syncing repository status."""
window = Gtk.Window()
window.set_title(_("Software Station"))
window.connect("delete-event", Gtk.main_quit)
- window.set_keep_above(True)
+ window.set_keep_above(True) # pylint: disable=no-member
window.set_size_request(200, 80)
box1 = Gtk.VBox(homogeneous=False, spacing=0)
- window.add(box1)
+ window.add(box1) # pylint: disable=no-member
box1.show()
+ msg = None
if self.network == 'Down':
msg = _('Network device is offline')
elif self.online is False:
@@ -428,75 +443,75 @@ class TableWindow(Gtk.Window):
msg = _(
"Software repositories are syncing with new software packages"
)
- label = Gtk.Label(label=msg)
- box1.pack_start(label, True, True, 0)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- box1.pack_end(hBox, False, False, 5)
+ if msg:
+ label = Gtk.Label(label=msg)
+ box1.pack_start(label, True, True, 0)
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox.show()
+ box1.pack_end(hbox, False, False, 5)
offline_button = Gtk.Button()
offline_button.set_label(_("Use Offline"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-ok', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-ok', 1)
offline_button.set_image(apply_img)
offline_button.connect("clicked", self.hide_window, window)
- hBox.pack_end(offline_button, False, False, 5)
+ hbox.pack_end(offline_button, False, False, 5)
close_button = Gtk.Button()
close_button.set_label(_("Close"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-close', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-close', 1)
close_button.set_image(apply_img)
close_button.connect("clicked", Gtk.main_quit)
- hBox.pack_end(close_button, False, False, 5)
- window.show_all()
+ hbox.pack_end(close_button, False, False, 5)
+ window.show_all() # pylint: disable=no-member
- def start_upgrade(self, button):
+ def start_upgrade(self, button): # pylint: disable=unused-argument
+ """Start the system upgrade process."""
start_update_station()
Gtk.main_quit()
def confirm_upgrade(self):
+ """Show a dialog prompting for system upgrade."""
window = Gtk.Window()
window.set_title(_("Upgrade Needed"))
window.connect("delete-event", Gtk.main_quit)
- window.set_keep_above(True)
+ window.set_keep_above(True) # pylint: disable=no-member
window.set_size_request(200, 80)
- vBox = Gtk.VBox(homogeneous=False, spacing=0)
- window.add(vBox)
- vBox.show()
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- vBox.pack_start(hBox, False, False, 10)
+ vbox = Gtk.VBox(homogeneous=False, spacing=0)
+ window.add(vbox) # pylint: disable=no-member
+ vbox.show()
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox.show()
+ vbox.pack_start(hbox, False, False, 10)
title = _("Warning this system needs to be upgraded!")
title_label = Gtk.Label(label=title)
title_label.set_use_markup(True)
title_label.set_justify(Gtk.Justification.CENTER)
- hBox.pack_start(title_label, True, True, 10)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- vBox.pack_start(hBox, False, False, 5)
+ hbox.pack_start(title_label, True, True, 10)
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox.show()
+ vbox.pack_start(hbox, False, False, 5)
msg = _("Installing software without upgrading could harm this "
"installation.\nWould you like to upgrade now?")
msg_label = Gtk.Label(label=msg)
- hBox.pack_start(msg_label, True, True, 20)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- vBox.pack_end(hBox, False, False, 5)
+ hbox.pack_start(msg_label, True, True, 20)
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox.show()
+ vbox.pack_end(hbox, False, False, 5)
offline_button = Gtk.Button()
offline_button.set_label(_("Yes"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-yes', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-yes', 1)
offline_button.set_image(apply_img)
offline_button.connect("clicked", self.start_upgrade)
- hBox.pack_end(offline_button, False, False, 5)
+ hbox.pack_end(offline_button, False, False, 5)
close_button = Gtk.Button()
close_button.set_label(_("No"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-no', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-no', 1)
close_button.set_image(apply_img)
close_button.connect("clicked", self.hide_window, window)
- hBox.pack_end(close_button, False, False, 5)
- window.show_all()
+ hbox.pack_end(close_button, False, False, 5)
+ window.show_all() # pylint: disable=no-member
def unlock_ui(self):
+ """Enable UI elements."""
self.desc_toggle.set_sensitive(True)
self.origin_treeview.set_sensitive(True)
self.pkgtreeview.set_sensitive(True)
@@ -508,6 +523,7 @@ class TableWindow(Gtk.Window):
self.search_description.set_sensitive(True)
def lock_ui(self):
+ """Disable UI elements during operations."""
self.desc_toggle.set_sensitive(False)
self.search_entry.set_sensitive(False)
self.search_description.set_sensitive(False)
@@ -517,21 +533,25 @@ class TableWindow(Gtk.Window):
self.pkgtreeview.set_sensitive(False)
def stop_tread(self):
+ """Stop the synchronization thread."""
self.thr.join()
- def initial_thread(self, sync):
+ def initial_thread(self, sync): # pylint: disable=unused-argument
+ """Start the initial synchronization thread."""
self.thr = threading.Thread(target=self.initial_sync, args=())
self.thr.start()
- def selected_software(self, view, event):
+ def selected_software(self, view, event): # pylint: disable=unused-argument
+ """Handle package selection in the UI."""
selection = self.pkgtreeview.get_selection()
- (model, iter) = selection.get_selected()
+ selection.get_selected() # No-op, kept for compatibility
- def search_release(self, widget, event):
+ def search_release(self, widget, event): # pylint: disable=unused-argument
+ """Handle search input events."""
self.search = widget.get_text()
try:
- self.previous_search
- globals()[f'stop_search_{self.previous_search}'] = True
+ if hasattr(self, 'previous_search'):
+ globals()[f'stop_search_{self.previous_search}'] = True
except (AttributeError, KeyError):
pass
if self.search:
@@ -543,6 +563,7 @@ class TableWindow(Gtk.Window):
self.previous_search = self.search
def update_search(self, search):
+ """Update package search results."""
if globals()[f'stop_search_{search}'] is True:
return
pixbuf = Gtk.IconTheme.get_default().load_icon('package-x-generic', 42, 0)
@@ -558,20 +579,30 @@ class TableWindow(Gtk.Window):
for pkg in search_packages(search, self.description_search):
if globals()[f'stop_search_{search}'] is True:
return
- version = self.available_pkg['all'][pkg]['version']
- size = self.available_pkg['all'][pkg]['size']
- comment = self.available_pkg['all'][pkg]['comment']
- description = self.available_pkg['all'][pkg]['description']
- if pkg in pkg_to_install:
- installed = True
- elif pkg in pkg_to_uninstall:
- installed = False
- else:
- installed = self.available_pkg['all'][pkg]['installed']
- self.pkg_store.append([pixbuf, pkg, version, size, comment,
- installed, description])
+ if not pkg: # Skip empty package names
+ continue
+ if not self.available_pkg.get('all'): # Check if package data exists
+ continue
+ try:
+ pkg_data = self.available_pkg['all'].get(pkg, {})
+ version = pkg_data.get('version', 'N/A')
+ size = pkg_data.get('size', 'N/A')
+ comment = pkg_data.get('comment', '')
+ description = pkg_data.get('description', '')
+ if pkg in pkg_to_install:
+ installed = True
+ elif pkg in pkg_to_uninstall:
+ installed = False
+ else:
+ installed = pkg_data.get('installed', False)
+ self.pkg_store.append([pixbuf, pkg, version, size, comment,
+ installed, description])
+ except (KeyError, TypeError) as e:
+ print(f"Error processing package {pkg}: {e}")
+ continue
def category_store_sync(self):
+ """Sync package categories in the UI."""
self.store.clear()
self.search_entry.set_text('')
if self.online is True and repository_is_syncing() is False:
@@ -585,6 +616,7 @@ class TableWindow(Gtk.Window):
self.origin_treeview.set_cursor(0)
def selection_category(self, tree_selection):
+ """Handle category selection in the UI."""
(model, pathlist) = tree_selection.get_selected_rows()
if pathlist:
path = pathlist[0]
@@ -594,6 +626,7 @@ class TableWindow(Gtk.Window):
self.update_pkg_store()
def update_pkg_store(self):
+ """Update the package store with current data."""
self.pkg_store.clear()
pixbuf = Gtk.IconTheme.get_default().load_icon('package-x-generic', 42, 0)
if self.available_or_installed == 'available':
@@ -618,7 +651,8 @@ class TableWindow(Gtk.Window):
self.pkg_store.append([pixbuf, pkg, version, size, comment,
installed, description])
- def add_and_rm_pkg(self, cell, path, model):
+ def add_and_rm_pkg(self, cell, path, model): # pylint: disable=unused-argument
+ """Add or remove packages from install/uninstall lists."""
model[path][5] = not model[path][5]
pkg = model[path][1]
if pkg not in pkg_to_uninstall and pkg not in pkg_to_install:
@@ -638,11 +672,12 @@ class TableWindow(Gtk.Window):
self.apply_button.set_sensitive(True)
self.cancel_button.set_sensitive(True)
- def MainBook(self):
+ def main_book(self):
+ """Create the main notebook for package display."""
self.table = Gtk.Table(n_rows=12, n_columns=12, homogeneous=True)
self.table.show_all()
category_sw = Gtk.ScrolledWindow()
- category_sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+ category_sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) # pylint: disable=no-member
category_sw.set_policy(Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC)
self.store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
@@ -663,11 +698,11 @@ class TableWindow(Gtk.Window):
self.category_tree_selection.connect(
"changed", self.selection_category)
self.origin_treeview.set_sensitive(False)
- category_sw.add(self.origin_treeview)
+ category_sw.add(self.origin_treeview) # pylint: disable=no-member
category_sw.show()
self.pkg_sw = Gtk.ScrolledWindow()
- self.pkg_sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+ self.pkg_sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) # pylint: disable=no-member
self.pkg_sw.set_policy(
Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self.pkg_store = Gtk.ListStore(
@@ -690,7 +725,6 @@ class TableWindow(Gtk.Window):
self.pkgtreeview.append_column(pixbuf_column)
name_cell = Gtk.CellRendererText()
name_column = Gtk.TreeViewColumn(_('Package Name'), name_cell, text=1)
- # name_column.set_sizing(Gtk.TREE_VIEW_COLUMN_AUTOSIZE)
name_column.set_fixed_width(150)
name_column.set_sort_column_id(1)
name_column.set_resizable(True)
@@ -706,7 +740,6 @@ class TableWindow(Gtk.Window):
size_column.set_fixed_width(100)
size_column.set_sort_column_id(3)
size_column.set_resizable(True)
- # self.pkgtreeview.append_column(size_column)
comment_cell = Gtk.CellRendererText()
comment_column = Gtk.TreeViewColumn(_('Comment'), comment_cell, text=4)
comment_column.set_sort_column_id(4)
@@ -715,7 +748,7 @@ class TableWindow(Gtk.Window):
self.pkgtreeview.set_tooltip_column(4)
self.description_sw = Gtk.ScrolledWindow()
- self.description_sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
+ self.description_sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) # pylint: disable=no-member
self.description_sw.set_policy(
Gtk.PolicyType.AUTOMATIC,
Gtk.PolicyType.AUTOMATIC
@@ -723,71 +756,68 @@ class TableWindow(Gtk.Window):
description_label = Gtk.Label(
label=_("Click on a package to show its detailed description.")
)
- self.description_sw.add_with_viewport(description_label)
-
+ self.description_sw.add_with_viewport(description_label) # pylint: disable=no-member
self.pkg_tree_selection = self.pkgtreeview.get_selection()
- # self.pkg_tree_selection.set_mode(Gtk.SelectionMode.NONE)
- # tree_selection.connect("clicked", self.selected_software)
self.pkgtreeview.set_sensitive(False)
self.pkg_tree_selection.connect("changed", self.on_selection_changed,
self.description_sw, description_label)
- self.pkg_sw.add(self.pkgtreeview)
+ self.pkg_sw.add(self.pkgtreeview) # pylint: disable=no-member
self.pkg_sw.show()
self.table.attach(category_sw, 0, 2, 0, 12)
self.table.attach(self.pkg_sw, 2, 12, 0, 12)
self.show()
return self.table
- def on_selection_changed(self, selection, description_sw,
- description_label):
+ def on_selection_changed(self, selection, description_sw, description_label): # pylint: disable=unused-argument
+ """Update package description when selection changes."""
model, treeiter = selection.get_selected()
if treeiter is not None:
description_label.set_text(model[treeiter][6])
- self.description_sw.show_all()
+ self.description_sw.show_all() # pylint: disable=no-member
- def hidewindow(self, widget):
+ def hidewindow(self, widget): # pylint: disable=unused-argument
+ """Hide the confirmation window."""
self.confirm_window.hide()
self.cancel_change(None)
self.unlock_ui()
- def delete_event(self, widget):
- # don't delete; hide instead
- self.confirm_window.hide_on_delete()
+ def delete_event(self, widget): # pylint: disable=unused-argument
+ """Handle window deletion events."""
+ self.confirm_window.hide_on_delete() # pylint: disable=no-member
def create_bbox(self):
+ """Create buttons for the confirmation dialog."""
table = Gtk.Table(
n_rows=1, n_columns=2, homogeneous=False, column_spacing=5)
- img = Gtk.Image(icon_name='gtk-cancel')
- Close_button = Gtk.Button(label=_("Cancel"))
- Close_button.set_image(img)
- table.attach(Close_button, 0, 1, 0, 1)
- Close_button.connect("clicked", self.hidewindow)
+ close_button = Gtk.Button(label=_("Cancel"))
+ img = Gtk.Image.new_from_icon_name('gtk-cancel', 1)
+ close_button.set_image(img)
+ table.attach(close_button, 0, 1, 0, 1)
+ close_button.connect("clicked", self.hidewindow)
confirm_button = Gtk.Button(label=_("Confirm"))
table.attach(confirm_button, 1, 2, 0, 1)
confirm_button.connect("clicked", self.apply_change)
return table
- def confirm_packages(self, widget):
+ def confirm_packages(self, widget): # pylint: disable=unused-argument
+ """Show the package confirmation dialog."""
self.apply_button.set_sensitive(False)
self.cancel_button.set_sensitive(False)
self.lock_ui()
self.confirm_window = Gtk.Window()
self.confirm_window.connect("destroy", self.delete_event)
self.confirm_window.set_size_request(600, 300)
- self.confirm_window.set_keep_above(True)
- # self.confirm_window.set_resizable(False)
+ self.confirm_window.set_keep_above(True) # pylint: disable=no-member
self.confirm_window.set_title(_("Confirm software changes"))
- self.confirm_window.set_border_width(0)
- self.confirm_window.set_position(Gtk.WindowPosition.CENTER)
- self.confirm_window.set_default_icon_name('system-software-install')
+ self.confirm_window.set_border_width(0) # pylint: disable=no-member
+ self.confirm_window.set_position(Gtk.WindowPosition.CENTER) # pylint: disable=no-member
box1 = Gtk.VBox(homogeneous=False, spacing=0)
- self.confirm_window.add(box1)
+ self.confirm_window.add(box1) # pylint: disable=no-member
box1.show()
box2 = Gtk.VBox(homogeneous=False, spacing=0)
box2.set_border_width(5)
box1.pack_start(box2, True, True, 0)
box2.show()
- # Title
titletext = _("Software changes to apply")
titlelabel = Gtk.Label(
label=f"{titletext}")
@@ -795,20 +825,19 @@ class TableWindow(Gtk.Window):
box2.pack_start(titlelabel, False, False, 0)
self.tree_store = Gtk.TreeStore(str)
sw = Gtk.ScrolledWindow()
- sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
- sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
- sw.add(self.display(self.store_changes()))
+ sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) # pylint: disable=no-member
+ sw.add(self.display(self.store_changes())) # pylint: disable=no-member
sw.show()
box2.pack_start(sw, True, True, 5)
box2 = Gtk.HBox(homogeneous=False, spacing=5)
box2.set_border_width(5)
box1.pack_start(box2, False, False, 5)
box2.show()
- # Add button
box2.pack_start(self.create_bbox(), True, True, 5)
- self.confirm_window.show_all()
+ self.confirm_window.show_all() # pylint: disable=no-member
def display(self, model):
+ """Display package changes in a tree view."""
self.view = Gtk.TreeView(model=model)
self.renderer = Gtk.CellRendererText()
self.column0 = Gtk.TreeViewColumn(_("Name"), self.renderer, text=0)
@@ -818,9 +847,11 @@ class TableWindow(Gtk.Window):
return self.view
def store_changes(self):
+ """Store package changes for display."""
packages_dictionary = get_pkg_changes_data(
pkg_to_uninstall, pkg_to_install)
self.tree_store.clear()
+ # pylint: disable=global-variable-undefined
global total_num
r_num = 0
u_num = 0
@@ -828,29 +859,25 @@ class TableWindow(Gtk.Window):
ri_num = 0
if bool(packages_dictionary['remove']):
r_num = len(packages_dictionary['remove'])
- message = _('Installed packages to be REMOVED:')
- message += f' {r_num}'
+ message = f"{_('Installed packages to be REMOVED:')} {r_num}"
r_pinter = self.tree_store.append(None, [message])
for line in packages_dictionary['remove']:
self.tree_store.append(r_pinter, [line])
if bool(packages_dictionary['upgrade']):
u_num = len(packages_dictionary['upgrade'])
- message = _('Installed packages to be UPGRADED:')
- message += f' {u_num}'
+ message = f"{_('Installed packages to be UPGRADED:')} {u_num}"
u_pinter = self.tree_store.append(None, [message])
for line in packages_dictionary['upgrade']:
self.tree_store.append(u_pinter, [line])
if bool(packages_dictionary['install']):
i_num = len(packages_dictionary['install'])
- message = _('New packages to be INSTALLED:')
- message += f' {i_num}'
+ message = f"{_('New packages to be INSTALLED:')} {i_num}"
i_pinter = self.tree_store.append(None, [message])
for line in packages_dictionary['install']:
self.tree_store.append(i_pinter, [line])
if bool(packages_dictionary['reinstall']):
ri_num = len(packages_dictionary['reinstall'])
- message = _('Installed packages to be REINSTALL:')
- message += f' {ri_num}'
+ message = f"{_('Installed packages to be REINSTALL:')} {ri_num}"
ri_pinter = self.tree_store.append(None, [message])
for line in packages_dictionary['reinstall']:
self.tree_store.append(ri_pinter, [line])
@@ -858,93 +885,116 @@ class TableWindow(Gtk.Window):
return self.tree_store
-class not_root(Gtk.Window):
+class NotRoot(Gtk.Window): # pylint: disable=missing-class-docstring
def __init__(self):
- Gtk.Window.__init__(self)
+ """Show a dialog indicating root privileges are required."""
+ super().__init__()
self.set_title(_("Software Station"))
self.connect("delete-event", Gtk.main_quit)
self.set_size_request(200, 80)
box1 = Gtk.VBox(homogeneous=False, spacing=0)
- self.add(box1)
+ self.add(box1) # pylint: disable=no-member
box1.show()
label = Gtk.Label(label=_('You need to be root'))
box1.pack_start(label, True, True, 0)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- box1.pack_end(hBox, False, False, 5)
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox.show()
+ box1.pack_end(hbox, False, False, 5)
ok_button = Gtk.Button()
ok_button.set_label(_("OK"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-ok', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-ok', 1)
ok_button.set_image(apply_img)
ok_button.connect("clicked", Gtk.main_quit)
- hBox.pack_end(ok_button, False, False, 5)
- self.show_all()
+ hbox.pack_end(ok_button, False, False, 5)
+ self.show_all() # pylint: disable=no-member
-class confirmation(Gtk.Window):
+class Confirmation(Gtk.Window): # pylint: disable=missing-class-docstring
+ def confirm_passwd(self, widget, user): # pylint: disable=unused-argument,redefined-outer-name
+ """Verify the user's password against the stored SHA-512 hash."""
+ print(f"Verifying password for user: {user}")
+ try:
+ # Read the stored hash from /etc/master.passwd
+ stored_hash = None
+ with open('/etc/master.passwd', 'r') as f:
+ for line in f:
+ if line.startswith(f"{user}:"):
+ stored_hash = line.split(':')[1]
+ print(f"Stored hash prefix: {stored_hash[:10]}...") # Truncate for safety
+ break
+ if not stored_hash or stored_hash in ('x', '*'):
+ print(f"No valid hash found for user {user}")
+ self.hide()
+ self.wrong_password()
+ return
- def confirm_passwd(self, widget, user):
- pwd_hash = pwd.getpwnam(user).pw_passwd
- password = self.passwd.get_text()
- if crypt.crypt(password, pwd_hash) == pwd_hash:
- self.hide()
- TableWindow()
- else:
+ # Verify the input password using SHA-512
+ password = self.passwd.get_text()
+ hashed_input = crypt.crypt(password, stored_hash)
+ if hashed_input == stored_hash:
+ print("Password verified successfully")
+ self.hide()
+ TableWindow()
+ else:
+ print("Password verification failed: incorrect password")
+ self.hide()
+ self.wrong_password()
+ except Exception as e:
+ print(f"Password verification error: {e}")
self.hide()
self.wrong_password()
def wrong_password(self):
+ """Show a dialog indicating an incorrect password."""
window = Gtk.Window()
window.set_title(_("Software Station"))
window.connect("delete-event", Gtk.main_quit)
window.set_size_request(200, 80)
box1 = Gtk.VBox(homogeneous=False, spacing=0)
- window.add(box1)
+ window.add(box1) # pylint: disable=no-member
box1.show()
label = Gtk.Label(label=_('Wrong password'))
box1.pack_start(label, True, True, 0)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- box1.pack_end(hBox, False, False, 5)
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox.show()
+ box1.pack_end(hbox, False, False, 5)
ok_button = Gtk.Button()
ok_button.set_label(_("OK"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-ok', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-ok', 1)
ok_button.set_image(apply_img)
ok_button.connect("clicked", Gtk.main_quit)
- hBox.pack_end(ok_button, False, False, 5)
- window.show_all()
+ hbox.pack_end(ok_button, False, False, 5)
+ window.show_all() # pylint: disable=no-member
- def __init__(self, user):
- Gtk.Window.__init__(self)
+ def __init__(self, user): # pylint: disable=redefined-outer-name
+ """Initialize the password confirmation dialog."""
+ super().__init__()
self.set_title(_("Software Station"))
self.connect("delete-event", Gtk.main_quit)
self.set_size_request(200, 80)
- vBox = Gtk.VBox(homogeneous=False, spacing=0)
- self.add(vBox)
- vBox.show()
- label = _("Confirm password for")
- label = Gtk.Label(label=label + f" {user}")
- vBox.pack_start(label, True, True, 5)
+ vbox = Gtk.VBox(homogeneous=False, spacing=0)
+ self.add(vbox) # pylint: disable=no-member
+ vbox.show()
+ label = f"{_('Confirm password for')} {user}"
+ label_widget = Gtk.Label(label=label)
+ vbox.pack_start(label_widget, True, True, 5)
self.passwd = Gtk.Entry()
self.passwd.set_visibility(False)
self.passwd.connect("activate", self.confirm_passwd, user)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- vBox.pack_start(hBox, False, False, 5)
- hBox.pack_start(self.passwd, True, True, 20)
- hBox = Gtk.HBox(homogeneous=False, spacing=0)
- hBox.show()
- vBox.pack_end(hBox, False, False, 5)
+ hbox = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox.show()
+ vbox.pack_start(hbox, False, False, 5)
+ hbox.pack_start(self.passwd, True, True, 20)
+ hbox2 = Gtk.HBox(homogeneous=False, spacing=0)
+ hbox2.show()
+ vbox.pack_end(hbox2, False, False, 5)
ok_button = Gtk.Button()
ok_button.set_label(_("OK"))
- apply_img = Gtk.Image()
- apply_img.set_from_icon_name('gtk-ok', 1)
+ apply_img = Gtk.Image.new_from_icon_name('gtk-ok', 1)
ok_button.set_image(apply_img)
ok_button.connect("clicked", self.confirm_passwd, user)
- hBox.pack_end(ok_button, False, False, 5)
- self.show_all()
+ hbox2.pack_end(ok_button, False, False, 5)
+ self.show_all() # pylint: disable=no-member
if os.geteuid() == 0:
@@ -952,8 +1002,8 @@ if os.geteuid() == 0:
if user is None:
TableWindow()
else:
- confirmation(user)
+ Confirmation(user)
else:
- not_root()
+ NotRoot()
Gtk.main()
diff --git a/software_station/pkg_data_provider.py b/software_station/pkg_data_provider.py
new file mode 100644
index 0000000..573f0d8
--- /dev/null
+++ b/software_station/pkg_data_provider.py
@@ -0,0 +1,106 @@
+# File: pkg_data_provider.py
+
+"""
+Provides an abstraction layer to search FreeBSD/GhostBSD packages
+using either a local SQLite repository or the 'pkg' binary.
+"""
+
+import subprocess
+import sqlite3
+from typing import List, Union
+
+from software_station.PkgRepoSqlReader import PkgRepoSqlReader, Package
+
+
+class PkgBinaryWrapper:
+ """Fallback class to search packages using the 'pkg' binary tool."""
+
+
+ def __init__(self):
+ pass
+
+
+ def search_packages(self, prefix: str) -> Union[List[Package], str]:
+ """
+ Searches available packages using the `pkg` command-line tool.
+
+ Args:
+ prefix (str): Package name prefix to match.
+
+ Returns:
+ Union[List[Package], str]: List of Package objects or error message.
+ """
+ try:
+ result = subprocess.run(
+ ['pkg', 'query', '-a', '%n %v %e'],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ check=True,
+ text=True,
+ timeout=10
+ )
+ output = result.stdout.strip().splitlines()
+
+ installed = self.get_installed_packages()
+ packages = []
+
+ for line in output:
+ parts = line.split(' ', 2)
+ if len(parts) >= 2 and parts[0].startswith(prefix):
+ name, version = parts[0], parts[1]
+ desc = parts[2] if len(parts) == 3 else ''
+ packages.append(Package(
+ name=name,
+ version=version,
+ description=desc,
+ installed=(name in installed)
+ ))
+ return packages
+ except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired) as e:
+ return f"pkg error: {e}"
+
+
+ def get_installed_packages(self) -> set[str]:
+ """
+ Retrieves installed package names from the local SQLite database.
+
+ Returns:
+ set[str]: A set of installed package names.
+ """
+ try:
+ conn = sqlite3.connect("/var/db/pkg/local.sqlite")
+ cursor = conn.cursor()
+ cursor.execute("SELECT name FROM packages")
+ names = {row[0] for row in cursor.fetchall()}
+ conn.close()
+ return names
+ except sqlite3.DatabaseError:
+ return set()
+
+
+class PkgDataProvider:
+ """
+ Main interface to query package data from the repository or fall back
+ to the pkg binary if the SQLite method fails.
+ """
+
+
+ def __init__(self, repo_name="GhostBSD"):
+ self.repo_reader = PkgRepoSqlReader(repo_name)
+ self.fallback_reader = PkgBinaryWrapper()
+
+
+ def search(self, prefix: str) -> Union[List[Package], str]:
+ """
+ Searches for packages by prefix using repository first, then binary fallback.
+
+ Args:
+ prefix (str): Package name prefix to match.
+
+ Returns:
+ Union[List[Package], str]: A list of matched packages or an error string.
+ """
+ result = self.repo_reader.search_packages(prefix)
+ if isinstance(result, str):
+ return self.fallback_reader.search_packages(prefix)
+ return result
diff --git a/software_station/pkg_repo_sql_reader.py b/software_station/pkg_repo_sql_reader.py
new file mode 100644
index 0000000..375f386
--- /dev/null
+++ b/software_station/pkg_repo_sql_reader.py
@@ -0,0 +1,106 @@
+# File: pkg_repo_sql_reader.py
+
+"""
+Provides a class to query a FreeBSD-style SQLite package repository.
+Used to search available packages and detect installed ones.
+"""
+
+import sqlite3
+from typing import List, Union
+from dataclasses import dataclass
+
+
+@dataclass
+class Package:
+ """
+ Represents a software package entry.
+ """
+ name: str
+ version: str
+ description: str = ""
+ installed: bool = False
+
+
+class PkgRepoSqlReader:
+ """
+ Reads package metadata from the FreeBSD/GhostBSD repository SQLite database.
+ """
+
+ def __init__(self, repo_name: str, base_path: str = "/var/db/pkg/repos"):
+ """
+ Initializes the reader with paths to the repo and local package databases.
+ """
+ self.db_path = f"{base_path}/{repo_name}/db"
+ self.local_db_path = "/var/db/pkg/local.sqlite"
+
+ def is_available(self) -> bool:
+ """
+ Checks whether the SQLite repository database is present and valid.
+
+ Returns:
+ bool: True if the database is found and has a valid SQLite header.
+ """
+ try:
+ with open(self.db_path, "rb") as f:
+ header = f.read(16)
+ return header.startswith(b"SQLite format")
+ except FileNotFoundError:
+ return False
+
+ def get_installed_packages(self) -> set[str]:
+ """
+ Loads the names of installed packages from the local package database.
+
+ Returns:
+ set[str]: A set of installed package names.
+ """
+ try:
+ conn = sqlite3.connect(self.local_db_path)
+ cursor = conn.cursor()
+ cursor.execute("SELECT name FROM packages")
+ installed = {row[0] for row in cursor.fetchall()}
+ conn.close()
+ return installed
+ except (sqlite3.DatabaseError, FileNotFoundError):
+ return set()
+
+ def search_packages(self, prefix: str) -> Union[List[Package], str]:
+ """
+ Searches for packages in the repository that match the given prefix.
+
+ Args:
+ prefix (str): The starting string of the package name.
+
+ Returns:
+ Union[List[Package], str]: A list of matched Package objects or an error message.
+ """
+ if not self.is_available():
+ return f"Database not found or invalid: {self.db_path}"
+
+ try:
+ conn = sqlite3.connect(self.db_path)
+ cursor = conn.cursor()
+ cursor.execute(
+ """
+ SELECT name, version, comment
+ FROM packages
+ WHERE name LIKE ?
+ ORDER BY name ASC
+ """,
+ (prefix + '%',)
+ )
+ results = cursor.fetchall()
+ conn.close()
+
+ installed = self.get_installed_packages()
+ return [
+ Package(
+ name=row[0],
+ version=row[1],
+ description=row[2],
+ installed=(row[0] in installed)
+ )
+ for row in results
+ ]
+ except sqlite3.DatabaseError as e:
+ return f"SQLite error: {e}"
diff --git a/software_station/search_index.py b/software_station/search_index.py
new file mode 100644
index 0000000..4c9a1aa
--- /dev/null
+++ b/software_station/search_index.py
@@ -0,0 +1,37 @@
+# File: search_index.py
+
+import bisect
+from dataclasses import dataclass
+from typing import List, Optional
+
+@dataclass
+class Package:
+ name: str
+ description: str = ""
+ version: str = ""
+ installed: bool = False
+
+class PkgSearchIndex:
+ def __init__(self, packages: List[Package], key: str = 'name'):
+ self.key = key
+ self.sorted_packages = sorted(
+ packages,
+ key=lambda p: getattr(p, key).casefold() if hasattr(p, key) else ""
+ )
+ self.sorted_keys = [
+ getattr(p, key).casefold() if hasattr(p, key) else ""
+ for p in self.sorted_packages
+ ]
+
+ def search_exact(self, value: str) -> Optional[Package]:
+ folded_value = value.casefold()
+ index = bisect.bisect_left(self.sorted_keys, folded_value)
+ if index != len(self.sorted_keys) and self.sorted_keys[index] == folded_value:
+ return self.sorted_packages[index]
+ return None
+
+ def search_prefix(self, prefix: str) -> List[Package]:
+ folded_prefix = prefix.casefold()
+ start = bisect.bisect_left(self.sorted_keys, folded_prefix)
+ end = bisect.bisect_right(self.sorted_keys, folded_prefix + '\uffff')
+ return self.sorted_packages[start:end]