diff --git a/pqcli/__main__.py b/pqcli/__main__.py index 169b10f..264433f 100644 --- a/pqcli/__main__.py +++ b/pqcli/__main__.py @@ -38,6 +38,13 @@ def parse_args() -> argparse.Namespace: help="Use curses user interface (slow, but rich colors output)", ) + parser.add_argument( + "--no-colors", + dest="colors", + action="store_false", + help="Disable color highlighting in curses interface", + ) + parser.add_argument( "--no-config", dest="use_config", diff --git a/pqcli/ui/curses/__init__.py b/pqcli/ui/curses/__init__.py index f1a0f97..3be57c8 100644 --- a/pqcli/ui/curses/__init__.py +++ b/pqcli/ui/curses/__init__.py @@ -9,6 +9,15 @@ from pqcli import lingo from pqcli.config import Class, Race from pqcli.mechanic import Player, Stats, create_player from pqcli.roster import Roster +from pqcli.ui.curses.colors import ( + COLOR_FOCUSED, + COLOR_LOGO, + COLOR_PROGRESSBAR, + COLOR_SCROLLBAR_THUMB, + COLOR_SCROLLBAR_TRACK, + has_colors, + set_colors, +) from pqcli.ui.curses.views import ( BaseView, ChooseCharacterClassView, @@ -38,6 +47,16 @@ class CursesUserInterface(BaseUserInterface): curses.noecho() curses.curs_set(0) + set_colors(args.colors) + if has_colors(): + curses.start_color() + curses.use_default_colors() + curses.init_pair(COLOR_LOGO, 10, -1) + curses.init_pair(COLOR_FOCUSED, -1, 8) + curses.init_pair(COLOR_SCROLLBAR_THUMB, -1, 0) + curses.init_pair(COLOR_SCROLLBAR_TRACK, -1, -1) + curses.init_pair(COLOR_PROGRESSBAR, 0, 9) + def signal_handler(sig: T.Any, frame: T.Any) -> None: if self.args.use_saves: self.roster.save() diff --git a/pqcli/ui/curses/colors.py b/pqcli/ui/curses/colors.py new file mode 100644 index 0000000..9db5a40 --- /dev/null +++ b/pqcli/ui/curses/colors.py @@ -0,0 +1,16 @@ +_HAS_COLORS = False + +COLOR_LOGO = 1 +COLOR_FOCUSED = 2 +COLOR_SCROLLBAR_TRACK = 3 +COLOR_SCROLLBAR_THUMB = 4 +COLOR_PROGRESSBAR = 5 + + +def has_colors() -> bool: + return _HAS_COLORS + + +def set_colors(has_colors: bool) -> None: + global _HAS_COLORS + _HAS_COLORS = has_colors diff --git a/pqcli/ui/curses/views/game_view/__init__.py b/pqcli/ui/curses/views/game_view/__init__.py index 58221a1..20b1fb4 100644 --- a/pqcli/ui/curses/views/game_view/__init__.py +++ b/pqcli/ui/curses/views/game_view/__init__.py @@ -8,11 +8,10 @@ from pqcli.roster import Roster from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.util import KEYS_DOWN, KEYS_LEFT, KEYS_RIGHT, KEYS_UP from pqcli.ui.curses.views.base_view import BaseView -from pqcli.ui.curses.widgets import Scrollable, Widget +from pqcli.ui.curses.widgets import Focusable, Scrollable, Widget from .character_sheet_window import CharacterSheetWindow from .equipment_window import EquipmentWindow -from .focusable import Focusable from .inventory_window import InventoryWindow from .plot_window import PlotWindow from .quest_book_window import QuestBookWindow diff --git a/pqcli/ui/curses/views/game_view/character_sheet_window.py b/pqcli/ui/curses/views/game_view/character_sheet_window.py index 9cc39d4..278b8c7 100644 --- a/pqcli/ui/curses/views/game_view/character_sheet_window.py +++ b/pqcli/ui/curses/views/game_view/character_sheet_window.py @@ -1,8 +1,8 @@ import typing as T from pqcli.mechanic import Player, StatType +from pqcli.ui.curses.widgets import Focusable -from .focusable import Focusable from .progress_bar_window import DataTableProgressBarWindow diff --git a/pqcli/ui/curses/views/game_view/equipment_window.py b/pqcli/ui/curses/views/game_view/equipment_window.py index 4c5bd63..e344c8e 100644 --- a/pqcli/ui/curses/views/game_view/equipment_window.py +++ b/pqcli/ui/curses/views/game_view/equipment_window.py @@ -1,9 +1,7 @@ import typing as T from pqcli.mechanic import EquipmentType, Player -from pqcli.ui.curses.widgets import DataTable, WindowWrapper - -from .focusable import Focusable +from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper class EquipmentWindow(Focusable, WindowWrapper): diff --git a/pqcli/ui/curses/views/game_view/inventory_window.py b/pqcli/ui/curses/views/game_view/inventory_window.py index 86d128b..9416a11 100644 --- a/pqcli/ui/curses/views/game_view/inventory_window.py +++ b/pqcli/ui/curses/views/game_view/inventory_window.py @@ -3,9 +3,13 @@ import typing as T from pqcli.lingo import to_roman from pqcli.mechanic import InventoryItem, Player -from pqcli.ui.curses.widgets import DataTable, ProgressBar, WindowWrapper +from pqcli.ui.curses.widgets import ( + DataTable, + Focusable, + ProgressBar, + WindowWrapper, +) -from .focusable import Focusable from .progress_bar_window import DataTableProgressBarWindow diff --git a/pqcli/ui/curses/views/game_view/plot_window.py b/pqcli/ui/curses/views/game_view/plot_window.py index fc54439..d28a173 100644 --- a/pqcli/ui/curses/views/game_view/plot_window.py +++ b/pqcli/ui/curses/views/game_view/plot_window.py @@ -2,9 +2,8 @@ import typing as T from pqcli.lingo import act_name from pqcli.mechanic import Player -from pqcli.ui.curses.widgets import ListBox, WindowWrapper +from pqcli.ui.curses.widgets import Focusable, ListBox, WindowWrapper -from .focusable import Focusable from .progress_bar_window import ListBoxProgressBarWindow diff --git a/pqcli/ui/curses/views/game_view/progress_bar_window.py b/pqcli/ui/curses/views/game_view/progress_bar_window.py index aec527b..8ac0a10 100644 --- a/pqcli/ui/curses/views/game_view/progress_bar_window.py +++ b/pqcli/ui/curses/views/game_view/progress_bar_window.py @@ -1,9 +1,9 @@ import curses import typing as T -from pqcli.ui.curses.views.game_view.focusable import Focusable from pqcli.ui.curses.widgets import ( DataTable, + Focusable, ListBox, ProgressBar, WindowWrapper, diff --git a/pqcli/ui/curses/views/game_view/quest_book_window.py b/pqcli/ui/curses/views/game_view/quest_book_window.py index df93e99..9022d48 100644 --- a/pqcli/ui/curses/views/game_view/quest_book_window.py +++ b/pqcli/ui/curses/views/game_view/quest_book_window.py @@ -1,9 +1,8 @@ import typing as T from pqcli.mechanic import Player -from pqcli.ui.curses.widgets import ListBox, WindowWrapper +from pqcli.ui.curses.widgets import Focusable, ListBox, WindowWrapper -from .focusable import Focusable from .progress_bar_window import ListBoxProgressBarWindow diff --git a/pqcli/ui/curses/views/game_view/spell_book_window.py b/pqcli/ui/curses/views/game_view/spell_book_window.py index ccf755b..a968191 100644 --- a/pqcli/ui/curses/views/game_view/spell_book_window.py +++ b/pqcli/ui/curses/views/game_view/spell_book_window.py @@ -2,9 +2,7 @@ import typing as T from pqcli.lingo import to_roman from pqcli.mechanic import Player, Spell -from pqcli.ui.curses.widgets import DataTable, WindowWrapper - -from .focusable import Focusable +from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper class SpellBookWindow(Focusable, WindowWrapper): diff --git a/pqcli/ui/curses/views/game_view/task_progress_window.py b/pqcli/ui/curses/views/game_view/task_progress_window.py index 0fdc9bc..5c48d52 100644 --- a/pqcli/ui/curses/views/game_view/task_progress_window.py +++ b/pqcli/ui/curses/views/game_view/task_progress_window.py @@ -1,9 +1,12 @@ import typing as T from pqcli.mechanic import Player -from pqcli.ui.curses.widgets import Label, ProgressBar, WindowWrapper - -from .focusable import Focusable +from pqcli.ui.curses.widgets import ( + Focusable, + Label, + ProgressBar, + WindowWrapper, +) class TaskProgressWindow(Focusable, WindowWrapper): diff --git a/pqcli/ui/curses/views/roster_view.py b/pqcli/ui/curses/views/roster_view.py index 519025a..757bde7 100644 --- a/pqcli/ui/curses/views/roster_view.py +++ b/pqcli/ui/curses/views/roster_view.py @@ -1,8 +1,11 @@ +import curses import typing as T -from ..event_handler import EventHandler -from ..util import Choice -from ..widgets import Menu +from pqcli.ui.curses.colors import COLOR_LOGO, has_colors +from pqcli.ui.curses.event_handler import EventHandler +from pqcli.ui.curses.util import Choice +from pqcli.ui.curses.widgets import Menu + from .base_view import BaseView LOGO = """ @@ -13,6 +16,22 @@ LOGO = """ """.strip() +class MainMenu(Menu): + def __init__( + self, choices: T.List[Choice], scr_height: int, scr_width: int + ) -> None: + super().__init__( + header=LOGO, + choices=choices, + active_choice=0, + scr_height=scr_height, + scr_width=scr_width, + ) + if has_colors(): + for y in range(len(self._header_lines)): + self._pad.chgat(y, 0, curses.color_pair(COLOR_LOGO)) + + class RosterView(BaseView): def __init__(self, screen: T.Any) -> None: super().__init__(screen) @@ -50,12 +69,8 @@ class RosterView(BaseView): self.screen.erase() self.screen.noutrefresh() - self.main_menu = Menu( - header=LOGO, - choices=self.choices, - active_choice=0, - scr_height=scr_height, - scr_width=scr_width, + self.main_menu = MainMenu( + choices=self.choices, scr_height=scr_height, scr_width=scr_width ) self.main_menu.render() diff --git a/pqcli/ui/curses/widgets/__init__.py b/pqcli/ui/curses/widgets/__init__.py index ed56237..53232e6 100644 --- a/pqcli/ui/curses/widgets/__init__.py +++ b/pqcli/ui/curses/widgets/__init__.py @@ -1,5 +1,6 @@ from .base import Widget, WindowWrapper from .data_table import DataTable +from .focusable import Focusable from .label import Label from .list_box import ListBox from .menu import Menu diff --git a/pqcli/ui/curses/views/game_view/focusable.py b/pqcli/ui/curses/widgets/focusable.py similarity index 57% rename from pqcli/ui/curses/views/game_view/focusable.py rename to pqcli/ui/curses/widgets/focusable.py index f8ade7a..1d3640e 100644 --- a/pqcli/ui/curses/views/game_view/focusable.py +++ b/pqcli/ui/curses/widgets/focusable.py @@ -1,9 +1,26 @@ import contextlib +import curses import typing as T +from pqcli.ui.curses.colors import COLOR_FOCUSED, has_colors from pqcli.ui.curses.event_handler import EventHandler +@contextlib.contextmanager +def focus_standout(focused: bool, win: T.Any) -> T.Generator: + if focused: + if has_colors(): + win.attron(curses.color_pair(COLOR_FOCUSED)) + else: + win.standout() + yield + if focused: + if has_colors(): + win.attroff(curses.color_pair(COLOR_FOCUSED)) + else: + win.standend() + + class Focusable: def __init__(self, *args: T.Any, **kwargs: T.Any) -> None: self._focused = False @@ -21,8 +38,5 @@ class Focusable: @contextlib.contextmanager def focus_standout(self, win: T.Any) -> T.Generator: - if self._focused: - win.standout() - yield - if self._focused: - win.standend() + with focus_standout(self._focused, win): + yield diff --git a/pqcli/ui/curses/widgets/menu.py b/pqcli/ui/curses/widgets/menu.py index e016502..9298a16 100644 --- a/pqcli/ui/curses/widgets/menu.py +++ b/pqcli/ui/curses/widgets/menu.py @@ -3,6 +3,7 @@ import curses.ascii import typing as T from pqcli.ui.curses.util import KEYS_DOWN, KEYS_UP +from pqcli.ui.curses.widgets.focusable import focus_standout from ..util import Choice from .base import Widget @@ -73,11 +74,8 @@ class Menu(Widget): for y, choice in enumerate(self._choices): self._pad.move(len(self._header_lines) + 1 + y, 0) - if y == self._active_choice: - self._pad.standout() - self._pad.addstr(choice.desc) - if y == self._active_choice: - self._pad.standend() + with focus_standout(y == self._active_choice, self._pad): + self._pad.addstr(choice.desc) self._pad.refresh( 0, diff --git a/pqcli/ui/curses/widgets/progress_bar.py b/pqcli/ui/curses/widgets/progress_bar.py index 83c543f..a56ab73 100644 --- a/pqcli/ui/curses/widgets/progress_bar.py +++ b/pqcli/ui/curses/widgets/progress_bar.py @@ -3,6 +3,7 @@ import datetime import typing as T from pqcli.lingo import format_timespan +from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors from pqcli.ui.curses.widgets.base import WindowWrapper @@ -47,7 +48,13 @@ class ProgressBar(WindowWrapper): self._win.addnstr(0, x, text, min(len(text), self.getmaxyx()[1] - 1)) x = int(cur_pos * self.getmaxyx()[1] // max_pos) if x > 0: - self._win.chgat(0, 0, curses.A_REVERSE) + self._win.chgat( + 0, + 0, + curses.color_pair(COLOR_PROGRESSBAR) + if has_colors() + else curses.A_REVERSE, + ) if x < self.getmaxyx()[1]: self._win.chgat(0, x, curses.A_NORMAL) self._win.noutrefresh() diff --git a/pqcli/ui/curses/widgets/scrollable.py b/pqcli/ui/curses/widgets/scrollable.py index 1a91e6f..eee7d64 100644 --- a/pqcli/ui/curses/widgets/scrollable.py +++ b/pqcli/ui/curses/widgets/scrollable.py @@ -1,6 +1,12 @@ import curses import typing as T +from pqcli.ui.curses.colors import ( + COLOR_SCROLLBAR_THUMB, + COLOR_SCROLLBAR_TRACK, + has_colors, +) + from .base import WindowWrapper @@ -69,13 +75,25 @@ class Scrollable(WindowWrapper): 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, - curses.A_REVERSE + attr_thumb if thumb_y1 <= win_y <= thumb_y2 - else curses.A_NORMAL, + else attr_track, ) self._win.noutrefresh() diff --git a/pqcli/ui/urwid/widgets/__init__.py b/pqcli/ui/urwid/widgets/__init__.py index cdcc877..f1a6879 100644 --- a/pqcli/ui/urwid/widgets/__init__.py +++ b/pqcli/ui/urwid/widgets/__init__.py @@ -5,4 +5,4 @@ from .data_table import DataTable from .double_line_box import DoubleLineBox from .layout import NColumns, NPile from .read_only_check_box import ReadOnlyCheckBox -from .scrollable import ScrollBar, Scrollable +from .scrollable import Scrollable, ScrollBar