feat: add configurable theme with semantic color roles
This commit is contained in:
parent
9a1d620556
commit
f94771c67a
8 changed files with 80 additions and 21 deletions
11
src/IO/UI.py
11
src/IO/UI.py
|
|
@ -14,10 +14,10 @@ class UI:
|
|||
self._app = app
|
||||
|
||||
HEADER_ROWS = 2
|
||||
ERROR_COLOR = 5
|
||||
|
||||
def _get_attr(self, item: any, index: int) -> str:
|
||||
attr = curses.color_pair(item.color_pair)
|
||||
pair = self.app.config.theme.pair_for(item.color_role)
|
||||
attr = curses.color_pair(pair)
|
||||
if index == self.app.navigator.cursor.pos:
|
||||
attr |= curses.A_REVERSE
|
||||
return attr
|
||||
|
|
@ -29,8 +29,9 @@ class UI:
|
|||
|
||||
stdscr.addstr(0, 0, str(navigator.history[-1])[:width - 1])
|
||||
if navigator.error:
|
||||
error_pair = self.app.config.theme.pair_for("error")
|
||||
stdscr.addstr(1, 0, navigator.error[:width - 1],
|
||||
curses.color_pair(self.ERROR_COLOR))
|
||||
curses.color_pair(error_pair))
|
||||
|
||||
items = navigator.items
|
||||
available = max(0, height - self.HEADER_ROWS)
|
||||
|
|
@ -62,8 +63,10 @@ class UI:
|
|||
stdscr.addstr(row, 0, name.ljust(col_width), attr)
|
||||
|
||||
if type(item) is Symlink:
|
||||
target_pair = self.app.config.theme.pair_for(
|
||||
Symlink.target_role(item.point_to))
|
||||
attr_arrow = curses.color_pair(0)
|
||||
attr_target = curses.color_pair(Symlink.target_color(item.point_to))
|
||||
attr_target = curses.color_pair(target_pair)
|
||||
if index == self.app.navigator.cursor.pos:
|
||||
attr_arrow |= curses.A_REVERSE
|
||||
attr_target |= curses.A_REVERSE
|
||||
|
|
|
|||
|
|
@ -23,11 +23,7 @@ class App():
|
|||
curses.curs_set(0)
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
curses.init_pair(1, curses.COLOR_WHITE, -1)
|
||||
curses.init_pair(2, curses.COLOR_BLUE, -1)
|
||||
curses.init_pair(3, curses.COLOR_CYAN, -1)
|
||||
curses.init_pair(4, curses.COLOR_YELLOW, -1)
|
||||
curses.init_pair(5, curses.COLOR_RED, -1)
|
||||
self.config.theme.init_curses()
|
||||
stdscr.clear()
|
||||
while (True):
|
||||
self.navigator.refresh()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import sys
|
|||
import tomllib
|
||||
from pathlib import Path
|
||||
from . import Sort
|
||||
from .Theme import Theme
|
||||
|
||||
DEFAULT_KEYBINDS: dict[str, list[str]] = {
|
||||
"quit": ["q"],
|
||||
|
|
@ -20,10 +21,11 @@ DEFAULT_CURSOR_RESET = "top"
|
|||
|
||||
class Config:
|
||||
def __init__(self, keybinds: dict[str, list[str]], cursor_reset: str,
|
||||
sort_mode: str) -> None:
|
||||
sort_mode: str, theme: Theme) -> None:
|
||||
self._keybinds = keybinds
|
||||
self._cursor_reset = cursor_reset
|
||||
self._sort_mode = sort_mode
|
||||
self._theme = theme
|
||||
self._key_to_action: dict[str, str] = {
|
||||
key: action
|
||||
for action, keys in keybinds.items()
|
||||
|
|
@ -51,7 +53,8 @@ class Config:
|
|||
print(f"pyexplorer: unknown sort mode {sort_mode!r}, "
|
||||
f"falling back to {Sort.DEFAULT_MODE!r}", file=sys.stderr)
|
||||
sort_mode = Sort.DEFAULT_MODE
|
||||
return cls(keybinds, cursor_reset, sort_mode)
|
||||
theme = Theme.load(data)
|
||||
return cls(keybinds, cursor_reset, sort_mode, theme)
|
||||
|
||||
@staticmethod
|
||||
def _default_path() -> Path:
|
||||
|
|
@ -71,3 +74,7 @@ class Config:
|
|||
@property
|
||||
def sort_mode(self) -> str:
|
||||
return self._sort_mode
|
||||
|
||||
@property
|
||||
def theme(self) -> Theme:
|
||||
return self._theme
|
||||
|
|
|
|||
53
src/core/Theme.py
Normal file
53
src/core/Theme.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from __future__ import annotations
|
||||
import curses
|
||||
import sys
|
||||
|
||||
COLOR_NAMES: dict[str, int] = {
|
||||
"black": curses.COLOR_BLACK,
|
||||
"red": curses.COLOR_RED,
|
||||
"green": curses.COLOR_GREEN,
|
||||
"yellow": curses.COLOR_YELLOW,
|
||||
"blue": curses.COLOR_BLUE,
|
||||
"magenta": curses.COLOR_MAGENTA,
|
||||
"cyan": curses.COLOR_CYAN,
|
||||
"white": curses.COLOR_WHITE,
|
||||
}
|
||||
|
||||
ROLES = ["file", "folder", "symlink", "broken_symlink", "error"]
|
||||
|
||||
DEFAULT_COLORS: dict[str, str] = {
|
||||
"file": "white",
|
||||
"folder": "blue",
|
||||
"symlink": "cyan",
|
||||
"broken_symlink": "red",
|
||||
"error": "red",
|
||||
}
|
||||
|
||||
|
||||
class Theme:
|
||||
def __init__(self, colors: dict[str, str]) -> None:
|
||||
self._colors = colors
|
||||
self._pair_for_role = {role: i + 1 for i, role in enumerate(ROLES)}
|
||||
|
||||
@classmethod
|
||||
def load(cls, data: dict) -> Theme:
|
||||
colors = dict(DEFAULT_COLORS)
|
||||
for role, name in data.get("theme", {}).items():
|
||||
if role not in ROLES:
|
||||
print(f"pyexplorer: unknown theme role {role!r}, ignoring",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
if name not in COLOR_NAMES:
|
||||
print(f"pyexplorer: unknown color {name!r} for {role!r}, "
|
||||
f"ignoring", file=sys.stderr)
|
||||
continue
|
||||
colors[role] = name
|
||||
return cls(colors)
|
||||
|
||||
def init_curses(self) -> None:
|
||||
for role in ROLES:
|
||||
curses.init_pair(self._pair_for_role[role],
|
||||
COLOR_NAMES[self._colors[role]], -1)
|
||||
|
||||
def pair_for(self, role: str) -> int:
|
||||
return self._pair_for_role[role]
|
||||
|
|
@ -15,7 +15,7 @@ class AItem(ABC):
|
|||
self._path = path
|
||||
self.update()
|
||||
|
||||
COLOR = 0
|
||||
COLOR_ROLE = "file"
|
||||
|
||||
def update(self) -> None:
|
||||
self._stat = self._path.lstat()
|
||||
|
|
@ -48,5 +48,5 @@ class AItem(ABC):
|
|||
return self._permissions
|
||||
|
||||
@property
|
||||
def color_pair(self) -> int:
|
||||
return self.COLOR
|
||||
def color_role(self) -> str:
|
||||
return self.COLOR_ROLE
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from .AItems import AItem
|
|||
|
||||
|
||||
class File(AItem):
|
||||
COLOR = 1
|
||||
COLOR_ROLE = "file"
|
||||
|
||||
def __init__(self, path: Path) -> None:
|
||||
super().__init__(path)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from .AItems import AItem
|
|||
|
||||
|
||||
class Folder(AItem):
|
||||
COLOR = 2
|
||||
COLOR_ROLE = "folder"
|
||||
|
||||
def __init__(self, path: Path) -> None:
|
||||
super().__init__(path)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from .Folder import Folder
|
|||
|
||||
|
||||
class Symlink(AItem):
|
||||
COLOR = 3
|
||||
COLOR_ROLE = "symlink"
|
||||
|
||||
def __init__(self, path: Path) -> None:
|
||||
super().__init__(path)
|
||||
|
|
@ -18,13 +18,13 @@ class Symlink(AItem):
|
|||
self.point_to.name}"
|
||||
|
||||
@staticmethod
|
||||
def target_color(point_to: Path | None) -> int:
|
||||
def target_role(point_to: Path | None) -> str:
|
||||
if point_to is None:
|
||||
return 5
|
||||
return "broken_symlink"
|
||||
elif point_to.is_dir():
|
||||
return Folder.COLOR
|
||||
return Folder.COLOR_ROLE
|
||||
else:
|
||||
return File.COLOR
|
||||
return File.COLOR_ROLE
|
||||
|
||||
@property
|
||||
def point_to(self) -> Path:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue