Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Software Station - GhostBSD package manager

This package provides themed icon support, desktop entry indexing,
and package management utilities.
"""

__version__ = "2.0"

__all__ = [
"icons",
"desktop_index",
"pkg_desktop_map",
"accessories_map",
]
193 changes: 153 additions & 40 deletions iconlist.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,167 @@
#!/usr/local/bin/python3
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from gi.repository import Gtk
from gi.repository.GdkPixbuf import Pixbuf
import glob
import os
from __future__ import annotations

found = glob.glob('/usr/local/share/icons/mate/24x24/*/*png')
import sys
from typing import Optional, Callable

icons = []
# -------------------------
# Optional legacy fallback
# -------------------------
# If your repository ships embedded XPM icons, keep using them as a last resort.
_legacy_get_pixbuf = None
try:
# If your legacy module exposes a "get_pixbuf(name, size)" or similar,
# adapt it here. If not, this will remain None and simply not be used.
import software_station_xpm as _xpm

for f in found:
base = os.path.basename(f)
icons.append(os.path.splitext(base)[0])
# Try a few common names; bind the first that exists.
if hasattr(_xpm, "get_pixbuf") and callable(_xpm.get_pixbuf):
_legacy_get_pixbuf = _xpm.get_pixbuf # type: ignore
elif hasattr(_xpm, "icon_pixbuf") and callable(_xpm.icon_pixbuf):
_legacy_get_pixbuf = _xpm.icon_pixbuf # type: ignore
# Else: leave as None; we'll skip legacy fallback.
except Exception:
_xpm = None # not fatal

# --------------------------------------
# Themed, thread-safe enhanced helpers
# --------------------------------------
_THEMED_ICONS_AVAILABLE = False
try:
from gi.repository import Gtk, GdkPixbuf # noqa: F401

class IconViewWindow(Gtk.Window):
def test(self, widget, path):
model = widget.get_model()
data = model[path][1]
print(data)
# Our improved, thread-safe, theme-aware implementation lives here:
from software_station.icons import (
init_icon_runtime as _init_icon_runtime,
resolve_label_and_icon_async as _resolve_label_and_icon_async,
resolve_label_and_icon_sync as _resolve_label_and_icon_sync,
)
from software_station.accessories_map import ACCESSORIES_MAP as _ACCESSORIES_MAP

def __init__(self):
_THEMED_ICONS_AVAILABLE = True
except Exception:
# PyGObject or the helper modules not available - themed path disabled.
_ACCESSORIES_MAP = {} # type: ignore

Gtk.Window.__init__(self)
self.set_title("%d icon%c - %s" % (len(icons), '' if len(icons) < 2 else 's', 'usr/local/share/icons/mate/24x24/'))
self.set_default_size(660, 400)

liststore = Gtk.ListStore(Pixbuf, str, str)
iconview = Gtk.IconView.new()
iconview.set_model(liststore)
iconview.set_pixbuf_column(0)
iconview.set_text_column(1)
iconview.connect("item-activated", self.test)
iconview.set_tooltip_column(2)
bsd = 0
for icon in sorted(icons):
def init_icons_runtime() -> None:
"""
Initialize the themed icon runtime (must be called on the GTK main thread).
Safe to call even if themed icons are unavailable (becomes a no-op).
"""
if _THEMED_ICONS_AVAILABLE:
_init_icon_runtime() # type: ignore[misc]


def _category_uses_themed(category: str) -> bool:
# Use the themed path for all categories
return True


def themed_icon_and_label_async(
category: str,
pkg_name: str,
size: int,
on_ready: Callable[[str, Optional["GdkPixbuf.Pixbuf"]], None],
) -> None:
"""
Resolve a label + icon without blocking the UI.
- If themed stack is available and enabled for the category, use it.
- Otherwise, offload legacy_get_pixbuf to a worker thread.
The callback runs on the GTK main thread.
"""
if _THEMED_ICONS_AVAILABLE and _category_uses_themed(category):
_resolve_label_and_icon_async(pkg_name, _ACCESSORIES_MAP, size, on_ready) # type: ignore[misc]
return

# Non-themed path: offload legacy_get_pixbuf to a worker thread
def worker():
pix = None
if _legacy_get_pixbuf:
try:
pixbuf = Gtk.IconTheme.get_default().load_icon(icon, 64, 0)
liststore.append([pixbuf, icon, str(bsd)])
pix = _legacy_get_pixbuf(pkg_name, size) # type: ignore[call-arg]
except Exception:
pix = None
# Call back on main thread
from gi.repository import GLib
GLib.idle_add(on_ready, pkg_name, pix)

import threading
threading.Thread(target=worker, daemon=True).start()


def themed_icon_and_label_sync(
category: str,
pkg_name: str,
size: int = 32,
):
"""
Resolve label + icon synchronously (must run on the GTK main thread).
"""
if _THEMED_ICONS_AVAILABLE and _category_uses_themed(category):
# returns (label, pixbuf)
return _resolve_label_and_icon_sync(pkg_name, _ACCESSORIES_MAP, size=size) # type: ignore[misc]

# Legacy path: label = pkg name; icon via XPM if available.
pix = None
if _legacy_get_pixbuf:
try:
pix = _legacy_get_pixbuf(pkg_name, size) # type: ignore[call-arg]
except Exception:
pix = None
return pkg_name, pix


# -------------------------------------------------------
# Optional convenience for existing callers (compat)
# -------------------------------------------------------
def get_icon_for_package(pkg_name: str, size: int = 32):
"""
Backward-compatible helper for callers that only want an icon pixbuf
for a package name. Attempts themed resolution first (if available
and category policy allows), then falls back to XPM, then None.
"""
# Use themed sync path under 'Accessories' policy; otherwise legacy only.
if _THEMED_ICONS_AVAILABLE and _category_uses_themed("Accessories"):
# Only care about the pixbuf; ignore the label.
try:
_, pix = _resolve_label_and_icon_sync(pkg_name, _ACCESSORIES_MAP, size=size) # type: ignore[misc]
if pix is not None:
return pix
except Exception:
pass

if _legacy_get_pixbuf:
try:
return _legacy_get_pixbuf(pkg_name, size) # type: ignore[call-arg]
except Exception:
return None
return None

except Exception as inst:
print(inst)
bsd += 1

swnd = Gtk.ScrolledWindow()
swnd.add(iconview)
self.add(swnd)
def get_friendly_label(pkg_name: str) -> str:
"""
Best-effort friendly label for a package (falls back to pkg_name).
The themed path provides localized names via desktop index; otherwise
we just return the package name.
"""
if _THEMED_ICONS_AVAILABLE and _category_uses_themed("Accessories"):
try:
label, _ = _resolve_label_and_icon_sync(pkg_name, _ACCESSORIES_MAP, size=16) # type: ignore[misc]
return label or pkg_name
except Exception:
return pkg_name
return pkg_name


win = IconViewWindow()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
__all__ = [
# New themed API
"init_icons_runtime",
"themed_icon_and_label_async",
"themed_icon_and_label_sync",
# Optional compatibility helpers
"get_icon_for_package",
"get_friendly_label",
]
57 changes: 38 additions & 19 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@

import os
import sys
from setuptools import setup
from setuptools import setup, find_packages

import DistUtilsExtra.command.build_extra
import DistUtilsExtra.command.build_i18n
import DistUtilsExtra.command.clean_i18n
# Try to import DistUtilsExtra for i18n support, but make it optional
try:
import DistUtilsExtra.command.build_extra
import DistUtilsExtra.command.build_i18n
import DistUtilsExtra.command.clean_i18n
HAS_DISTUTILS_EXTRA = True
except ImportError:
HAS_DISTUTILS_EXTRA = False
print("Warning: DistUtilsExtra not found. i18n features will be disabled.")

# to update i18n .mo files (and merge .pot file into .po files):
# ,,python setup.py build_i18n -m''
# python setup.py build_i18n -m

for line in open('software-station').readlines():
if (line.startswith('__VERSION__')):
if line.startswith('__VERSION__'):
exec(line.strip())
break
# Silence flake8, __VERSION__ is properly assigned below
else:
__VERSION__ = '2.0'

Expand All @@ -35,33 +40,47 @@ def datafilelist(installbase, sourcebase):

prefix = sys.prefix


# '{prefix}/share/man/man1'.format(prefix=sys.prefix), glob('data/*.1')),

data_files = [
(f'{prefix}/share/applications', ['software-station.desktop']),
(f'{prefix}/etc/sudoers.d', ['sudoers.d/software-station']),
]

data_files.extend(datafilelist(f'{prefix}/share/locale', 'build/mo'))
# Only add locale files if they exist
if os.path.isdir('build/mo'):
data_files.extend(datafilelist(f'{prefix}/share/locale', 'build/mo'))

cmdclass = {
"build": DistUtilsExtra.command.build_extra.build_extra,
"build_i18n": DistUtilsExtra.command.build_i18n.build_i18n,
"clean": DistUtilsExtra.command.clean_i18n.clean_i18n,
}
# Only set up i18n commands if DistUtilsExtra is available
if HAS_DISTUTILS_EXTRA:
cmdclass = {
"build": DistUtilsExtra.command.build_extra.build_extra,
"build_i18n": DistUtilsExtra.command.build_i18n.build_i18n,
"clean": DistUtilsExtra.command.clean_i18n.clean_i18n,
}
else:
cmdclass = {}

setup(
name="software-station",
version=PROGRAM_VERSION,
description="GhostBSD software manager",
license='BSD',
author='Eric Turgeon',
url='https://github/GhostBSD/software-station/',
url='https://github.com/GhostBSD/software-station/',
package_dir={'': '.'},
packages=['software_station'],
package_data={
'software_station': [
'__init__.py',
'icons.py',
'desktop_index.py',
'pkg_desktop_map.py',
'accessories_map.py',
],
},
data_files=data_files,
install_requires=['setuptools'],
py_modules=["software_station_pkg", "software_station_xpm"],
py_modules=["software_station_pkg", "software_station_xpm", "iconlist"],
scripts=['software-station'],
cmdclass=cmdclass
cmdclass=cmdclass,
python_requires='>=3.11',
)
Loading