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

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