Compare commits
4 commits
d2b490470f
...
fa3e68b505
Author | SHA1 | Date | |
---|---|---|---|
Izalia Mae | fa3e68b505 | ||
Izalia Mae | c9571dd7e2 | ||
Izalia Mae | d5702a4ba1 | ||
Izalia Mae | 54f6614c36 |
|
@ -37,6 +37,14 @@ from .errors import (
|
|||
|
||||
)
|
||||
|
||||
from .icon_theme import (
|
||||
FileIconTheme,
|
||||
Icon,
|
||||
IconTheme,
|
||||
IconThemes,
|
||||
MemoryIconTheme
|
||||
)
|
||||
|
||||
from .misc import (
|
||||
catch_errors,
|
||||
convert_to_boolean,
|
||||
|
|
|
@ -235,7 +235,8 @@ class HttpStatus(IntEnum):
|
|||
RequestRangeNotSatisfiable = 416
|
||||
ExpectationFailed = 417
|
||||
IAmATeapot = 418
|
||||
TooHighToHandleYourShit = 420
|
||||
EnhanceYourCalm = 420
|
||||
TooHighToHandleYourShit = 420 # this will get removed in the future
|
||||
MisdirectedRequest = 421
|
||||
UnprocessableEntity = 422
|
||||
Locked = 423
|
||||
|
|
|
@ -128,16 +128,16 @@ class Error(Exception):
|
|||
class FileError(Error, metaclass = ErrorMeta):
|
||||
"Raised on errors involving files"
|
||||
|
||||
NotFound = 0
|
||||
NotFound: ErrorCode = 0 # type: ignore[assignment]
|
||||
"Raised when a file or directory could not be found"
|
||||
|
||||
Found = 1
|
||||
Found: ErrorCode = 1 # type: ignore[assignment]
|
||||
"Raised when a file or directory exists when it should not"
|
||||
|
||||
IsDirectory = 2
|
||||
IsDirectory: ErrorCode = 2 # type: ignore[assignment]
|
||||
"Raised when the path is a directory when it should not be"
|
||||
|
||||
IsFile = 3
|
||||
IsFile: ErrorCode = 3 # type: ignore[assignment]
|
||||
"Raised when the path is a file when it should not be"
|
||||
|
||||
|
||||
|
|
242
blib/icon_theme.py
Normal file
242
blib/icon_theme.py
Normal file
|
@ -0,0 +1,242 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from .errors import FileError
|
||||
from .path import File
|
||||
|
||||
|
||||
class Icon(dict[int, Any]):
|
||||
"Represents an icon"
|
||||
|
||||
__slots__ = ("name",)
|
||||
|
||||
|
||||
def __init__(self, name: str, icons: dict[int, Any] | None = None) -> None:
|
||||
"""
|
||||
Create a new ``Icon`` object
|
||||
|
||||
:param name: Name of the icon
|
||||
:param icons: Icon data and their sizes
|
||||
"""
|
||||
|
||||
dict.__init__(self, icons or {})
|
||||
|
||||
self.name: str = name
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
sizes = ",".join(repr(size) for size in self.sizes)
|
||||
return f"Icon({repr(self.name)}, sizes={sizes})"
|
||||
|
||||
|
||||
@property
|
||||
def sizes(self) -> tuple[int, ...]:
|
||||
"Return a tuple of the available icon sizes"
|
||||
|
||||
return tuple(sorted(self.keys()))
|
||||
|
||||
|
||||
@property
|
||||
def small(self) -> Any:
|
||||
"Return a 16x16 icon if available"
|
||||
|
||||
return self[16]
|
||||
|
||||
|
||||
@property
|
||||
def normal(self) -> Any:
|
||||
"Return a 32x32 icon if available"
|
||||
|
||||
return self[32]
|
||||
|
||||
|
||||
@property
|
||||
def large(self) -> Any:
|
||||
"Return a 64x64 icon if available"
|
||||
|
||||
return self[64]
|
||||
|
||||
|
||||
@property
|
||||
def extra_large(self) -> Any:
|
||||
"Return a 128x128 icon if available"
|
||||
|
||||
return self[128]
|
||||
|
||||
|
||||
@property
|
||||
def scalable(self) -> Any:
|
||||
"Return a scalable (usually svg) icon if available"
|
||||
|
||||
return self[0]
|
||||
|
||||
|
||||
class IconTheme(ABC):
|
||||
"Base class for icon themes"
|
||||
|
||||
name: str
|
||||
"Name of the icon theme"
|
||||
|
||||
@abstractmethod
|
||||
def get_icon(self, name: str) -> Icon: ...
|
||||
|
||||
|
||||
class IconThemes(dict[str, IconTheme]):
|
||||
"Represets a collection of icon themes"
|
||||
|
||||
|
||||
def __init__(self, base_path: str = "/usr/share/icons", default: str = "hicolor") -> None:
|
||||
"""
|
||||
Create a new ``IconThemes`` object
|
||||
|
||||
:param base_path: Filesystem path to search for icon themes
|
||||
:param default: Fallback theme to use when searching for icons
|
||||
:raises ValueError: When the default theme cannot be found
|
||||
"""
|
||||
|
||||
self.base_path: File = File(base_path).resolve()
|
||||
"Filesystem path to search for icon themes"
|
||||
|
||||
self.themes: dict[str, IconTheme] = {}
|
||||
"Loaded icon themes"
|
||||
|
||||
self.default: IconTheme = MemoryIconTheme("default")
|
||||
"Fallback icon theme"
|
||||
|
||||
self.load(default)
|
||||
|
||||
|
||||
def get_icon(self, theme: str, name: str) -> Icon:
|
||||
"""
|
||||
Get an icon. If the icon cannot be found in the specified theme, the default theme
|
||||
will get searched.
|
||||
|
||||
:param theme: Icon theme to search
|
||||
:param name: Name of the icon (case-insensitive)
|
||||
:raises KeyError: If the icon cannot be found
|
||||
"""
|
||||
|
||||
try:
|
||||
self.themes[theme].get_icon(name)
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return self.default.get_icon(name)
|
||||
|
||||
|
||||
def load(self, default: str = "hicolor") -> None:
|
||||
"""
|
||||
Load themes from the base path
|
||||
|
||||
:params defualt: Fallback theme to use when searching for icons
|
||||
"""
|
||||
|
||||
themes: dict[str, IconTheme]
|
||||
fallback: IconTheme | None = None
|
||||
|
||||
for path in self.base_path.glob():
|
||||
if path.isdir:
|
||||
themes[path.name] = FileIconTheme(path)
|
||||
|
||||
if path.name.lower() == default.lower():
|
||||
fallback = themes[path.name]
|
||||
|
||||
if fallback is None:
|
||||
raise ValueError(f"Cannot find default theme: {default}")
|
||||
|
||||
self.default = fallback
|
||||
self.themes = themes
|
||||
|
||||
|
||||
class FileIconTheme(IconTheme):
|
||||
"Represents an icon theme in a directory"
|
||||
|
||||
__slots__ = ("path", "name", "default", "_cache")
|
||||
|
||||
|
||||
def __init__(self, base_path: str, name: str | None = None) -> None:
|
||||
"""
|
||||
Create a new ``FileIconTheme`` object
|
||||
|
||||
:param base_path: Path to the icon theme
|
||||
:param name: Name of the icon theme
|
||||
:raises blib.FileError: If the path is not a directory or does not exist
|
||||
"""
|
||||
|
||||
self.path: File = File(base_path).resolve()
|
||||
self.name: str = name or self.path.name
|
||||
self._cache: dict[str, Icon | None] = {}
|
||||
|
||||
if not self.path.isdir:
|
||||
raise FileError.IsFile(self.path)
|
||||
|
||||
if not self.path.exists:
|
||||
raise FileError.NotFound(self.path)
|
||||
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"Clear the cached icon results"
|
||||
|
||||
self._cache.clear()
|
||||
|
||||
|
||||
def get_icon(self, name: str) -> Icon:
|
||||
if name in self._cache:
|
||||
if (cached := self._cache[name]) is None:
|
||||
raise KeyError(name)
|
||||
|
||||
return cached
|
||||
|
||||
icon = Icon(name)
|
||||
|
||||
for path in self.path.glob(recursive = True, ext = ["png", "svg", "svgz"]):
|
||||
if name.lower() == path.stem.lower():
|
||||
size_str = path.split(self.name)[1].split("/")[1]
|
||||
|
||||
if size_str != "scalable":
|
||||
icon[int(size_str.split("x")[0])] = path
|
||||
|
||||
else:
|
||||
icon[0] = path
|
||||
|
||||
if len(icon) == 0:
|
||||
self._cache[name] = None
|
||||
raise KeyError(name)
|
||||
|
||||
self._cache[name] = icon
|
||||
return icon
|
||||
|
||||
|
||||
class MemoryIconTheme(IconTheme):
|
||||
__slots__ = ("name", "icons",)
|
||||
|
||||
def __init__(self, name: str, *icons: Icon) -> None:
|
||||
"""
|
||||
Create a new ``MemoryIconTheme`` object
|
||||
|
||||
:param name: Name of the icon theme
|
||||
:param icons: List of icons to be included in the theme
|
||||
"""
|
||||
|
||||
self.name: str = name
|
||||
self.icons: dict[str, Icon] = {icon.name: icon for icon in icons}
|
||||
|
||||
|
||||
def get_icon(self, name: str) -> Icon:
|
||||
return self.icons[name]
|
||||
|
||||
|
||||
def set_icon(self, name: str, size: int, data: bytes) -> None:
|
||||
"""
|
||||
Add an icon to the theme
|
||||
|
||||
:param name: Name of the icon
|
||||
:param size: Size of the icon. Use ``0`` for scalable.
|
||||
:param data: Raw data of the icon
|
||||
"""
|
||||
|
||||
if name not in self.icons:
|
||||
self.icons[name] = Icon(name)
|
||||
|
||||
self.icons[name][size] = data
|
39
blib/path.py
39
blib/path.py
|
@ -4,7 +4,11 @@ import os
|
|||
import shutil
|
||||
import sys
|
||||
|
||||
from collections.abc import Iterator, Sequence
|
||||
from glob import iglob
|
||||
|
||||
from .enums import FileType, XdgDir
|
||||
from .errors import FileError
|
||||
from .misc import FileSize
|
||||
|
||||
try:
|
||||
|
@ -69,6 +73,11 @@ class Path(str):
|
|||
return self.__class__(os.path.dirname(self))
|
||||
|
||||
|
||||
@property
|
||||
def stem(self) -> str:
|
||||
return self.name.rstrip(self.ext).rstrip(".")
|
||||
|
||||
|
||||
def join(self, *parts: str, normalize: bool = False) -> Self: # type: ignore[override]
|
||||
"""
|
||||
Append a path segment
|
||||
|
@ -208,6 +217,36 @@ class File(Path):
|
|||
return tuple(types)
|
||||
|
||||
|
||||
def glob(self,
|
||||
pattern: str = "**",
|
||||
recursive: bool = False,
|
||||
hidden: bool = False,
|
||||
ext: Sequence[str] | None = None) -> Iterator[File]:
|
||||
"""
|
||||
Iterate through a directory with paths matching a specific pattern
|
||||
|
||||
.. note:: See :class:`glob.iglob` for pattern usage
|
||||
|
||||
:param pattern: Filename pattern to match
|
||||
:param recursive: Whether or not to search through sub-directories
|
||||
:param hidden: List hidden files
|
||||
:param ext: Include only the specified extensions in the result if set
|
||||
:raises FileError: If the path is not a directory or does not exist
|
||||
"""
|
||||
|
||||
if self.isfile:
|
||||
raise FileError.IsFile(self)
|
||||
|
||||
if not self.exists:
|
||||
raise FileError.NotFound(self)
|
||||
|
||||
for path in iglob(pattern, root_dir = self, recursive = recursive, include_hidden = hidden):
|
||||
filepath = self.join(path)
|
||||
|
||||
if ext is None or filepath.ext in ext:
|
||||
yield filepath
|
||||
|
||||
|
||||
def mkdir(self, mode: int = 0o755) -> None:
|
||||
"""
|
||||
Create a directory and all parent directories
|
||||
|
|
22
docs/api/icon_theme.rst
Normal file
22
docs/api/icon_theme.rst
Normal file
|
@ -0,0 +1,22 @@
|
|||
Icon Themes
|
||||
===========
|
||||
|
||||
.. autoclass:: blib.IconThemes
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: blib.IconTheme
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: blib.FileIconTheme
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: blib.MemoryIconTheme
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: blib.Icon
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -5,6 +5,7 @@ subtrees:
|
|||
- file: api/index.rst
|
||||
subtrees:
|
||||
- entries:
|
||||
- file: api/icon_theme.rst
|
||||
- file: api/router.rst
|
||||
- file: api/enums.rst
|
||||
- file: api/errors.rst
|
||||
|
|
Loading…
Reference in a new issue