1#
2# BitBake Graphical ncurses-based Dependency Explorer
3#   * Based on the GTK implementation
4#   * Intended to run on any Linux host
5#
6# Copyright (C) 2007        Ross Burton
7# Copyright (C) 2007 - 2008 Richard Purdie
8# Copyright (C) 2022 - 2024 David Reyna
9#
10# SPDX-License-Identifier: GPL-2.0-only
11#
12
13#
14# Execution example:
15#   $ bitbake -g -u taskexp_ncurses zlib acl
16#
17# Self-test example (executes a script of GUI actions):
18#   $ TASK_EXP_UNIT_TEST=1 bitbake -g -u taskexp_ncurses zlib acl
19#   ...
20#   $ echo $?
21#   0
22#   $ TASK_EXP_UNIT_TEST=1 bitbake -g -u taskexp_ncurses zlib acl foo
23#   ERROR: Nothing PROVIDES 'foo'. Close matches:
24#   ofono
25#   $ echo $?
26#   1
27#
28# Self-test with no terminal example (only tests dependency fetch from bitbake):
29#   $ TASK_EXP_UNIT_TEST_NOTERM=1 bitbake -g -u taskexp_ncurses quilt
30#   $ echo $?
31#   0
32#
33# Features:
34# * Ncurses is used for the presentation layer. Only the 'curses'
35#   library is used (none of the extension libraries), plus only
36#   one main screen is used (no sub-windows)
37# * Uses the 'generateDepTreeEvent' bitbake event to fetch the
38#   dynamic dependency data based on passed recipes
39# * Computes and provides reverse dependencies
40# * Supports task sorting on:
41#   (a) Task dependency order within each recipe
42#   (b) Pure alphabetical order
43#   (c) Provisions for third sort order (bitbake order?)
44# * The 'Filter' does a "*string*" wildcard filter on tasks in the
45#   main window, dynamically re-ordering and re-centering the content
46# * A 'Print' function exports the selected task or its whole recipe
47#   task set to the default file "taskdep.txt"
48# * Supports a progress bar for bitbake loads and file printing
49# * Line art for box drawing supported, ASCII art an alernative
50# * No horizontal scrolling support. Selected task's full name
51#   shown in bottom bar
52# * Dynamically catches terminals that are (or become) too small
53# * Exception to insure return to normal terminal on errors
54# * Debugging support, self test option
55#
56
57import sys
58import traceback
59import curses
60import re
61import time
62
63# Bitbake server support
64import threading
65from xmlrpc import client
66import bb
67import bb.event
68
69# Dependency indexes (depends_model)
70(TYPE_DEP, TYPE_RDEP) = (0, 1)
71DEPENDS_TYPE = 0
72DEPENDS_TASK = 1
73DEPENDS_DEPS = 2
74# Task indexes (task_list)
75TASK_NAME = 0
76TASK_PRIMARY = 1
77TASK_SORT_ALPHA = 2
78TASK_SORT_DEPS = 3
79TASK_SORT_BITBAKE = 4
80# Sort options (default is SORT_DEPS)
81SORT_ALPHA = 0
82SORT_DEPS = 1
83SORT_BITBAKE_ENABLE = False # NOTE: future sort
84SORT_BITBAKE = 2
85sort_model = SORT_DEPS
86# Print options
87PRINT_MODEL_1 = 0
88PRINT_MODEL_2 = 1
89print_model = PRINT_MODEL_2
90print_file_name = "taskdep_print.log"
91print_file_backup_name = "taskdep_print_backup.log"
92is_printed = False
93is_filter = False
94
95# Standard (and backup) key mappings
96CHAR_NUL = 0            # Used as self-test nop char
97CHAR_BS_H = 8           # Alternate backspace key
98CHAR_TAB = 9
99CHAR_RETURN = 10
100CHAR_ESCAPE = 27
101CHAR_UP = ord('{')      # Used as self-test ASCII char
102CHAR_DOWN = ord('}')    # Used as self-test ASCII char
103
104# Color_pair IDs
105CURSES_NORMAL = 0
106CURSES_HIGHLIGHT = 1
107CURSES_WARNING = 2
108
109
110#################################################
111### Debugging support
112###
113
114verbose = False
115
116# Debug: message display slow-step through display update issues
117def alert(msg,screen):
118    if msg:
119        screen.addstr(0, 10, '[%-4s]' % msg)
120        screen.refresh();
121        curses.napms(2000)
122    else:
123        if do_line_art:
124            for i in range(10, 24):
125                screen.addch(0, i, curses.ACS_HLINE)
126        else:
127            screen.addstr(0, 10, '-' * 14)
128        screen.refresh();
129
130# Debug: display edge conditions on frame movements
131def debug_frame(nbox_ojb):
132    if verbose:
133        nbox_ojb.screen.addstr(0, 50, '[I=%2d,O=%2d,S=%3s,H=%2d,M=%4d]' % (
134            nbox_ojb.cursor_index,
135            nbox_ojb.cursor_offset,
136            nbox_ojb.scroll_offset,
137            nbox_ojb.inside_height,
138            len(nbox_ojb.task_list),
139        ))
140        nbox_ojb.screen.refresh();
141
142#
143# Unit test (assumes that 'quilt-native' is always present)
144#
145
146unit_test = os.environ.get('TASK_EXP_UNIT_TEST')
147unit_test_cmnds=[
148    '# Default selected task in primary box',
149    'tst_selected=<TASK>.do_recipe_qa',
150    '# Default selected task in deps',
151    'tst_entry=<TAB>',
152    'tst_selected=',
153    '# Default selected task in rdeps',
154    'tst_entry=<TAB>',
155    'tst_selected=<TASK>.do_fetch',
156    "# Test 'select' back to primary box",
157    'tst_entry=<CR>',
158    '#tst_entry=<DOWN>',  # optional injected error
159    'tst_selected=<TASK>.do_fetch',
160    '# Check filter',
161    'tst_entry=/uilt-nativ/',
162    'tst_selected=quilt-native.do_recipe_qa',
163    '# Check print',
164    'tst_entry=p',
165    'tst_printed=quilt-native.do_fetch',
166    '#tst_printed=quilt-foo.do_nothing',  # optional injected error
167    '# Done!',
168    'tst_entry=q',
169]
170unit_test_idx=0
171unit_test_command_chars=''
172unit_test_results=[]
173def unit_test_action(active_package):
174    global unit_test_idx
175    global unit_test_command_chars
176    global unit_test_results
177    ret = CHAR_NUL
178    if unit_test_command_chars:
179        ch = unit_test_command_chars[0]
180        unit_test_command_chars = unit_test_command_chars[1:]
181        time.sleep(0.5)
182        ret = ord(ch)
183    else:
184        line = unit_test_cmnds[unit_test_idx]
185        unit_test_idx += 1
186        line = re.sub('#.*', '', line).strip()
187        line = line.replace('<TASK>',active_package.primary[0])
188        line = line.replace('<TAB>','\t').replace('<CR>','\n')
189        line = line.replace('<UP>','{').replace('<DOWN>','}')
190        if not line: line = 'nop=nop'
191        cmnd,value = line.split('=')
192        if cmnd == 'tst_entry':
193            unit_test_command_chars = value
194        elif cmnd == 'tst_selected':
195            active_selected = active_package.get_selected()
196            if active_selected != value:
197                unit_test_results.append("ERROR:SELFTEST:expected '%s' but got '%s' (NOTE:bitbake may have changed)" % (value,active_selected))
198                ret = ord('Q')
199            else:
200                unit_test_results.append("Pass:SELFTEST:found '%s'" % (value))
201        elif cmnd == 'tst_printed':
202            result = os.system('grep %s %s' % (value,print_file_name))
203            if result:
204                unit_test_results.append("ERROR:PRINTTEST:expected '%s' in '%s'" % (value,print_file_name))
205                ret = ord('Q')
206            else:
207                unit_test_results.append("Pass:PRINTTEST:found '%s'" % (value))
208    # Return the action (CHAR_NUL for no action til next round)
209    return(ret)
210
211# Unit test without an interative terminal (e.g. ptest)
212unit_test_noterm = os.environ.get('TASK_EXP_UNIT_TEST_NOTERM')
213
214
215#################################################
216### Window frame rendering
217###
218### By default, use the normal line art. Since
219###  these extended characters are not ASCII, one
220###  must use the ncursus API to render them
221### The alternate ASCII line art set is optionally
222###  available via the 'do_line_art' flag
223
224# By default, render frames using line art
225do_line_art = True
226
227# ASCII render set option
228CHAR_HBAR = '-'
229CHAR_VBAR = '|'
230CHAR_UL_CORNER = '/'
231CHAR_UR_CORNER = '\\'
232CHAR_LL_CORNER = '\\'
233CHAR_LR_CORNER = '/'
234
235# Box frame drawing with line-art
236def line_art_frame(box):
237    x = box.base_x
238    y = box.base_y
239    w = box.width
240    h = box.height + 1
241
242    if do_line_art:
243        for i in range(1, w - 1):
244            box.screen.addch(y, x + i, curses.ACS_HLINE, box.color)
245            box.screen.addch(y + h - 1, x + i, curses.ACS_HLINE, box.color)
246        body_line = "%s" % (' ' * (w - 2))
247        for i in range(1, h - 1):
248            box.screen.addch(y + i, x, curses.ACS_VLINE, box.color)
249            box.screen.addstr(y + i, x + 1, body_line, box.color)
250            box.screen.addch(y + i, x + w - 1, curses.ACS_VLINE, box.color)
251        box.screen.addch(y, x, curses.ACS_ULCORNER, box.color)
252        box.screen.addch(y, x + w - 1, curses.ACS_URCORNER, box.color)
253        box.screen.addch(y + h - 1, x, curses.ACS_LLCORNER, box.color)
254        box.screen.addch(y + h - 1, x + w - 1, curses.ACS_LRCORNER, box.color)
255    else:
256        top_line  = "%s%s%s" % (CHAR_UL_CORNER,CHAR_HBAR * (w - 2),CHAR_UR_CORNER)
257        body_line = "%s%s%s" % (CHAR_VBAR,' ' * (w - 2),CHAR_VBAR)
258        bot_line  = "%s%s%s" % (CHAR_UR_CORNER,CHAR_HBAR * (w - 2),CHAR_UL_CORNER)
259        tag_line  = "%s%s%s" % ('[',CHAR_HBAR * (w - 2),']')
260        # Top bar
261        box.screen.addstr(y, x, top_line)
262        # Middle frame
263        for i in range(1, (h - 1)):
264            box.screen.addstr(y+i, x, body_line)
265        # Bottom bar
266        box.screen.addstr(y + (h - 1), x, bot_line)
267
268# Connect the separate boxes
269def line_art_fixup(box):
270    if do_line_art:
271        box.screen.addch(box.base_y+2, box.base_x, curses.ACS_LTEE, box.color)
272        box.screen.addch(box.base_y+2, box.base_x+box.width-1, curses.ACS_RTEE, box.color)
273
274
275#################################################
276### Ncurses box object : box frame object to display
277### and manage a sub-window's display elements
278### using basic ncurses
279###
280### Supports:
281###   * Frame drawing, content (re)drawing
282###   * Content scrolling via ArrowUp, ArrowDn, PgUp, PgDN,
283###   * Highlighting for active selected item
284###   * Content sorting based on selected sort model
285###
286
287class NBox():
288    def __init__(self, screen, label, primary, base_x, base_y, width, height):
289        # Box description
290        self.screen = screen
291        self.label = label
292        self.primary = primary
293        self.color = curses.color_pair(CURSES_NORMAL) if screen else None
294        # Box boundaries
295        self.base_x = base_x
296        self.base_y = base_y
297        self.width = width
298        self.height = height
299        # Cursor/scroll management
300        self.cursor_enable = False
301        self.cursor_index = 0   # Absolute offset
302        self.cursor_offset = 0  # Frame centric offset
303        self.scroll_offset = 0  # Frame centric offset
304        # Box specific content
305        # Format of each entry is [package_name,is_primary_recipe,alpha_sort_key,deps_sort_key]
306        self.task_list = []
307
308    @property
309    def inside_width(self):
310        return(self.width-2)
311
312    @property
313    def inside_height(self):
314        return(self.height-2)
315
316    # Populate the box's content, include the sort mappings and is_primary flag
317    def task_list_append(self,task_name,dep):
318        task_sort_alpha = task_name
319        task_sort_deps = dep.get_dep_sort(task_name)
320        is_primary = False
321        for primary in self.primary:
322            if task_name.startswith(primary+'.'):
323                is_primary = True
324        if SORT_BITBAKE_ENABLE:
325            task_sort_bitbake = dep.get_bb_sort(task_name)
326            self.task_list.append([task_name,is_primary,task_sort_alpha,task_sort_deps,task_sort_bitbake])
327        else:
328            self.task_list.append([task_name,is_primary,task_sort_alpha,task_sort_deps])
329
330    def reset(self):
331        self.task_list = []
332        self.cursor_index = 0   # Absolute offset
333        self.cursor_offset = 0  # Frame centric offset
334        self.scroll_offset = 0  # Frame centric offset
335
336    # Sort the box's content based on the current sort model
337    def sort(self):
338        if SORT_ALPHA == sort_model:
339            self.task_list.sort(key = lambda x: x[TASK_SORT_ALPHA])
340        elif SORT_DEPS == sort_model:
341            self.task_list.sort(key = lambda x: x[TASK_SORT_DEPS])
342        elif SORT_BITBAKE == sort_model:
343            self.task_list.sort(key = lambda x: x[TASK_SORT_BITBAKE])
344
345    # The target package list (to hightlight), from the command line
346    def set_primary(self,primary):
347        self.primary = primary
348
349    # Draw the box's outside frame
350    def draw_frame(self):
351        line_art_frame(self)
352        # Title
353        self.screen.addstr(self.base_y,
354            (self.base_x + (self.width//2))-((len(self.label)+2)//2),
355            '['+self.label+']')
356        self.screen.refresh()
357
358    # Draw the box's inside text content
359    def redraw(self):
360        task_list_len = len(self.task_list)
361        # Middle frame
362        body_line = "%s" % (' ' * (self.inside_width-1) )
363        for i in range(0,self.inside_height+1):
364            if i < (task_list_len + self.scroll_offset):
365                str_ctl = "%%-%ss" % (self.width-3)
366                # Safety assert
367                if (i + self.scroll_offset) >= task_list_len:
368                    alert("REDRAW:%2d,%4d,%4d" % (i,self.scroll_offset,task_list_len),self.screen)
369                    break
370
371                task_obj = self.task_list[i + self.scroll_offset]
372                task = task_obj[TASK_NAME][:self.inside_width-1]
373                task_primary = task_obj[TASK_PRIMARY]
374
375                if task_primary:
376                    line = str_ctl % task[:self.inside_width-1]
377                    self.screen.addstr(self.base_y+1+i, self.base_x+2, line, curses.A_BOLD)
378                else:
379                    line = str_ctl % task[:self.inside_width-1]
380                    self.screen.addstr(self.base_y+1+i, self.base_x+2, line)
381            else:
382                line = "%s" % (' ' * (self.inside_width-1) )
383                self.screen.addstr(self.base_y+1+i, self.base_x+2, line)
384        self.screen.refresh()
385
386    # Show the current selected task over the bottom of the frame
387    def show_selected(self,selected_task):
388        if not selected_task:
389            selected_task = self.get_selected()
390        tag_line   = "%s%s%s" % ('[',CHAR_HBAR * (self.width-2),']')
391        self.screen.addstr(self.base_y + self.height, self.base_x, tag_line)
392        self.screen.addstr(self.base_y + self.height,
393            (self.base_x + (self.width//2))-((len(selected_task)+2)//2),
394            '['+selected_task+']')
395        self.screen.refresh()
396
397    # Load box with new table of content
398    def update_content(self,task_list):
399        self.task_list = task_list
400        if self.cursor_enable:
401            cursor_update(turn_on=False)
402        self.cursor_index = 0
403        self.cursor_offset = 0
404        self.scroll_offset = 0
405        self.redraw()
406        if self.cursor_enable:
407            cursor_update(turn_on=True)
408
409    # Manage the box's highlighted task and blinking cursor character
410    def cursor_on(self,is_on):
411        self.cursor_enable = is_on
412        self.cursor_update(is_on)
413
414    # High-light the current pointed package, normal for released packages
415    def cursor_update(self,turn_on=True):
416        str_ctl = "%%-%ss" % (self.inside_width-1)
417        try:
418            if len(self.task_list):
419                task_obj = self.task_list[self.cursor_index]
420                task = task_obj[TASK_NAME][:self.inside_width-1]
421                task_primary = task_obj[TASK_PRIMARY]
422                task_font = curses.A_BOLD if task_primary else 0
423            else:
424                task = ''
425                task_font = 0
426        except Exception as e:
427            alert("CURSOR_UPDATE:%s" % (e),self.screen)
428            return
429        if turn_on:
430            self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+1,">", curses.color_pair(CURSES_HIGHLIGHT) | curses.A_BLINK)
431            self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+2,str_ctl % task, curses.color_pair(CURSES_HIGHLIGHT) | task_font)
432        else:
433            self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+1," ")
434            self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+2,str_ctl % task, task_font)
435
436    # Down arrow
437    def line_down(self):
438        if len(self.task_list) <= (self.cursor_index+1):
439            return
440        self.cursor_update(turn_on=False)
441        self.cursor_index += 1
442        self.cursor_offset += 1
443        if self.cursor_offset > (self.inside_height):
444            self.cursor_offset -= 1
445            self.scroll_offset += 1
446            self.redraw()
447        self.cursor_update(turn_on=True)
448        debug_frame(self)
449
450    # Up arrow
451    def line_up(self):
452        if 0 > (self.cursor_index-1):
453            return
454        self.cursor_update(turn_on=False)
455        self.cursor_index -= 1
456        self.cursor_offset -= 1
457        if self.cursor_offset < 0:
458            self.cursor_offset += 1
459            self.scroll_offset -= 1
460            self.redraw()
461        self.cursor_update(turn_on=True)
462        debug_frame(self)
463
464    # Page down
465    def page_down(self):
466        max_task = len(self.task_list)-1
467        if max_task < self.inside_height:
468            return
469        self.cursor_update(turn_on=False)
470        self.cursor_index += 10
471        self.cursor_index = min(self.cursor_index,max_task)
472        self.cursor_offset = min(self.inside_height,self.cursor_index)
473        self.scroll_offset = self.cursor_index - self.cursor_offset
474        self.redraw()
475        self.cursor_update(turn_on=True)
476        debug_frame(self)
477
478    # Page up
479    def page_up(self):
480        max_task = len(self.task_list)-1
481        if max_task < self.inside_height:
482            return
483        self.cursor_update(turn_on=False)
484        self.cursor_index -= 10
485        self.cursor_index = max(self.cursor_index,0)
486        self.cursor_offset = max(0, self.inside_height - (max_task - self.cursor_index))
487        self.scroll_offset = self.cursor_index - self.cursor_offset
488        self.redraw()
489        self.cursor_update(turn_on=True)
490        debug_frame(self)
491
492    # Return the currently selected task name for this box
493    def get_selected(self):
494        if self.task_list:
495            return(self.task_list[self.cursor_index][TASK_NAME])
496        else:
497            return('')
498
499#################################################
500### The helper sub-windows
501###
502
503# Show persistent help at the top of the screen
504class HelpBarView(NBox):
505    def __init__(self, screen, label, primary, base_x, base_y, width, height):
506        super(HelpBarView, self).__init__(screen, label, primary, base_x, base_y, width, height)
507
508    def show_help(self,show):
509        self.screen.addstr(self.base_y,self.base_x, "%s" % (' ' * self.inside_width))
510        if show:
511            help = "Help='?' Filter='/' NextBox=<Tab> Select=<Enter> Print='p','P' Quit='q'"
512            bar_size = self.inside_width - 5 - len(help)
513            self.screen.addstr(self.base_y,self.base_x+((self.inside_width-len(help))//2), help)
514        self.screen.refresh()
515
516# Pop up a detailed Help box
517class HelpBoxView(NBox):
518    def __init__(self, screen, label, primary, base_x, base_y, width, height, dep):
519        super(HelpBoxView, self).__init__(screen, label, primary, base_x, base_y, width, height)
520        self.x_pos = 0
521        self.y_pos = 0
522        self.dep = dep
523
524    # Instantial the pop-up help box
525    def show_help(self,show):
526        self.x_pos = self.base_x + 4
527        self.y_pos = self.base_y + 2
528
529        def add_line(line):
530            if line:
531                self.screen.addstr(self.y_pos,self.x_pos,line)
532            self.y_pos += 1
533
534        # Gather some statisics
535        dep_count = 0
536        rdep_count = 0
537        for task_obj in self.dep.depends_model:
538            if TYPE_DEP == task_obj[DEPENDS_TYPE]:
539                dep_count += 1
540            elif TYPE_RDEP == task_obj[DEPENDS_TYPE]:
541                rdep_count += 1
542
543        self.draw_frame()
544        line_art_fixup(self.dep)
545        add_line("Quit                : 'q' ")
546        add_line("Filter task names   : '/'")
547        add_line("Tab to next box     : <Tab>")
548        add_line("Select a task       : <Enter>")
549        add_line("Print task's deps   : 'p'")
550        add_line("Print recipe's deps : 'P'")
551        add_line(" -> '%s'" % print_file_name)
552        add_line("Sort toggle         : 's'")
553        add_line(" %s Recipe inner-depends order" % ('->' if (SORT_DEPS == sort_model) else '- '))
554        add_line(" %s Alpha-numeric order" % ('->' if (SORT_ALPHA == sort_model) else '- '))
555        if SORT_BITBAKE_ENABLE:
556            add_line(" %s Bitbake order" % ('->' if (TASK_SORT_BITBAKE == sort_model) else '- '))
557        add_line("Alternate backspace : <CTRL-H>")
558        add_line("")
559        add_line("Primary recipes = %s"  % ','.join(self.primary))
560        add_line("Task  count     = %4d" % len(self.dep.pkg_model))
561        add_line("Deps  count     = %4d" % dep_count)
562        add_line("RDeps count     = %4d" % rdep_count)
563        add_line("")
564        self.screen.addstr(self.y_pos,self.x_pos+7,"<Press any key>", curses.color_pair(CURSES_HIGHLIGHT))
565        self.screen.refresh()
566        c = self.screen.getch()
567
568# Show a progress bar
569class ProgressView(NBox):
570    def __init__(self, screen, label, primary, base_x, base_y, width, height):
571        super(ProgressView, self).__init__(screen, label, primary, base_x, base_y, width, height)
572
573    def progress(self,title,current,max):
574        if title:
575            self.label = title
576        else:
577            title = self.label
578        if max <=0: max = 10
579        bar_size = self.width - 7 - len(title)
580        bar_done = int( (float(current)/float(max)) * float(bar_size) )
581        self.screen.addstr(self.base_y,self.base_x, " %s:[%s%s]" % (title,'*' * bar_done,' ' * (bar_size-bar_done)))
582        self.screen.refresh()
583        return(current+1)
584
585    def clear(self):
586        self.screen.addstr(self.base_y,self.base_x, "%s" % (' ' * self.width))
587        self.screen.refresh()
588
589# Implement a task filter bar
590class FilterView(NBox):
591    SEARCH_NOP = 0
592    SEARCH_GO = 1
593    SEARCH_CANCEL = 2
594
595    def __init__(self, screen, label, primary, base_x, base_y, width, height):
596        super(FilterView, self).__init__(screen, label, primary, base_x, base_y, width, height)
597        self.do_show = False
598        self.filter_str = ""
599
600    def clear(self,enable_show=True):
601        self.filter_str = ""
602
603    def show(self,enable_show=True):
604        self.do_show = enable_show
605        if self.do_show:
606            self.screen.addstr(self.base_y,self.base_x, "[ Filter: %-25s ]      '/'=cancel, format='abc'       " % self.filter_str[0:25])
607        else:
608            self.screen.addstr(self.base_y,self.base_x, "%s" % (' ' * self.width))
609        self.screen.refresh()
610
611    def show_prompt(self):
612        self.screen.addstr(self.base_y,self.base_x + 10 + len(self.filter_str), " ")
613        self.screen.addstr(self.base_y,self.base_x + 10 + len(self.filter_str), "")
614
615    # Keys specific to the filter box (start/stop filter keys are in the main loop)
616    def input(self,c,ch):
617        ret = self.SEARCH_GO
618        if c in (curses.KEY_BACKSPACE,CHAR_BS_H):
619            #  Backspace
620            if self.filter_str:
621                self.filter_str = self.filter_str[0:-1]
622                self.show()
623        elif ((ch >= 'a') and (ch <= 'z')) or ((ch >= 'A') and (ch <= 'Z')) or ((ch >= '0') and (ch <= '9')) or (ch in (' ','_','.','-')):
624            # The isalnum() acts strangly with keypad(True), so explicit bounds
625            self.filter_str += ch
626            self.show()
627        else:
628            ret = self.SEARCH_NOP
629        return(ret)
630
631
632#################################################
633### The primary dependency windows
634###
635
636# The main list of package tasks
637class PackageView(NBox):
638    def __init__(self, screen, label, primary, base_x, base_y, width, height):
639        super(PackageView, self).__init__(screen, label, primary, base_x, base_y, width, height)
640
641    # Find and verticaly center a selected task (from filter or from dependent box)
642    # The 'task_filter_str' can be a full or a partial (filter) task name
643    def find(self,task_filter_str):
644        found = False
645        max = self.height-2
646        if not task_filter_str:
647            return(found)
648        for i,task_obj in enumerate(self.task_list):
649            task = task_obj[TASK_NAME]
650            if task.startswith(task_filter_str):
651                self.cursor_on(False)
652                self.cursor_index = i
653
654                # Position selected at vertical center
655                vcenter = self.inside_height // 2
656                if self.cursor_index <= vcenter:
657                    self.scroll_offset = 0
658                    self.cursor_offset = self.cursor_index
659                elif self.cursor_index >= (len(self.task_list) - vcenter - 1):
660                    self.cursor_offset = self.inside_height-1
661                    self.scroll_offset = self.cursor_index - self.cursor_offset
662                else:
663                    self.cursor_offset = vcenter
664                    self.scroll_offset = self.cursor_index - self.cursor_offset
665
666                self.redraw()
667                self.cursor_on(True)
668                found = True
669                break
670        return(found)
671
672# The view of dependent packages
673class PackageDepView(NBox):
674    def __init__(self, screen, label, primary, base_x, base_y, width, height):
675        super(PackageDepView, self).__init__(screen, label, primary, base_x, base_y, width, height)
676
677# The view of reverse-dependent packages
678class PackageReverseDepView(NBox):
679    def __init__(self, screen, label, primary, base_x, base_y, width, height):
680        super(PackageReverseDepView, self).__init__(screen, label, primary, base_x, base_y, width, height)
681
682
683#################################################
684### DepExplorer : The parent frame and object
685###
686
687class DepExplorer(NBox):
688    def __init__(self,screen):
689        title = "Task Dependency Explorer"
690        super(DepExplorer, self).__init__(screen, 'Task Dependency Explorer','',0,0,80,23)
691
692        self.screen = screen
693        self.pkg_model = []
694        self.depends_model = []
695        self.dep_sort_map = {}
696        self.bb_sort_map = {}
697        self.filter_str = ''
698        self.filter_prev = 'deadbeef'
699
700        if self.screen:
701            self.help_bar_view = HelpBarView(screen, "Help",'',1,1,79,1)
702            self.help_box_view = HelpBoxView(screen, "Help",'',0,2,40,20,self)
703            self.progress_view = ProgressView(screen, "Progress",'',2,1,76,1)
704            self.filter_view = FilterView(screen, "Filter",'',2,1,76,1)
705            self.package_view = PackageView(screen, "Package",'alpha', 0,2,40,20)
706            self.dep_view = PackageDepView(screen, "Dependencies",'beta',40,2,40,10)
707            self.reverse_view = PackageReverseDepView(screen, "Dependent Tasks",'gamma',40,13,40,9)
708            self.draw_frames()
709
710    # Draw this main window's frame and all sub-windows
711    def draw_frames(self):
712        self.draw_frame()
713        self.package_view.draw_frame()
714        self.dep_view.draw_frame()
715        self.reverse_view.draw_frame()
716        if is_filter:
717            self.filter_view.show(True)
718            self.filter_view.show_prompt()
719        else:
720            self.help_bar_view.show_help(True)
721        self.package_view.redraw()
722        self.dep_view.redraw()
723        self.reverse_view.redraw()
724        self.show_selected(self.package_view.get_selected())
725        line_art_fixup(self)
726
727    # Parse the bitbake dependency event object
728    def parse(self, depgraph):
729        for task in depgraph["tdepends"]:
730            self.pkg_model.insert(0, task)
731            for depend in depgraph["tdepends"][task]:
732                self.depends_model.insert (0, (TYPE_DEP, task, depend))
733                self.depends_model.insert (0, (TYPE_RDEP, depend, task))
734        if self.screen:
735            self.dep_sort_prep()
736
737    # Prepare the dependency sort order keys
738    # This method creates sort keys per recipe tasks in
739    # the order of each recipe's internal dependecies
740    # Method:
741    #   Filter the tasks in dep order in dep_sort_map = {}
742    #   (a) Find a task that has no dependecies
743    #       Ignore non-recipe specific tasks
744    #   (b) Add it to the sort mapping dict with
745    #       key of "<task_group>_<order>"
746    #   (c) Remove it as a dependency from the other tasks
747    #   (d) Repeat till all tasks are mapped
748    # Use placeholders to insure each sub-dict is instantiated
749    def dep_sort_prep(self):
750        self.progress_view.progress('DepSort',0,4)
751        # Init the task base entries
752        self.progress_view.progress('DepSort',1,4)
753        dep_table = {}
754        bb_index = 0
755        for task in self.pkg_model:
756            # First define the incoming bitbake sort order
757            self.bb_sort_map[task] = "%04d" % (bb_index)
758            bb_index += 1
759            task_group = task[0:task.find('.')]
760            if task_group not in dep_table:
761                dep_table[task_group] = {}
762                dep_table[task_group]['-'] = {}         # Placeholder
763            if task not in dep_table[task_group]:
764                dep_table[task_group][task] = {}
765                dep_table[task_group][task]['-'] = {}   # Placeholder
766        # Add the task dependecy entries
767        self.progress_view.progress('DepSort',2,4)
768        for task_obj in self.depends_model:
769            if task_obj[DEPENDS_TYPE] != TYPE_DEP:
770                continue
771            task = task_obj[DEPENDS_TASK]
772            task_dep = task_obj[DEPENDS_DEPS]
773            task_group = task[0:task.find('.')]
774            # Only track depends within same group
775            if task_dep.startswith(task_group+'.'):
776                dep_table[task_group][task][task_dep] = 1
777        self.progress_view.progress('DepSort',3,4)
778        for task_group in dep_table:
779            dep_index = 0
780            # Whittle down the tasks of each group
781            this_pass = 1
782            do_loop = True
783            while (len(dep_table[task_group]) > 1) and do_loop:
784                this_pass += 1
785                is_change = False
786                delete_list = []
787                for task in dep_table[task_group]:
788                    if '-' == task:
789                        continue
790                    if 1 == len(dep_table[task_group][task]):
791                        is_change = True
792                        # No more deps, so collect this task...
793                        self.dep_sort_map[task] = "%s_%04d" % (task_group,dep_index)
794                        dep_index += 1
795                        # ... remove it from other lists as resolved ...
796                        for dep_task in dep_table[task_group]:
797                            if task in dep_table[task_group][dep_task]:
798                                del dep_table[task_group][dep_task][task]
799                        # ... and remove it from from the task group
800                        delete_list.append(task)
801                for task in delete_list:
802                    del dep_table[task_group][task]
803                if not is_change:
804                    alert("ERROR:DEP_SIEVE_NO_CHANGE:%s" % task_group,self.screen)
805                    do_loop = False
806                    continue
807        self.progress_view.progress('',4,4)
808        self.progress_view.clear()
809        self.help_bar_view.show_help(True)
810        if len(self.dep_sort_map) != len(self.pkg_model):
811            alert("ErrorDepSort:%d/%d" % (len(self.dep_sort_map),len(self.pkg_model)),self.screen)
812
813    # Look up a dep sort order key
814    def get_dep_sort(self,key):
815        if key in self.dep_sort_map:
816            return(self.dep_sort_map[key])
817        else:
818            return(key)
819
820    # Look up a bitbake sort order key
821    def get_bb_sort(self,key):
822        if key in self.bb_sort_map:
823            return(self.bb_sort_map[key])
824        else:
825            return(key)
826
827    # Find the selected package in the main frame, update the dependency frames content accordingly
828    def select(self, package_name, only_update_dependents=False):
829        if not package_name:
830            package_name = self.package_view.get_selected()
831        # alert("SELECT:%s:" % package_name,self.screen)
832
833        if self.filter_str != self.filter_prev:
834            self.package_view.cursor_on(False)
835            # Fill of the main package task list using new filter
836            self.package_view.task_list = []
837            for package in self.pkg_model:
838                if self.filter_str:
839                    if self.filter_str in package:
840                        self.package_view.task_list_append(package,self)
841                else:
842                    self.package_view.task_list_append(package,self)
843            self.package_view.sort()
844            self.filter_prev = self.filter_str
845
846            # Old position is lost, assert new position of previous task (if still filtered in)
847            self.package_view.cursor_index = 0
848            self.package_view.cursor_offset = 0
849            self.package_view.scroll_offset = 0
850            self.package_view.redraw()
851            self.package_view.cursor_on(True)
852
853        # Make sure the selected package is in view, with implicit redraw()
854        if (not only_update_dependents):
855            self.package_view.find(package_name)
856        # In case selected name change (i.e. filter removed previous)
857        package_name = self.package_view.get_selected()
858
859        # Filter the package's dependent list to the dependent view
860        self.dep_view.reset()
861        for package_def in self.depends_model:
862            if (package_def[DEPENDS_TYPE] == TYPE_DEP) and (package_def[DEPENDS_TASK] == package_name):
863                self.dep_view.task_list_append(package_def[DEPENDS_DEPS],self)
864        self.dep_view.sort()
865        self.dep_view.redraw()
866        # Filter the package's dependent list to the reverse dependent view
867        self.reverse_view.reset()
868        for package_def in self.depends_model:
869            if (package_def[DEPENDS_TYPE] == TYPE_RDEP) and (package_def[DEPENDS_TASK] == package_name):
870                self.reverse_view.task_list_append(package_def[DEPENDS_DEPS],self)
871        self.reverse_view.sort()
872        self.reverse_view.redraw()
873        self.show_selected(package_name)
874        self.screen.refresh()
875
876    # The print-to-file method
877    def print_deps(self,whole_group=False):
878        global is_printed
879        # Print the selected deptree(s) to a file
880        if not is_printed:
881            try:
882                # Move to backup any exiting file before first write
883                if os.path.isfile(print_file_name):
884                    os.system('mv -f %s %s' % (print_file_name,print_file_backup_name))
885            except Exception as e:
886                alert(e,self.screen)
887                alert('',self.screen)
888        print_list = []
889        selected_task = self.package_view.get_selected()
890        if not selected_task:
891            return
892        if not whole_group:
893            print_list.append(selected_task)
894        else:
895            # Use the presorted task_group order from 'package_view'
896            task_group = selected_task[0:selected_task.find('.')+1]
897            for task_obj in self.package_view.task_list:
898                task = task_obj[TASK_NAME]
899                if task.startswith(task_group):
900                    print_list.append(task)
901        with open(print_file_name, "a") as fd:
902            print_max = len(print_list)
903            print_count = 1
904            self.progress_view.progress('Write "%s"' % print_file_name,0,print_max)
905            for task in print_list:
906                print_count = self.progress_view.progress('',print_count,print_max)
907                self.select(task)
908                self.screen.refresh();
909                # Utilize the current print output model
910                if print_model == PRINT_MODEL_1:
911                    print("=== Dependendency Snapshot ===",file=fd)
912                    print(" = Package =",file=fd)
913                    print('   '+task,file=fd)
914                    # Fill in the matching dependencies
915                    print(" = Dependencies =",file=fd)
916                    for task_obj in self.dep_view.task_list:
917                        print('   '+ task_obj[TASK_NAME],file=fd)
918                    print(" = Dependent Tasks =",file=fd)
919                    for task_obj in self.reverse_view.task_list:
920                        print('   '+ task_obj[TASK_NAME],file=fd)
921                if print_model == PRINT_MODEL_2:
922                    print("=== Dependendency Snapshot ===",file=fd)
923                    dep_count = len(self.dep_view.task_list) - 1
924                    for i,task_obj in enumerate(self.dep_view.task_list):
925                        print('%s%s' % ("Dep    =" if (i==dep_count) else "        ",task_obj[TASK_NAME]),file=fd)
926                    if not self.dep_view.task_list:
927                        print('Dep    =',file=fd)
928                    print("Package=%s" % task,file=fd)
929                    for i,task_obj in enumerate(self.reverse_view.task_list):
930                        print('%s%s' % ("RDep   =" if (i==0) else "        ",task_obj[TASK_NAME]),file=fd)
931                    if not self.reverse_view.task_list:
932                        print('RDep   =',file=fd)
933                curses.napms(2000)
934                self.progress_view.clear()
935                self.help_bar_view.show_help(True)
936            print('',file=fd)
937        # Restore display to original selected task
938        self.select(selected_task)
939        is_printed = True
940
941#################################################
942### Load bitbake data
943###
944
945def bitbake_load(server, eventHandler, params, dep, curses_off, screen):
946    global bar_len_old
947    bar_len_old = 0
948
949    # Support no screen
950    def progress(msg,count,max):
951        global bar_len_old
952        if screen:
953            dep.progress_view.progress(msg,count,max)
954        else:
955            if msg:
956                if bar_len_old:
957                    bar_len_old = 0
958                    print("\n")
959                print(f"{msg}: ({count} of {max})")
960            else:
961                bar_len = int((count*40)/max)
962                if bar_len_old != bar_len:
963                    print(f"{'*' * (bar_len-bar_len_old)}",end='',flush=True)
964                bar_len_old = bar_len
965    def clear():
966        if screen:
967            dep.progress_view.clear()
968    def clear_curses(screen):
969        if screen:
970            curses_off(screen)
971
972    #
973    # Trigger bitbake "generateDepTreeEvent"
974    #
975
976    cmdline = ''
977    try:
978        params.updateToServer(server, os.environ.copy())
979        params.updateFromServer(server)
980        cmdline = params.parseActions()
981        if not cmdline:
982            clear_curses(screen)
983            print("ERROR: nothing to do.  Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
984            return 1,cmdline
985        if 'msg' in cmdline and cmdline['msg']:
986            clear_curses(screen)
987            print('ERROR: ' + cmdline['msg'])
988            return 1,cmdline
989        cmdline = cmdline['action']
990        if not cmdline or cmdline[0] != "generateDotGraph":
991            clear_curses(screen)
992            print("ERROR: This UI requires the -g option")
993            return 1,cmdline
994        ret, error = server.runCommand(["generateDepTreeEvent", cmdline[1], cmdline[2]])
995        if error:
996            clear_curses(screen)
997            print("ERROR: running command '%s': %s" % (cmdline, error))
998            return 1,cmdline
999        elif not ret:
1000            clear_curses(screen)
1001            print("ERROR: running command '%s': returned %s" % (cmdline, ret))
1002            return 1,cmdline
1003    except client.Fault as x:
1004        clear_curses(screen)
1005        print("ERROR: XMLRPC Fault getting commandline:\n %s" % x)
1006        return 1,cmdline
1007    except Exception as e:
1008        clear_curses(screen)
1009        print("ERROR: in startup:\n %s" % traceback.format_exc())
1010        return 1,cmdline
1011
1012    #
1013    # Receive data from bitbake
1014    #
1015
1016    progress_total = 0
1017    load_bitbake = True
1018    quit = False
1019    try:
1020        while load_bitbake:
1021            try:
1022                event = eventHandler.waitEvent(0.25)
1023                if quit:
1024                    _, error = server.runCommand(["stateForceShutdown"])
1025                    clear_curses(screen)
1026                    if error:
1027                        print('Unable to cleanly stop: %s' % error)
1028                    break
1029
1030                if event is None:
1031                    continue
1032
1033                if isinstance(event, bb.event.CacheLoadStarted):
1034                    progress_total = event.total
1035                    progress('Loading Cache',0,progress_total)
1036                    continue
1037
1038                if isinstance(event, bb.event.CacheLoadProgress):
1039                    x = event.current
1040                    progress('',x,progress_total)
1041                    continue
1042
1043                if isinstance(event, bb.event.CacheLoadCompleted):
1044                    clear()
1045                    progress('Bitbake... ',1,2)
1046                    continue
1047
1048                if isinstance(event, bb.event.ParseStarted):
1049                    progress_total = event.total
1050                    progress('Processing recipes',0,progress_total)
1051                    if progress_total == 0:
1052                        continue
1053
1054                if isinstance(event, bb.event.ParseProgress):
1055                    x = event.current
1056                    progress('',x,progress_total)
1057                    continue
1058
1059                if isinstance(event, bb.event.ParseCompleted):
1060                    progress('Generating dependency tree',0,3)
1061                    continue
1062
1063                if isinstance(event, bb.event.DepTreeGenerated):
1064                    progress('Generating dependency tree',1,3)
1065                    dep.parse(event._depgraph)
1066                    progress('Generating dependency tree',2,3)
1067
1068                if isinstance(event, bb.command.CommandCompleted):
1069                    load_bitbake = False
1070                    progress('Generating dependency tree',3,3)
1071                    clear()
1072                    if screen:
1073                        dep.help_bar_view.show_help(True)
1074                    continue
1075
1076                if isinstance(event, bb.event.NoProvider):
1077                    clear_curses(screen)
1078                    print('ERROR: %s' % event)
1079
1080                    _, error = server.runCommand(["stateShutdown"])
1081                    if error:
1082                        print('ERROR: Unable to cleanly shutdown: %s' % error)
1083                    return 1,cmdline
1084
1085                if isinstance(event, bb.command.CommandFailed):
1086                    clear_curses(screen)
1087                    print('ERROR: ' + str(event))
1088                    return event.exitcode,cmdline
1089
1090                if isinstance(event, bb.command.CommandExit):
1091                    clear_curses(screen)
1092                    return event.exitcode,cmdline
1093
1094                if isinstance(event, bb.cooker.CookerExit):
1095                    break
1096
1097                continue
1098            except EnvironmentError as ioerror:
1099                # ignore interrupted io
1100                if ioerror.args[0] == 4:
1101                    pass
1102            except KeyboardInterrupt:
1103                if shutdown == 2:
1104                    clear_curses(screen)
1105                    print("\nThird Keyboard Interrupt, exit.\n")
1106                    break
1107                if shutdown == 1:
1108                    clear_curses(screen)
1109                    print("\nSecond Keyboard Interrupt, stopping...\n")
1110                    _, error = server.runCommand(["stateForceShutdown"])
1111                    if error:
1112                        print('Unable to cleanly stop: %s' % error)
1113                if shutdown == 0:
1114                    clear_curses(screen)
1115                    print("\nKeyboard Interrupt, closing down...\n")
1116                    _, error = server.runCommand(["stateShutdown"])
1117                    if error:
1118                        print('Unable to cleanly shutdown: %s' % error)
1119                shutdown = shutdown + 1
1120                pass
1121    except Exception as e:
1122        # Safe exit on error
1123        clear_curses(screen)
1124        print("Exception : %s" % e)
1125        print("Exception in startup:\n %s" % traceback.format_exc())
1126
1127    return 0,cmdline
1128
1129#################################################
1130### main
1131###
1132
1133SCREEN_COL_MIN = 83
1134SCREEN_ROW_MIN = 26
1135
1136def main(server, eventHandler, params):
1137    global verbose
1138    global sort_model
1139    global print_model
1140    global is_printed
1141    global is_filter
1142    global screen_too_small
1143
1144    shutdown = 0
1145    screen_too_small = False
1146    quit = False
1147
1148    # Unit test with no terminal?
1149    if unit_test_noterm:
1150        # Load bitbake, test that there is valid dependency data, then exit
1151        screen = None
1152        print("* UNIT TEST:START")
1153        dep = DepExplorer(screen)
1154        print("* UNIT TEST:BITBAKE FETCH")
1155        ret,cmdline = bitbake_load(server, eventHandler, params, dep, None, screen)
1156        if ret:
1157            print("* UNIT TEST: BITBAKE FAILED")
1158            return ret
1159        # Test the acquired dependency data
1160        quilt_native_deps = 0
1161        quilt_native_rdeps = 0
1162        quilt_deps = 0
1163        quilt_rdeps = 0
1164        for i,task_obj in enumerate(dep.depends_model):
1165            if TYPE_DEP == task_obj[0]:
1166                task = task_obj[1]
1167                if task.startswith('quilt-native'):
1168                    quilt_native_deps += 1
1169                elif task.startswith('quilt'):
1170                    quilt_deps += 1
1171            elif TYPE_RDEP == task_obj[0]:
1172                task = task_obj[1]
1173                if task.startswith('quilt-native'):
1174                    quilt_native_rdeps += 1
1175                elif task.startswith('quilt'):
1176                    quilt_rdeps += 1
1177        # Print results
1178        failed = False
1179        if 0 < len(dep.depends_model):
1180             print(f"Pass:Bitbake dependency count = {len(dep.depends_model)}")
1181        else:
1182            failed = True
1183            print(f"FAIL:Bitbake dependency count = 0")
1184        if quilt_native_deps:
1185             print(f"Pass:Quilt-native depends count = {quilt_native_deps}")
1186        else:
1187            failed = True
1188            print(f"FAIL:Quilt-native depends count = 0")
1189        if quilt_native_rdeps:
1190             print(f"Pass:Quilt-native rdepends count = {quilt_native_rdeps}")
1191        else:
1192            failed = True
1193            print(f"FAIL:Quilt-native rdepends count = 0")
1194        if quilt_deps:
1195             print(f"Pass:Quilt depends count = {quilt_deps}")
1196        else:
1197            failed = True
1198            print(f"FAIL:Quilt depends count = 0")
1199        if quilt_rdeps:
1200             print(f"Pass:Quilt rdepends count = {quilt_rdeps}")
1201        else:
1202            failed = True
1203            print(f"FAIL:Quilt rdepends count = 0")
1204        print("* UNIT TEST:STOP")
1205        return failed
1206
1207    # Help method to dynamically test parent window too small
1208    def check_screen_size(dep, active_package):
1209        global screen_too_small
1210        rows, cols = screen.getmaxyx()
1211        if (rows >= SCREEN_ROW_MIN) and (cols >= SCREEN_COL_MIN):
1212            if screen_too_small:
1213                # Now big enough, remove error message and redraw screen
1214                dep.draw_frames()
1215                active_package.cursor_on(True)
1216                screen_too_small = False
1217            return True
1218        # Test on App init
1219        if not dep:
1220            # Do not start this app if screen not big enough
1221            curses.endwin()
1222            print("")
1223            print("ERROR(Taskexp_cli): Mininal screen size is %dx%d" % (SCREEN_COL_MIN,SCREEN_ROW_MIN))
1224            print("Current screen is Cols=%s,Rows=%d" % (cols,rows))
1225            return False
1226        # First time window too small
1227        if not screen_too_small:
1228            active_package.cursor_on(False)
1229            dep.screen.addstr(0,2,'[BIGGER WINDOW PLEASE]', curses.color_pair(CURSES_WARNING) | curses.A_BLINK)
1230            screen_too_small = True
1231        return False
1232
1233    # Helper method to turn off curses mode
1234    def curses_off(screen):
1235        if not screen: return
1236        # Safe error exit
1237        screen.keypad(False)
1238        curses.echo()
1239        curses.curs_set(1)
1240        curses.endwin()
1241
1242        if unit_test_results:
1243            print('\nUnit Test Results:')
1244            for line in unit_test_results:
1245                print(" %s" % line)
1246
1247    #
1248    # Initialize the ncurse environment
1249    #
1250
1251    screen = curses.initscr()
1252    try:
1253        if not check_screen_size(None, None):
1254            exit(1)
1255        try:
1256            curses.start_color()
1257            curses.use_default_colors();
1258            curses.init_pair(0xFF, curses.COLOR_BLACK, curses.COLOR_WHITE);
1259            curses.init_pair(CURSES_NORMAL, curses.COLOR_WHITE, curses.COLOR_BLACK)
1260            curses.init_pair(CURSES_HIGHLIGHT, curses.COLOR_WHITE, curses.COLOR_BLUE)
1261            curses.init_pair(CURSES_WARNING, curses.COLOR_WHITE, curses.COLOR_RED)
1262        except:
1263            curses.endwin()
1264            print("")
1265            print("ERROR(Taskexp_cli): Requires 256 colors. Please use this or the equivalent:")
1266            print("  $ export TERM='xterm-256color'")
1267            exit(1)
1268
1269        screen.keypad(True)
1270        curses.noecho()
1271        curses.curs_set(0)
1272        screen.refresh();
1273    except Exception as e:
1274        # Safe error exit
1275        curses_off(screen)
1276        print("Exception : %s" % e)
1277        print("Exception in startup:\n %s" % traceback.format_exc())
1278        exit(1)
1279
1280    try:
1281        #
1282        # Instantiate the presentation layers
1283        #
1284
1285        dep = DepExplorer(screen)
1286
1287        #
1288        # Prepare bitbake
1289        #
1290
1291        # Fetch bitbake dependecy data
1292        ret,cmdline = bitbake_load(server, eventHandler, params, dep, curses_off, screen)
1293        if ret: return ret
1294
1295        #
1296        # Preset the views
1297        #
1298
1299        # Cmdline example = ['generateDotGraph', ['acl', 'zlib'], 'build']
1300        primary_packages = cmdline[1]
1301        dep.package_view.set_primary(primary_packages)
1302        dep.dep_view.set_primary(primary_packages)
1303        dep.reverse_view.set_primary(primary_packages)
1304        dep.help_box_view.set_primary(primary_packages)
1305        dep.help_bar_view.show_help(True)
1306        active_package = dep.package_view
1307        active_package.cursor_on(True)
1308        dep.select(primary_packages[0]+'.')
1309        if unit_test:
1310            alert('UNIT_TEST',screen)
1311
1312        # Help method to start/stop the filter feature
1313        def filter_mode(new_filter_status):
1314            global is_filter
1315            if is_filter == new_filter_status:
1316                # Ignore no changes
1317                return
1318            if not new_filter_status:
1319                # Turn off
1320                curses.curs_set(0)
1321                #active_package.cursor_on(False)
1322                active_package = dep.package_view
1323                active_package.cursor_on(True)
1324                is_filter = False
1325                dep.help_bar_view.show_help(True)
1326                dep.filter_str = ''
1327                dep.select('')
1328            else:
1329                # Turn on
1330                curses.curs_set(1)
1331                dep.help_bar_view.show_help(False)
1332                dep.filter_view.clear()
1333                dep.filter_view.show(True)
1334                dep.filter_view.show_prompt()
1335                is_filter = True
1336
1337        #
1338        # Main user loop
1339        #
1340
1341        while not quit:
1342            if is_filter:
1343                dep.filter_view.show_prompt()
1344            if unit_test:
1345                c = unit_test_action(active_package)
1346            else:
1347                c = screen.getch()
1348            ch = chr(c)
1349
1350            # Do not draw if window now too small
1351            if not check_screen_size(dep,active_package):
1352                continue
1353
1354            if verbose:
1355                if c == CHAR_RETURN:
1356                    screen.addstr(0, 4, "|%3d,CR |" % (c))
1357                else:
1358                    screen.addstr(0, 4, "|%3d,%3s|" % (c,chr(c)))
1359
1360            # pre-map alternate filter close keys
1361            if is_filter and (c == CHAR_ESCAPE):
1362                # Alternate exit from filter
1363                ch = '/'
1364                c = ord(ch)
1365
1366            # Filter and non-filter mode command keys
1367            # https://docs.python.org/3/library/curses.html
1368            if c in (curses.KEY_UP,CHAR_UP):
1369                active_package.line_up()
1370                if active_package == dep.package_view:
1371                    dep.select('',only_update_dependents=True)
1372            elif c in (curses.KEY_DOWN,CHAR_DOWN):
1373                active_package.line_down()
1374                if active_package == dep.package_view:
1375                    dep.select('',only_update_dependents=True)
1376            elif curses.KEY_PPAGE == c:
1377                active_package.page_up()
1378                if active_package == dep.package_view:
1379                    dep.select('',only_update_dependents=True)
1380            elif curses.KEY_NPAGE == c:
1381                active_package.page_down()
1382                if active_package == dep.package_view:
1383                    dep.select('',only_update_dependents=True)
1384            elif CHAR_TAB == c:
1385                # Tab between boxes
1386                active_package.cursor_on(False)
1387                if active_package == dep.package_view:
1388                    active_package = dep.dep_view
1389                elif active_package == dep.dep_view:
1390                    active_package = dep.reverse_view
1391                else:
1392                    active_package = dep.package_view
1393                active_package.cursor_on(True)
1394            elif curses.KEY_BTAB == c:
1395                # Shift-Tab reverse between boxes
1396                active_package.cursor_on(False)
1397                if active_package == dep.package_view:
1398                    active_package = dep.reverse_view
1399                elif active_package == dep.reverse_view:
1400                    active_package = dep.dep_view
1401                else:
1402                    active_package = dep.package_view
1403                active_package.cursor_on(True)
1404            elif (CHAR_RETURN == c):
1405                # CR to select
1406                selected = active_package.get_selected()
1407                if selected:
1408                    active_package.cursor_on(False)
1409                    active_package = dep.package_view
1410                    filter_mode(False)
1411                    dep.select(selected)
1412                else:
1413                    filter_mode(False)
1414                    dep.select(primary_packages[0]+'.')
1415
1416            elif '/' == ch: # Enter/exit dep.filter_view
1417                if is_filter:
1418                    filter_mode(False)
1419                else:
1420                    filter_mode(True)
1421            elif is_filter:
1422                # If in filter mode, re-direct all these other keys to the filter box
1423                result = dep.filter_view.input(c,ch)
1424                dep.filter_str = dep.filter_view.filter_str
1425                dep.select('')
1426
1427            # Non-filter mode command keys
1428            elif 'p' == ch:
1429                dep.print_deps(whole_group=False)
1430            elif 'P' == ch:
1431                dep.print_deps(whole_group=True)
1432            elif 'w' == ch:
1433                # Toggle the print model
1434                if print_model == PRINT_MODEL_1:
1435                    print_model = PRINT_MODEL_2
1436                else:
1437                    print_model = PRINT_MODEL_1
1438            elif 's' == ch:
1439                # Toggle the sort model
1440                if sort_model == SORT_DEPS:
1441                    sort_model = SORT_ALPHA
1442                elif sort_model == SORT_ALPHA:
1443                    if SORT_BITBAKE_ENABLE:
1444                        sort_model = TASK_SORT_BITBAKE
1445                    else:
1446                        sort_model = SORT_DEPS
1447                else:
1448                    sort_model = SORT_DEPS
1449                active_package.cursor_on(False)
1450                current_task = active_package.get_selected()
1451                dep.package_view.sort()
1452                dep.dep_view.sort()
1453                dep.reverse_view.sort()
1454                active_package = dep.package_view
1455                active_package.cursor_on(True)
1456                dep.select(current_task)
1457                # Announce the new sort model
1458                alert("SORT=%s" % ("ALPHA" if (sort_model == SORT_ALPHA) else "DEPS"),screen)
1459                alert('',screen)
1460
1461            elif 'q' == ch:
1462                quit = True
1463            elif ch in ('h','?'):
1464                dep.help_box_view.show_help(True)
1465                dep.select(active_package.get_selected())
1466
1467            #
1468            # Debugging commands
1469            #
1470
1471            elif 'V' == ch:
1472                verbose = not verbose
1473                alert('Verbose=%s' % str(verbose),screen)
1474                alert('',screen)
1475            elif 'R' == ch:
1476                screen.refresh()
1477            elif 'B' == ch:
1478                # Progress bar unit test
1479                dep.progress_view.progress('Test',0,40)
1480                curses.napms(1000)
1481                dep.progress_view.progress('',10,40)
1482                curses.napms(1000)
1483                dep.progress_view.progress('',20,40)
1484                curses.napms(1000)
1485                dep.progress_view.progress('',30,40)
1486                curses.napms(1000)
1487                dep.progress_view.progress('',40,40)
1488                curses.napms(1000)
1489                dep.progress_view.clear()
1490                dep.help_bar_view.show_help(True)
1491            elif 'Q' == ch:
1492                # Simulated error
1493                curses_off(screen)
1494                print('ERROR: simulated error exit')
1495                return 1
1496
1497        # Safe exit
1498        curses_off(screen)
1499    except Exception as e:
1500        # Safe exit on error
1501        curses_off(screen)
1502        print("Exception : %s" % e)
1503        print("Exception in startup:\n %s" % traceback.format_exc())
1504
1505    # Reminder to pick up your printed results
1506    if is_printed:
1507        print("")
1508        print("You have output ready!")
1509        print("  * Your printed dependency file is: %s" % print_file_name)
1510        print("  * Your previous results  saved in: %s" % print_file_backup_name)
1511        print("")
1512