Gitlab CI - Debugging
[tpg/opendispense2.git] / src / client / menu.c
1 /*
2  * OpenDispense 2 
3  * UCC (University [of WA] Computer Club) Electronic Accounting System
4  * - Dispense Client
5  *
6  * menu.c - ncurses dispense menu
7  *
8  * This file is licenced under the 3-clause BSD Licence. See the file
9  * COPYING for full details.
10  */
11 #include <stdlib.h>
12 #include <ncurses.h>
13 #include <pwd.h>        // getpwuids
14 #include <unistd.h>     // getuid
15 #include "common.h"
16
17 // === CONSTANTS ===
18 #define COLOURPAIR_DEFAULT      0
19 #define COLOURPAIR_CANTBUY      1
20 #define COLOURPAIR_SELECTED     2
21
22 // === PROTOTYPES ===
23  int    ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted);
24 void    PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1, const char *Mid, char Pad2, const char *Right, ...);
25
26 // -------------------
27 // --- NCurses GUI ---
28 // -------------------
29 /**
30  * \brief Render the NCurses UI
31  */
32 int ShowNCursesUI(void)
33 {
34          int    ch;
35          int    i, times;
36          int    xBase, yBase;
37         const int       displayMinWidth = 50;
38         const char      *titleString = "Dispense";
39          int    items_in_view;
40          int    maxItemIndex;
41          int    itemBase = 0;
42          int    currentItem;
43          int    ret = -2;       // -2: Used for marking "no return yet"
44         
45         char    balance_str[5+1+2+1];   // If $9999.99 is too little, something's wrong
46         char    *username;
47         struct passwd *pwd;
48          
49          int    height, width;
50         
51         void _ItemDown(void)
52         {
53                 currentItem ++;
54                 // Skip over spacers
55                 while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
56                         currentItem ++;
57                 
58                 if( currentItem >= maxItemIndex ) {
59                         currentItem = 0;
60                         // Skip over spacers
61                         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
62                                 currentItem ++;
63                 }
64         }
65         
66         void _ItemUp(void)
67         {
68                 currentItem --;
69                 // Skip over spacers
70                 while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
71                         currentItem --;
72                 
73                 if( currentItem < 0 ) {
74                         currentItem = maxItemIndex - 1;
75                         // Skip over spacers
76                         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
77                                 currentItem --;
78                 }
79         }
80
81         // Get Username
82         if( gsEffectiveUser )
83                 username = gsEffectiveUser;
84         else {
85                 pwd = getpwuid( getuid() );
86                 username = pwd->pw_name;
87         }
88         // Get balance
89         snprintf(balance_str, sizeof(balance_str), "$%i.%02i", giUserBalance/100, abs(giUserBalance)%100);
90         
91         // Enter curses mode
92         initscr();
93         start_color();
94         use_default_colors();
95         init_pair(COLOURPAIR_DEFAULT,  -1,  -1);        // Not avaliable
96         init_pair(COLOURPAIR_CANTBUY,  COLOR_BLACK,  -1);       // Not avaliable
97         init_pair(COLOURPAIR_SELECTED, COLOR_GREEN,  -1);       // Selected
98         cbreak(); noecho();
99         
100         // Get max index
101         maxItemIndex = ShowItemAt(0, 0, 0, -1, 0);
102         // Get item count per screen
103         // - 6: randomly chosen (Need at least 3)
104         items_in_view = LINES - 6;
105         if( items_in_view > maxItemIndex )
106                 items_in_view = maxItemIndex;
107         // Get first index
108         currentItem = 0;
109         while( ShowItemAt(0, 0, 0, currentItem, 0) == -1 )
110                 currentItem ++;
111         
112         
113         // Get dimensions
114         height = items_in_view + 3;
115         width = displayMinWidth;
116         
117         // Get positions
118         xBase = COLS/2 - width/2;
119         yBase = LINES/2 - height/2;
120         
121         for( ;; )
122         {
123                 // Header
124                 PrintAlign(yBase, xBase, width, "/", '-', titleString, '-', "\\");
125                 
126                 // Items
127                 for( i = 0; i < items_in_view; i ++ )
128                 {
129                          int    pos = 0;
130                         
131                         move( yBase + 1 + i, xBase );
132                         printw("| ");
133                         
134                         pos += 2;
135                         
136                         // Check for the '...' row
137                         // - Oh god, magic numbers!
138                         if( (i == 0 && itemBase > 0)
139                          || (i == items_in_view - 1 && itemBase < maxItemIndex - items_in_view) )
140                         {
141                                 printw("     ...");     pos += 8;
142                                 times = (width - pos) - 1;
143                                 while(times--)  addch(' ');
144                         }
145                         // Show an item
146                         else {
147                                 ShowItemAt(
148                                         yBase + 1 + i, xBase + pos,     // Position
149                                         (width - pos) - 3,      // Width
150                                         itemBase + i,   // Index
151                                         !!(currentItem == itemBase + i) // Hilighted
152                                         );
153                                 printw("  ");
154                         }
155                         
156                         // Scrollbar (if needed)
157                         if( maxItemIndex > items_in_view ) {
158                                 if( i == 0 ) {
159                                         addch('A');
160                                 }
161                                 else if( i == items_in_view - 1 ) {
162                                         addch('V');
163                                 }
164                                 else {
165                                          int    percentage = itemBase * 100 / (maxItemIndex-items_in_view);
166                                         if( i-1 == percentage*(items_in_view-3)/100 ) {
167                                                 addch('#');
168                                         }
169                                         else {
170                                                 addch('|');
171                                         }
172                                 }
173                         }
174                         else {
175                                 addch('|');
176                         }
177                 }
178                 
179                 // Footer
180                 PrintAlign(yBase+height-2, xBase, width, "\\", '-', "", '-', "/");
181                 
182                 // User line
183                 // - Username, balance, flags
184                 PrintAlign(yBase+height-1, xBase+1, width-2,
185                         username, ' ', balance_str, ' ', gsUserFlags);
186                 PrintAlign(yBase+height, xBase+1, width-2,
187                         "q: Quit", ' ', "Arrows: Select", ' ', "Enter: Buy");
188                 
189                 
190                 // Get input
191                 ch = getch();
192                 
193                 if( ch == '\x1B' ) {
194                         ch = getch();
195                         if( ch == '[' ) {
196                                 ch = getch();
197                                 
198                                 switch(ch)
199                                 {
200                                 case 'B':       _ItemDown();    break;
201                                 case 'A':       _ItemUp();      break;
202                                 }
203                         }
204                         else if( ch == ERR || ch == '\x1b' ) {
205                                 ret = -1;
206                                 break;
207                         }
208                         else {
209                                 fprintf(stderr, "Unknown character 0x%x\n", ch);
210                         }
211                 }
212                 else {
213                         switch(ch)
214                         {
215                         case '\n':
216                                 ret = ShowItemAt(0, 0, 0, currentItem, 0);
217                                 break;
218                         case 'h':       break;
219                         case 'j':       _ItemDown();    break;
220                         case 'k':       _ItemUp();      break;
221                         case 'l':       break;
222                         case 'q':
223                                 ret = -1;       // -1: Return with no dispense
224                                 break;
225                         }
226                         
227                         // Check if the return value was changed
228                         if( ret != -2 ) break;
229                 }
230                 
231                 // Scroll only if needed
232                 if( items_in_view < maxItemIndex )
233                 {
234                         // - If the current item is above the second item shown, and we're not at the top
235                         if( currentItem < itemBase + 2 && itemBase > 0 ) {
236                                 itemBase = currentItem - 2;
237                                 if(itemBase < 0)        itemBase = 0;
238                         }
239                         // - If the current item is below the second item show, and we're not at the bottom
240                         if( currentItem > itemBase + items_in_view - 2 && itemBase + items_in_view < maxItemIndex ) {
241                                 itemBase = currentItem - items_in_view + 2;
242                                 if( itemBase > maxItemIndex - items_in_view )
243                                         itemBase = maxItemIndex - items_in_view;
244                         }
245                 }
246         }
247         
248         
249         // Leave
250         endwin();
251         return ret;
252 }
253
254 #define COKE_LABEL      "Coke Machine"
255 #define NCOKESLOTS      7       // slots 0 -- 6
256 #define EPS_LABEL       "Electronic Payment System"
257
258 /**
259  * \brief Show item \a Index at (\a Col, \a Row)
260  * \return Dispense index of item
261  * \note Part of the NCurses UI
262  */
263 int ShowItemAt(int Row, int Col, int Width, int Index, int bHilighted)
264 {
265         char    *name = NULL;
266          int    price = 0;
267          int    status = -1;
268         
269         switch(giUIMode)
270         {
271         // Standard UI
272         case UI_MODE_STANDARD:
273                 // Bounds check
274                 // Index = -1, request limit
275                 if( Index < 0 || Index >= giNumItems+2 )
276                         return giNumItems+2;
277                 // Drink label
278                 if( Index == 0 )
279                 {
280                         price = 0;
281                         name = COKE_LABEL;
282                         Index = -1;     // -1 indicates a label
283                         break;
284                 }
285                 Index --;
286                 // Drinks 0 - 6
287                 if( Index < NCOKESLOTS )
288                 {
289                         name = gaItems[Index].Desc;
290                         price = gaItems[Index].Price;
291                         status = gaItems[Index].Status;
292                         break;
293                 }
294                 Index -= NCOKESLOTS;
295                 // EPS label
296                 if( Index == 0 )
297                 {
298                         price = 0;
299                         name = EPS_LABEL;
300                         Index = -1;     // -1 indicates a label
301                         break;
302                 }
303                 Index --;
304                 Index += 7;
305                 name = gaItems[Index].Desc;
306                 price = gaItems[Index].Price;
307                 status = gaItems[Index].Status;
308                 break;
309         case UI_MODE_BASIC:
310         case UI_MODE_DRINKSONLY:
311         case UI_MODE_ALL:
312         default:
313                 return -1;
314         }
315         
316         // Width = 0, don't print
317         if( Width > 0 )
318         {
319                 // 4 preceding, 5 price
320                 int nameWidth = Width - 4 - snprintf(NULL, 0, " %4i", price);
321                 move( Row, Col );
322                 
323                 if( Index >= 0 )
324                 {
325                         // Show hilight and status
326                         switch( status )
327                         {
328                         case 0:
329                                 if( bHilighted ) {
330                                         color_set( COLOURPAIR_SELECTED, NULL );
331                                         printw("->  ");
332                                 }
333                                 else if( price > giUserBalance ) {
334                                         attrset(A_BOLD);
335                                         color_set( COLOURPAIR_CANTBUY, NULL );
336                                         printw("    ");
337                                 }
338                                 else {
339                                         color_set( 0, NULL );
340                                         printw("    ");
341                                 }
342                                 break;
343                         case 1:
344                                 attrset(A_BOLD);
345                                 color_set( COLOURPAIR_CANTBUY, NULL );
346                                 printw("SLD ");
347                                 break;
348                         
349                         default:
350                         case -1:
351                                 color_set( COLOURPAIR_CANTBUY, NULL );
352                                 printw("ERR ");
353                                 break;
354                         }
355                         
356                         printw("%-*.*s", nameWidth, nameWidth, name);
357                 
358                         printw(" %4i", price);
359                         color_set(0, NULL);
360                         attrset(A_NORMAL);
361                 }
362                 else
363                 {
364                         printw("-- %-*.*s ", Width-4, Width-4, name);
365                 }
366         }
367         
368         // If the item isn't availiable for sale, return -1 (so it's skipped)
369         if( status > 0 || (price > giUserBalance && gbDisallowSelectWithoutBalance) )
370                 Index = -2;
371         
372         return Index;
373 }
374
375 /**
376  * \brief Print a three-part string at the specified position (formatted)
377  * \note NCurses UI Helper
378  * 
379  * Prints \a Left on the left of the area, \a Right on the righthand side
380  * and \a Mid in the middle of the area. These are padded with \a Pad1
381  * between \a Left and \a Mid, and \a Pad2 between \a Mid and \a Right.
382  * 
383  * ::printf style format codes are allowed in \a Left, \a Mid and \a Right,
384  * and the arguments to these are read in that order.
385  */
386 void PrintAlign(int Row, int Col, int Width, const char *Left, char Pad1,
387         const char *Mid, char Pad2, const char *Right, ...)
388 {
389          int    lLen, mLen, rLen;
390          int    times;
391         
392         va_list args;
393         
394         // Get the length of the strings
395         va_start(args, Right);
396         lLen = vsnprintf(NULL, 0, Left, args);
397         mLen = vsnprintf(NULL, 0, Mid, args);
398         rLen = vsnprintf(NULL, 0, Right, args);
399         va_end(args);
400         
401         // Sanity check
402         if( lLen + mLen/2 > Width/2 || mLen/2 + rLen > Width/2 ) {
403                 return ;        // TODO: What to do?
404         }
405         
406         move(Row, Col);
407         
408         // Render strings
409         va_start(args, Right);
410         // - Left
411         {
412                 char    tmp[lLen+1];
413                 vsnprintf(tmp, lLen+1, Left, args);
414                 addstr(tmp);
415         }
416         // - Left padding
417         times = (Width - mLen)/2 - lLen;
418         while(times--)  addch(Pad1);
419         // - Middle
420         {
421                 char    tmp[mLen+1];
422                 vsnprintf(tmp, mLen+1, Mid, args);
423                 addstr(tmp);
424         }
425         // - Right Padding
426         times = (Width - mLen)/2 - rLen;
427         if( (Width - mLen) % 2 )        times ++;
428         while(times--)  addch(Pad2);
429         // - Right
430         {
431                 char    tmp[rLen+1];
432                 vsnprintf(tmp, rLen+1, Right, args);
433                 addstr(tmp);
434         }
435 }
436

UCC git Repository :: git.ucc.asn.au