]> git.armaanb.net Git - lightcards.git/blob - lightcards/display.py
Fix sort function
[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(key=lambda x: x.front)
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.starred:
164                         cont = True
165                         break
166
167                 if cont:
168                     self.outer.stack = [
169                         x for x in self.outer.stack if x.starred
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.index = len(self.outer.stack) - 1
176                 self.outer.get_key()
177             elif key == "r":
178                 self.outer.obj.index = 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.index + 1 == len(self.stack):
224             self.obj.index = 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.starred]
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.index / (len(self.stack) - 1) * 100)
246             ).zfill(2)
247
248         # Print yellow if starred
249         if self.current_card().starred:
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.index).zfill(len(str(len(self.stack))))}"
261             f"/{str(len(self.stack))})] ["
262             f"{self.headers[self.current_card().side]} ("
263             f"{str(int(self.current_card().side) + 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.index + 1).zfill(len(str(len(self.stack))))
296         if self.current_card().side == 0:
297             top = num_done + " | " + self.headers[self.current_card().side]
298         else:
299             top = (
300                 num_done
301                 + " | "
302                 + self.headers[self.current_card().side]
303                 + ' | "'
304                 + str(self.current_card().front)
305                 + '"'
306             )
307         header_width = mcols
308         if mcols > 80:
309             header_width = 80
310
311         self.main_win.addstr(
312             0,
313             0,
314             textwrap.shorten(top, width=header_width, placeholder="…"),
315             curses.A_BOLD,
316         )
317
318         # Add horizontal line
319         lin_width = header_width
320         if len(top) < header_width:
321             lin_width = len(top)
322         self.main_win.hline(1, 0, curses.ACS_HLINE, lin_width)
323
324         # Show current side
325         self.main_win.addstr(
326             2,
327             0,
328             textwrap.fill(
329                 self.current_card().get(),
330                 width=self.wrap_width(),
331             ),
332         )
333         self.update_panels()
334         self.disp_bar()
335         self.disp_sidebar()
336         self.win.hline(mlines - 2, 0, 0, mcols)
337
338     def current_card(self):
339         """Get current card object"""
340         return self.stack[self.obj.index]
341
342     def get_key(self):
343         """
344         Display a card and wait for the input.
345         Used as a general way of getting back into the card flow from a menu
346         """
347         self.disp_card()
348         while True:
349             key = self.win.getkey()
350             if key == "q":
351                 self.leave()
352             elif key in ["h", "KEY_LEFT"]:
353                 self.obj.back()
354                 self.current_card().side = 0
355                 self.disp_card()
356             elif key in ["l", "KEY_RIGHT"]:
357                 if self.obj.index + 1 == len(self.stack):
358                     self.menu_obj.disp()
359                 else:
360                     self.obj.forward(self.stack)
361                     self.current_card().side = 0
362                     self.disp_card()
363             elif key in ["j", "k", "KEY_UP", "KEY_DOWN"]:
364                 self.current_card().flip()
365                 self.disp_card()
366             elif key in ["i", "/"]:
367                 self.current_card().toggleStar()
368                 self.disp_card()
369             elif key in ["0", "^", "KEY_HOME"]:
370                 self.obj.index = 0
371                 self.current_card().side = 0
372                 self.disp_card()
373             elif key in ["$", "KEY_END"]:
374                 self.obj.index = len(self.stack) - 1
375                 self.current_card().side = 0
376                 self.disp_card()
377             elif key in ["H", "?"]:
378                 self.help_obj.disp()
379             elif key == "m":
380                 self.menu_obj.disp()
381             elif key == "e":
382                 (self.headers, self.stack) = runner.reparse()
383                 self.get_key()
384
385     def disp_sidebar(self):
386         """Display a sidebar with the starred terms"""
387         (mlines, mcols) = self.win.getmaxyx()
388         left = mcols - 19
389
390         self.win.addstr(
391             0,
392             mcols - 16,
393             "STARRED CARDS",
394             curses.color_pair(3) + curses.A_BOLD,
395         )
396         self.win.vline(0, mcols - 20, 0, mlines - 2)
397         self.win.hline(1, left, 0, mlines)
398
399         nstarred = self.nstarred()
400         if mlines - 5 < len(self.nstarred()):
401             nstarred = self.nstarred()[: mlines - 4]
402         elif mlines - 5 == len(self.nstarred()):
403             nstarred = self.nstarred()[: mlines - 3]
404
405         for _ in nstarred:
406             for i, card in enumerate(nstarred):
407                 term = card.front
408                 if len(term) > 18:
409                     term = term + "…"
410                 self.win.addstr(2 + i, left, term)
411             if not nstarred == self.nstarred():
412                 self.win.addstr(
413                     mlines - 3,
414                     left,
415                     f"({len(self.nstarred()) - len(nstarred)} more)",
416                 )
417                 break
418
419         if len(self.nstarred()) == 0:
420             self.win.addstr(2, left, "None starred")