mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
Python 3.10 is the minimum, so annotations use `X | None`, `list[X]` and friends, enforced by ruff's pyupgrade rules. The remaining typing names are imported directly, with abstract types coming from collections.abc. first() is now generic over the item and default.
117 lines
3.3 KiB
Python
117 lines
3.3 KiB
Python
import curses
|
|
from typing import Any
|
|
|
|
from pqcli.ui.curses.colors import (
|
|
COLOR_SCROLLBAR_THUMB,
|
|
COLOR_SCROLLBAR_TRACK,
|
|
has_colors,
|
|
)
|
|
|
|
from .base import WindowWrapper
|
|
|
|
|
|
class Scrollable(WindowWrapper):
|
|
def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None:
|
|
super().__init__(parent, h, w, y, x)
|
|
self._scroll_pos = 0
|
|
self._pad: Any | None = curses.newpad(1, 1)
|
|
self._items: list[Any] = []
|
|
|
|
def clear(self) -> None:
|
|
self._items = []
|
|
self.scroll_to_item(0)
|
|
|
|
def scroll_page_up(self) -> None:
|
|
self.scroll_to_item(max(0, self._scroll_pos - self.getmaxyx()[0]))
|
|
|
|
def scroll_page_down(self) -> None:
|
|
self.scroll_to_item(
|
|
min(
|
|
len(self._items) - 1,
|
|
self._scroll_pos + self.getmaxyx()[0] * 2 - 1,
|
|
)
|
|
)
|
|
|
|
def scroll_to_item(self, y: int) -> None:
|
|
if not self._items or not self._win:
|
|
return
|
|
if y < 0:
|
|
y %= len(self._items)
|
|
|
|
h = self.getmaxyx()[0]
|
|
delta = 1 if self._scroll_pos < y else -1
|
|
while not self._scroll_pos <= y < self._scroll_pos + h:
|
|
self._scroll_pos += delta
|
|
|
|
def stop(self) -> None:
|
|
super().stop()
|
|
del self._pad
|
|
self._pad = None
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._items)
|
|
|
|
def render(self) -> None:
|
|
if not self._pad or not self._win:
|
|
return
|
|
|
|
if self._items:
|
|
self._pad.resize(
|
|
max(1, len(self._items)), max(1, self.getmaxyx()[1] + 1)
|
|
)
|
|
else:
|
|
self._pad.resize(1, 1)
|
|
|
|
self._win.erase()
|
|
self._win.noutrefresh()
|
|
|
|
self._pad.erase()
|
|
h, w = self.getmaxyx()
|
|
y, x = self.getbegyx()
|
|
|
|
try:
|
|
if self._scroll_pos > 0 or self._scroll_pos + h < len(self._items):
|
|
y1 = self._scroll_pos
|
|
y2 = self._scroll_pos + h
|
|
thumb_y1 = int(y1 * h // len(self._items))
|
|
thumb_y2 = int(y2 * h // len(self._items))
|
|
|
|
attr_thumb = (
|
|
curses.color_pair(COLOR_SCROLLBAR_THUMB)
|
|
if has_colors()
|
|
else curses.A_REVERSE
|
|
)
|
|
attr_track = (
|
|
curses.color_pair(COLOR_SCROLLBAR_TRACK)
|
|
if has_colors()
|
|
else curses.A_NORMAL
|
|
)
|
|
|
|
for win_y in range(h):
|
|
self._win.chgat(
|
|
win_y,
|
|
w - 1,
|
|
(
|
|
attr_thumb
|
|
if thumb_y1 <= win_y <= thumb_y2
|
|
else attr_track
|
|
),
|
|
)
|
|
self._win.noutrefresh()
|
|
|
|
self._render_impl(h, w - 1)
|
|
self._pad.noutrefresh(
|
|
self._scroll_pos, 0, y, x, y + h - 1, x + w - 2
|
|
)
|
|
else:
|
|
self._render_impl(h, w)
|
|
self._pad.noutrefresh(
|
|
self._scroll_pos, 0, y, x, y + h - 1, x + w - 1
|
|
)
|
|
|
|
except curses.error:
|
|
pass
|
|
|
|
def _render_impl(self, h: int, w: int) -> None:
|
|
raise NotImplementedError("not implemented")
|