]> git.armaanb.net Git - lightcards.git/blob - lightcards/display.py
Restructure menu and help into seperate classes
[lightcards.git] / lightcards / display.py
1 # Display card output and retreive input
2 # Armaan Bhojwani 2021
3
4 import curses
5 import curses.panel
6 from random import shuffle
7 import sys
8 import textwrap
9
10 from . import runner, progress
11
12
13 def panel_create(x, y):
14     """Create popup panels to a certain scale"""
15     win = curses.newwin(x, y)
16     panel = curses.panel.new_panel(win)
17     win.erase()
18     return (win, panel)
19
20
21 class Help:
22     def __init__(self, outer):
23         """Initialize help screen"""
24         self.outer = outer
25         (self.win, self.panel) = panel_create(20, 52)
26         self.panel.top()
27         self.panel.hide()
28         self.win.clear()
29
30         text = [
31             "Welcome to runner. Here are some keybindings",
32             "to get you started:",
33             "",
34             "h, left          previous card",
35             "l, right         next card",
36             "j, k, up, down   flip card",
37             "i, /             star card",
38             "0, ^, home       go to the start of the deck",
39             "$, end           go to the end of the deck",
40             "H, ?             open this screen",
41             "e                open the input file in $EDITOR",
42             "m                open the control menu",
43             "",
44             "More information can be found in the man page, or",
45             "by running `lightcards --help`.",
46             "",
47             "Press [H], or [?] to go back.",
48         ]
49
50         self.win.addstr(
51             1, 1, "LIGHTCARDS HELP", curses.color_pair(1) + curses.A_BOLD
52         )
53         self.win.hline(2, 1, curses.ACS_HLINE, 15)
54
55         for t in enumerate(text):
56             self.win.addstr(t[0] + 3, 1, t[1])
57
58         self.win.box()
59
60     def disp(self):
61         """Display help screen"""
62         (mlines, mcols) = self.outer.win.getmaxyx()
63         self.win.mvwin(int(mlines / 2) - 10, int(mcols / 2) - 26)
64         self.outer.update_panels()
65         self.panel.show()
66
67         while True:
68             key = self.win.getkey()
69             if key == "q":
70                 self.outer.leave()
71             elif key in ["H", "?"]:
72                 self.panel.hide()
73                 self.outer.get_key()
74
75
76 class Menu:
77     def __init__(self, outer):
78         """Initialize the menu with content"""
79         self.outer = outer
80         (self.win, self.panel) = panel_create(17, 44)
81         self.panel.top()
82         self.panel.hide()
83
84         self.win.addstr(
85             1, 1, "LIGHTCARDS MENU", curses.color_pair(1) + curses.A_BOLD
86         )
87         self.win.hline(2, 1, curses.ACS_HLINE, 15)
88         text = [
89             "[y]: reset stack to original state",
90             "[a]: alphabetize stack",
91             "[z]: shuffle stack",
92             "[f]: flip all cards in stack",
93             "[t]: reverse stack order",
94             "[u]: unstar all",
95             "[d]: star all",
96             "[s]: update stack to include starred only",
97             "",
98             "[r]: restart",
99             "[m]: close menu",
100         ]
101
102         for t in enumerate(text):
103             self.win.addstr(t[0] + 3, 1, t[1])
104
105         self.win.box()
106         self.outer.update_panels()
107
108     def menu_print(self, string, err=False):
109         """Print messages on the menu screen"""
110         if err:
111             color = curses.color_pair(2)
112         else:
113             color = curses.color_pair(1)
114
115         for i in range(42):
116             self.win.addch(15, i + 1, " ")
117
118         self.win.addstr(15, 1, string, color)
119         self.outer.update_panels()
120         self.menu_grab()
121
122     def menu_grab(self):
123         """Grab keypresses on the menu screen"""
124         while True:
125             key = self.win.getkey()
126             if key in ["r", "m"]:
127                 self.panel.hide()
128                 self.outer.update_panels()
129                 self.outer.get_key()
130             elif key == "q":
131                 self.outer.leave()
132             elif key == "y":
133                 self.outer.stack = runner.get_orig()[1]
134                 self.menu_print("Stack reset!")
135             elif key == "a":
136                 self.outer.stack.sort()
137                 self.menu_print("Stack alphabetized!")
138             elif key == "u":
139                 [x.unStar() for x in self.outer.stack]
140                 self.menu_print("All unstarred!")
141             elif key == "d":
142                 [x.star() for x in self.outer.stack]
143                 self.menu_print("All starred!")
144             elif key == "t":
145                 self.outer.stack.reverse()
146                 self.menu_print("Stack reversed!")
147             elif key == "z":
148                 shuffle(self.outer.stack)
149                 self.menu_print("Stack shuffled!")
150             elif key == "f":
151                 for x in self.outer.stack:
152                     x.front, x.back = x.back, x.front
153                 (self.outer.headers[0], self.outer.headers[1]) = (
154                     self.outer.headers[1],
155                     self.outer.headers[0],
156                 )
157                 self.menu_print("Cards flipped!")
158             elif key == "s":
159                 # Check if there are any starred cards before proceeding, and
160                 # if not, don't allow to proceed and show an error message
161                 cont = False
162                 for x in self.outer.stack:
163                     if x.getStar():
164                         cont = True
165                         break
166
167                 if cont:
168                     self.outer.stack = [
169                         x for x in self.outer.stack if x.getStar()
170                     ]
171                     self.menu_print("Stars only!")
172                 else:
173                     self.menu_print("ERR: None are starred!", err=True)
174             elif key in ["h", "KEY_LEFT"]:
175                 self.outer.obj.setIdx(len(self.outer.stack) - 1)
176                 self.outer.get_key()
177             elif key == "r":
178                 self.outer.obj.setIdx(0)
179                 self.outer.get_key()
180
181     def disp(self):
182         """
183         Display a menu offering multiple options on how to manipulate the deck
184         and to continue
185         """
186         (mlines, mcols) = self.outer.win.getmaxyx()
187         self.win.mvwin(int(mlines / 2) - 9, int(mcols / 2) - 22)
188         self.panel.show()
189         self.outer.update_panels()
190
191         self.menu_grab()
192
193
194 class Display:
195     def __init__(self, stack, headers, obj):
196         self.stack = stack
197         self.headers = headers
198         self.obj = obj
199
200     def run(self, stdscr):
201         """Set important options that require stdscr before starting"""
202         self.win = stdscr
203         (mlines, mcols) = self.win.getmaxyx()
204         curses.curs_set(0)  # Hide cursor
205         curses.use_default_colors()  # Allow transparency
206         curses.init_pair(1, curses.COLOR_CYAN, -1)
207         curses.init_pair(2, curses.COLOR_RED, -1)
208         curses.init_pair(3, curses.COLOR_YELLOW, -1)
209
210         (self.main_win, self.main_panel) = panel_create(mlines, mcols)
211         self.menu_obj = Menu(self)
212         self.help_obj = Help(self)
213
214         self.get_key()
215
216     def update_panels(self):
217         """Update panel and window contents"""
218         curses.panel.update_panels()
219         self.win.refresh()
220
221     def leave(self):
222         """Pickle stack before quitting"""
223         if self.obj.getIdx() + 1 == len(self.stack):
224             self.obj.setIdx(0)
225
226         progress.dump(self.stack, runner.get_orig()[1])
227         sys.exit(0)
228
229     def nstarred(self):
230         """Get total number of starred cards"""
231         return [card for card in self.stack if card.getStar()]
232
233     def disp_bar(self):
234         """
235         Display the statusbar at the bottom of the screen with progress, star
236         status, and card side.
237         """
238         (mlines, _) = self.win.getmaxyx()
239
240         # Calculate percent done
241         if len(self.stack) <= 1:
242             percent = "100"
243         else:
244             percent = str(
245                 round(self.obj.getIdx() / (len(self.stack) - 1) * 100)
246             ).zfill(2)
247
248         # Print yellow if starred
249         if self.current_card().getStar():
250             star_color = curses.color_pair(3)
251         else:
252             star_color = curses.color_pair(1)
253
254         # Create bar component
255         bar_start = "["
256         bar_middle = self.current_card().printStar()
257         bar_end = (
258             f"] [{len(self.nstarred())}/{str(len(self.stack))} starred] "
259             f"[{percent}% ("
260             f"{str(self.obj.getIdx()).zfill(len(str(len(self.stack))))}"
261             f"/{str(len(self.stack))})] ["
262             f"{self.headers[self.current_card().getSide()]} ("
263             f"{str(int(self.current_card().getSide()) + 1)})]"
264         )
265
266         # Put it all togethor
267         self.win.addstr(mlines - 1, 0, bar_start, curses.color_pair(1))
268         self.win.addstr(mlines - 1, len(bar_start), bar_middle, star_color)
269         self.win.addstr(
270             mlines - 1,
271             len(bar_start + bar_middle),
272             bar_end,
273             curses.color_pair(1),
274         )
275
276     def wrap_width(self):
277         """Calculate the width at which the body text should wrap"""
278         (_, mcols) = self.win.getmaxyx()
279         wrap_width = mcols - 20
280         if wrap_width > 80:
281             wrap_width = 80
282         return wrap_width
283
284     def disp_card(self):
285         """
286         Display the contents of the card.
287         Shows a header, a horizontal line, and the contents of the current
288         side.
289         """
290         (mlines, mcols) = self.win.getmaxyx()
291         self.main_panel.bottom()
292         self.main_win.clear()
293         # If on the back of the card, show the content of the front side in
294         # the header
295         num_done = str(self.obj.getIdx() + 1).zfill(len(str(len(self.stack))))
296         if self.current_card().getSide() == 0:
297             top = (
298                 num_done + " | " + self.headers[self.current_card().getSide()]
299             )
300         else:
301             top = (
302                 num_done
303                 + " | "
304                 + self.headers[self.current_card().getSide()]
305                 + ' | "'
306                 + str(self.current_card().getFront())
307                 + '"'
308             )
309         header_width = mcols
310         if mcols > 80:
311             header_width = 80
312
313         self.main_win.addstr(
314             0,
315             0,
316             textwrap.shorten(top, width=header_width, placeholder="…"),
317             curses.A_BOLD,
318         )
319
320         # Add horizontal line
321         lin_width = header_width
322         if len(top) < header_width:
323             lin_width = len(top)
324         self.main_win.hline(1, 0, curses.ACS_HLINE, lin_width)
325
326         # Show current side
327         self.main_win.addstr(
328             2,
329             0,
330             textwrap.fill(
331                 self.current_card().get(),
332                 width=self.wrap_width(),
333             ),
334         )
335         self.update_panels()
336         self.disp_bar()
337         self.disp_sidebar()
338         self.win.hline(mlines - 2, 0, 0, mcols)
339
340     def current_card(self):
341         """Get current card object"""
342         return self.stack[self.obj.getIdx()]
343
344     def get_key(self):
345         """
346         Display a card and wait for the input.
347         Used as a general way of getting back into the card flow from a menu
348         """
349         self.disp_card()
350         while True:
351             key = self.win.getkey()
352             if key == "q":
353                 self.leave()
354             elif key in ["h", "KEY_LEFT"]:
355                 self.obj.back()
356                 self.current_card().setSide(0)
357                 self.disp_card()
358             elif key in ["l", "KEY_RIGHT"]:
359                 if self.obj.getIdx() + 1 == len(self.stack):
360                     self.menu_obj.disp()
361                 else:
362                     self.obj.forward(self.stack)
363                     self.current_card().setSide(0)
364                     self.disp_card()
365             elif key in ["j", "k", "KEY_UP", "KEY_DOWN"]:
366                 self.current_card().flip()
367                 self.disp_card()
368             elif key in ["i", "/"]:
369                 self.current_card().toggleStar()
370                 self.disp_card()
371             elif key in ["0", "^", "KEY_HOME"]:
372                 self.obj.setIdx(0)
373                 self.current_card().setSide(0)
374                 self.disp_card()
375             elif key in ["$", "KEY_END"]:
376                 self.obj.setIdx(len(self.stack) - 1)
377                 self.current_card().setSide(0)
378                 self.disp_card()
379             elif key in ["H", "?"]:
380                 self.help_obj.disp()
381             elif key == "m":
382                 self.menu_obj.disp()
383             elif key == "e":
384                 (self.headers, self.stack) = runner.reparse()
385                 self.get_key()
386
387     def disp_sidebar(self):
388         """Display a sidebar with the starred terms"""
389         (mlines, mcols) = self.win.getmaxyx()
390         left = mcols - 19
391
392         self.win.addstr(
393             0,
394             mcols - 16,
395             "STARRED CARDS",
396             curses.color_pair(3) + curses.A_BOLD,
397         )
398         self.win.vline(0, mcols - 20, 0, mlines - 2)
399         self.win.hline(1, left, 0, mlines)
400
401         nstarred = self.nstarred()
402         if mlines - 5 < len(self.nstarred()):
403             nstarred = self.nstarred()[: mlines - 4]
404         elif mlines - 5 == len(self.nstarred()):
405             nstarred = self.nstarred()[: mlines - 3]
406
407         for _ in nstarred:
408             for i, card in enumerate(nstarred):
409                 term = card.getFront()
410                 if len(term) > 18:
411                     term = term + "…"
412                 self.win.addstr(2 + i, left, term)
413             if not nstarred == self.nstarred():
414                 self.win.addstr(
415                     mlines - 3,
416                     left,
417                     f"({len(self.nstarred()) - len(nstarred)} more)",
418                 )
419                 break
420
421         if len(self.nstarred()) == 0:
422             self.win.addstr(2, left, "None starred")