pymenuconfig.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  1. # SPDX-License-Identifier: ISC
  2. # -*- coding: utf-8 -*-
  3. """
  4. Overview
  5. ========
  6. pymenuconfig is a small and simple frontend to Kconfiglib that's written
  7. entirely in Python using Tkinter as its GUI toolkit.
  8. Motivation
  9. ==========
  10. Kconfig is a nice and powerful framework for build-time configuration and lots
  11. of projects already benefit from using it. Kconfiglib allows to utilize power of
  12. Kconfig by using scripts written in pure Python, without requiring one to build
  13. Linux kernel tools written in C (this can be quite tedious on anything that's
  14. not *nix). The aim of this project is to implement simple and small Kconfiglib
  15. GUI frontend that runs on as much systems as possible.
  16. Tkinter GUI toolkit is a natural choice if portability is considered, as it's
  17. a part of Python standard library and is available virtually in every CPython
  18. installation.
  19. User interface
  20. ==============
  21. I've tried to replicate look and fill of Linux kernel 'menuconfig' tool that
  22. many users are used to, including keyboard-oriented control and textual
  23. representation of menus with fixed-width font.
  24. Usage
  25. =====
  26. The pymenuconfig module is executable and parses command-line args, so the
  27. most simple way to run menuconfig is to execute script directly:
  28. python pymenuconfig.py --kconfig Kconfig
  29. As with most command-line tools list of options can be obtained with '--help':
  30. python pymenuconfig.py --help
  31. If installed with setuptools, one can run it like this:
  32. python -m pymenuconfig --kconfig Kconfig
  33. In case you're making a wrapper around menuconfig, you can either call main():
  34. import pymenuconfig
  35. pymenuconfig.main(['--kconfig', 'Kconfig'])
  36. Or import MenuConfig class, instantiate it and manually run Tkinter's mainloop:
  37. import tkinter
  38. import kconfiglib
  39. from pymenuconfig import MenuConfig
  40. kconfig = kconfiglib.Kconfig()
  41. mconf = MenuConfig(kconfig)
  42. tkinter.mainloop()
  43. """
  44. from __future__ import print_function
  45. import os
  46. import sys
  47. import argparse
  48. import kconfiglib
  49. # Tk is imported differently depending on python major version
  50. if sys.version_info[0] < 3:
  51. import Tkinter as tk
  52. import tkFont as font
  53. import tkFileDialog as filedialog
  54. import tkMessageBox as messagebox
  55. else:
  56. import tkinter as tk
  57. from tkinter import font
  58. from tkinter import filedialog
  59. from tkinter import messagebox
  60. class ListEntry(object):
  61. """
  62. Represents visible menu node and holds all information related to displaying
  63. menu node in a Listbox.
  64. Instances of this class also handle all interaction with main window.
  65. A node is displayed as a single line of text:
  66. PREFIX INDENT BODY POSTFIX
  67. - The PREFIX is always 3 characters or more and can take following values:
  68. ' ' comment, menu, bool choice, etc.
  69. Inside menus:
  70. '< >' bool symbol has value 'n'
  71. '<*>' bool symbol has value 'y'
  72. '[ ]' tristate symbol has value 'n'
  73. '[M]' tristate symbol has value 'm'
  74. '[*]' tristate symbol has value 'y'
  75. '- -' symbol has value 'n' that's not editable
  76. '-M-' symbol has value 'm' that's not editable
  77. '-*-' symbol has value 'y' that's not editable
  78. '(M)' tristate choice has value 'm'
  79. '(*)' tristate choice has value 'y'
  80. '(some value)' value of non-bool/tristate symbols
  81. Inside choices:
  82. '( )' symbol has value 'n'
  83. '(M)' symbol has value 'm'
  84. '(*)' symbol has value 'y'
  85. - INDENT is a sequence of space characters. It's used in implicit menus, and
  86. adds 2 spaces for each nesting level
  87. - BODY is a menu node prompt. '***' is added if node is a comment
  88. - POSTFIX adds '(NEW)', '--->' and selected choice symbol where applicable
  89. Attributes:
  90. node:
  91. MenuNode instance this ListEntry is created for.
  92. visible:
  93. Whether entry should be shown in main window.
  94. text:
  95. String to display in a main window's Listbox.
  96. refresh():
  97. Updates .visible and .text attribute values.
  98. set_tristate_value():
  99. Set value for bool/tristate symbols, value should be one of 0,1,2 or None.
  100. Usually it's called when user presses 'y', 'n', 'm' key.
  101. set_str_value():
  102. Set value for non-bool/tristate symbols, value is a string. Usually called
  103. with a value returned by one of MenuConfig.ask_for_* methods.
  104. toggle():
  105. Toggle bool/tristate symbol value. Called when '<Space>' key is pressed in
  106. a main window. Also selects choice value.
  107. select():
  108. Called when '<Return>' key is pressed in a main window with 'SELECT'
  109. action selected. Displays submenu, choice selection menu, or just selects
  110. choice value. For non-bool/tristate symbols asks MenuConfig window to
  111. handle value input via one of MenuConfig.ask_for_* methods.
  112. show_help():
  113. Called when '<Return>' key is pressed in a main window with 'HELP' action
  114. selected. Prepares text help and calls MenuConfig.show_text() to display
  115. text window.
  116. """
  117. # How to display value of BOOL and TRISTATE symbols
  118. TRI_TO_DISPLAY = {
  119. 0: ' ',
  120. 1: 'M',
  121. 2: '*'
  122. }
  123. def __init__(self, mconf, node, indent):
  124. self.indent = indent
  125. self.node = node
  126. self.menuconfig = mconf
  127. self.visible = False
  128. self.text = None
  129. def __str__(self):
  130. return self.text
  131. def _is_visible(self):
  132. node = self.node
  133. v = True
  134. v = v and node.prompt is not None
  135. # It should be enough to check if prompt expression is not false and
  136. # for menu nodes whether 'visible if' is not false
  137. v = v and kconfiglib.expr_value(node.prompt[1]) > 0
  138. if node.item == kconfiglib.MENU:
  139. v = v and kconfiglib.expr_value(node.visibility) > 0
  140. # If node references Symbol, then we also account for symbol visibility
  141. # TODO: need to re-think whether this is needed
  142. if isinstance(node.item, kconfiglib.Symbol):
  143. if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  144. v = v and len(node.item.assignable) > 0
  145. else:
  146. v = v and node.item.visibility > 0
  147. return v
  148. def _get_text(self):
  149. """
  150. Compute textual representation of menu node (a line in ListView)
  151. """
  152. node = self.node
  153. item = node.item
  154. # Determine prefix
  155. prefix = ' '
  156. if (isinstance(item, kconfiglib.Symbol) and item.choice is None or
  157. isinstance(item, kconfiglib.Choice) and item.type is kconfiglib.TRISTATE):
  158. # The node is for either a symbol outside of choice statement
  159. # or a tristate choice
  160. if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  161. value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
  162. if len(item.assignable) > 1:
  163. # Symbol is editable
  164. if 1 in item.assignable:
  165. prefix = '<{}>'.format(value)
  166. else:
  167. prefix = '[{}]'.format(value)
  168. else:
  169. # Symbol is not editable
  170. prefix = '-{}-'.format(value)
  171. else:
  172. prefix = '({})'.format(item.str_value)
  173. elif isinstance(item, kconfiglib.Symbol) and item.choice is not None:
  174. # The node is for symbol inside choice statement
  175. if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  176. value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
  177. if len(item.assignable) > 0:
  178. # Symbol is editable
  179. prefix = '({})'.format(value)
  180. else:
  181. # Symbol is not editable
  182. prefix = '-{}-'.format(value)
  183. else:
  184. prefix = '({})'.format(item.str_value)
  185. # Prefix should be at least 3 chars long
  186. if len(prefix) < 3:
  187. prefix += ' ' * (3 - len(prefix))
  188. # Body
  189. body = ''
  190. if node.prompt is not None:
  191. if item is kconfiglib.COMMENT:
  192. body = '*** {} ***'.format(node.prompt[0])
  193. else:
  194. body = node.prompt[0]
  195. # Suffix
  196. is_menu = False
  197. is_new = False
  198. if (item is kconfiglib.MENU
  199. or isinstance(item, kconfiglib.Symbol) and node.is_menuconfig
  200. or isinstance(item, kconfiglib.Choice)):
  201. is_menu = True
  202. if isinstance(item, kconfiglib.Symbol) and item.user_value is None:
  203. is_new = True
  204. # For symbol inside choice that has 'y' value, '(NEW)' is not displayed
  205. if (isinstance(item, kconfiglib.Symbol)
  206. and item.choice and item.choice.tri_value == 2):
  207. is_new = False
  208. # Choice selection - displayed only for choices which have 'y' value
  209. choice_selection = None
  210. if isinstance(item, kconfiglib.Choice) and node.item.str_value == 'y':
  211. choice_selection = ''
  212. if item.selection is not None:
  213. sym = item.selection
  214. if sym.nodes and sym.nodes[0].prompt is not None:
  215. choice_selection = sym.nodes[0].prompt[0]
  216. text = ' {prefix} {indent}{body}{choice}{new}{menu}'.format(
  217. prefix=prefix,
  218. indent=' ' * self.indent,
  219. body=body,
  220. choice='' if choice_selection is None else ' ({})'.format(
  221. choice_selection
  222. ),
  223. new=' (NEW)' if is_new else '',
  224. menu=' --->' if is_menu else ''
  225. )
  226. return text
  227. def refresh(self):
  228. self.visible = self._is_visible()
  229. self.text = self._get_text()
  230. def set_tristate_value(self, value):
  231. """
  232. Call to change value of BOOL, TRISTATE symbols
  233. It's preferred to use this instead of item.set_value as it handles
  234. all necessary interaction with MenuConfig window when symbol value
  235. changes
  236. None value is accepted but ignored
  237. """
  238. item = self.node.item
  239. if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
  240. and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)
  241. and value is not None):
  242. if value in item.assignable:
  243. item.set_value(value)
  244. elif value == 2 and 1 in item.assignable:
  245. print(
  246. 'Symbol {} value is limited to \'m\'. Setting value \'m\' instead of \'y\''.format(item.name),
  247. file=sys.stderr
  248. )
  249. item.set_value(1)
  250. self.menuconfig.mark_as_changed()
  251. self.menuconfig.refresh_display()
  252. def set_str_value(self, value):
  253. """
  254. Call to change value of HEX, INT, STRING symbols
  255. It's preferred to use this instead of item.set_value as it handles
  256. all necessary interaction with MenuConfig window when symbol value
  257. changes
  258. None value is accepted but ignored
  259. """
  260. item = self.node.item
  261. if (isinstance(item, kconfiglib.Symbol)
  262. and item.type in (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)
  263. and value is not None):
  264. item.set_value(value)
  265. self.menuconfig.mark_as_changed()
  266. self.menuconfig.refresh_display()
  267. def toggle(self):
  268. """
  269. Called when <space> key is pressed
  270. """
  271. item = self.node.item
  272. if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
  273. and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)):
  274. value = item.tri_value
  275. # Find next value in Symbol/Choice.assignable, or use assignable[0]
  276. try:
  277. it = iter(item.assignable)
  278. while value != next(it):
  279. pass
  280. self.set_tristate_value(next(it))
  281. except StopIteration:
  282. self.set_tristate_value(item.assignable[0])
  283. def select(self):
  284. """
  285. Called when <Return> key is pressed and SELECT action is selected
  286. """
  287. item = self.node.item
  288. # - Menu: dive into submenu
  289. # - INT, HEX, STRING symbol: raise prompt to enter symbol value
  290. # - BOOL, TRISTATE symbol inside 'y'-valued Choice: set 'y' value
  291. if (item is kconfiglib.MENU
  292. or isinstance(item, kconfiglib.Symbol) and self.node.is_menuconfig
  293. or isinstance(item, kconfiglib.Choice)):
  294. # Dive into submenu
  295. self.menuconfig.show_submenu(self.node)
  296. elif (isinstance(item, kconfiglib.Symbol) and item.type in
  297. (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)):
  298. # Raise prompt to enter symbol value
  299. ident = self.node.prompt[0] if self.node.prompt is not None else None
  300. title = 'Symbol: {}'.format(item.name)
  301. if item.type is kconfiglib.INT:
  302. # Find enabled ranges
  303. ranges = [
  304. (int(start.str_value), int(end.str_value))
  305. for start, end, expr in item.ranges
  306. if kconfiglib.expr_value(expr) > 0
  307. ]
  308. # Raise prompt
  309. self.set_str_value(str(self.menuconfig.ask_for_int(
  310. ident=ident,
  311. title=title,
  312. value=item.str_value,
  313. ranges=ranges
  314. )))
  315. elif item.type is kconfiglib.HEX:
  316. # Find enabled ranges
  317. ranges = [
  318. (int(start.str_value, base=16), int(end.str_value, base=16))
  319. for start, end, expr in item.ranges
  320. if kconfiglib.expr_value(expr) > 0
  321. ]
  322. # Raise prompt
  323. self.set_str_value(hex(self.menuconfig.ask_for_hex(
  324. ident=ident,
  325. title=title,
  326. value=item.str_value,
  327. ranges=ranges
  328. )))
  329. elif item.type is kconfiglib.STRING:
  330. # Raise prompt
  331. self.set_str_value(self.menuconfig.ask_for_string(
  332. ident=ident,
  333. title=title,
  334. value=item.str_value
  335. ))
  336. elif (isinstance(item, kconfiglib.Symbol)
  337. and item.choice is not None and item.choice.tri_value == 2):
  338. # Symbol inside choice -> set symbol value to 'y'
  339. self.set_tristate_value(2)
  340. def show_help(self):
  341. node = self.node
  342. item = self.node.item
  343. if isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)):
  344. title = 'Help for symbol: {}'.format(item.name)
  345. if node.help:
  346. help = node.help
  347. else:
  348. help = 'There is no help available for this option.\n'
  349. lines = []
  350. lines.append(help)
  351. lines.append(
  352. 'Symbol: {} [={}]'.format(
  353. item.name if item.name else '<UNNAMED>', item.str_value
  354. )
  355. )
  356. lines.append('Type : {}'.format(kconfiglib.TYPE_TO_STR[item.type]))
  357. for n in item.nodes:
  358. lines.append('Prompt: {}'.format(n.prompt[0] if n.prompt else '<EMPTY>'))
  359. lines.append(' Defined at {}:{}'.format(n.filename, n.linenr))
  360. lines.append(' Depends on: {}'.format(kconfiglib.expr_str(n.dep)))
  361. text = '\n'.join(lines)
  362. else:
  363. title = 'Help'
  364. text = 'Help not available for this menu node.\n'
  365. self.menuconfig.show_text(text, title)
  366. self.menuconfig.refresh_display()
  367. class EntryDialog(object):
  368. """
  369. Creates modal dialog (top-level Tk window) with labels, entry box and two
  370. buttons: OK and CANCEL.
  371. """
  372. def __init__(self, master, text, title, ident=None, value=None):
  373. self.master = master
  374. dlg = self.dlg = tk.Toplevel(master)
  375. self.dlg.withdraw() #hiden window
  376. dlg.title(title)
  377. # Identifier label
  378. if ident is not None:
  379. self.label_id = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
  380. self.label_id['font'] = font.nametofont('TkFixedFont')
  381. self.label_id['text'] = '# {}'.format(ident)
  382. self.label_id.pack(fill=tk.X, padx=2, pady=2)
  383. # Label
  384. self.label = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
  385. self.label['font'] = font.nametofont('TkFixedFont')
  386. self.label['text'] = text
  387. self.label.pack(fill=tk.X, padx=10, pady=4)
  388. # Entry box
  389. self.entry = tk.Entry(dlg)
  390. self.entry['font'] = font.nametofont('TkFixedFont')
  391. self.entry.pack(fill=tk.X, padx=2, pady=2)
  392. # Frame for buttons
  393. self.frame = tk.Frame(dlg)
  394. self.frame.pack(padx=2, pady=2)
  395. # Button
  396. self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
  397. self.btn_accept['font'] = font.nametofont('TkFixedFont')
  398. self.btn_accept.pack(side=tk.LEFT, padx=2)
  399. self.btn_cancel = tk.Button(self.frame, text='< Cancel >', command=self.cancel)
  400. self.btn_cancel['font'] = font.nametofont('TkFixedFont')
  401. self.btn_cancel.pack(side=tk.LEFT, padx=2)
  402. # Bind Enter and Esc keys
  403. self.dlg.bind('<Return>', self.accept)
  404. self.dlg.bind('<Escape>', self.cancel)
  405. # Dialog is resizable only by width
  406. self.dlg.resizable(1, 0)
  407. # Set supplied value (if any)
  408. if value is not None:
  409. self.entry.insert(0, value)
  410. self.entry.selection_range(0, tk.END)
  411. # By default returned value is None. To caller this means that entry
  412. # process was cancelled
  413. self.value = None
  414. # Modal dialog
  415. dlg.transient(master)
  416. dlg.grab_set()
  417. # Center dialog window
  418. _center_window_above_parent(master, dlg)
  419. self.dlg.deiconify() # show window
  420. # Focus entry field
  421. self.entry.focus_set()
  422. def accept(self, ev=None):
  423. self.value = self.entry.get()
  424. self.dlg.destroy()
  425. def cancel(self, ev=None):
  426. self.dlg.destroy()
  427. class TextDialog(object):
  428. def __init__(self, master, text, title):
  429. self.master = master
  430. dlg = self.dlg = tk.Toplevel(master)
  431. self.dlg.withdraw() #hiden window
  432. dlg.title(title)
  433. dlg.minsize(600,400)
  434. # Text
  435. self.text = tk.Text(dlg, height=1)
  436. self.text['font'] = font.nametofont('TkFixedFont')
  437. self.text.insert(tk.END, text)
  438. # Make text read-only
  439. self.text['state'] = tk.DISABLED
  440. self.text.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
  441. # Frame for buttons
  442. self.frame = tk.Frame(dlg)
  443. self.frame.pack(padx=2, pady=2)
  444. # Button
  445. self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
  446. self.btn_accept['font'] = font.nametofont('TkFixedFont')
  447. self.btn_accept.pack(side=tk.LEFT, padx=2)
  448. # Bind Enter and Esc keys
  449. self.dlg.bind('<Return>', self.accept)
  450. self.dlg.bind('<Escape>', self.cancel)
  451. # Modal dialog
  452. dlg.transient(master)
  453. dlg.grab_set()
  454. # Center dialog window
  455. _center_window_above_parent(master, dlg)
  456. self.dlg.deiconify() # show window
  457. # Focus entry field
  458. self.text.focus_set()
  459. def accept(self, ev=None):
  460. self.dlg.destroy()
  461. def cancel(self, ev=None):
  462. self.dlg.destroy()
  463. class MenuConfig(object):
  464. (
  465. ACTION_SELECT,
  466. ACTION_EXIT,
  467. ACTION_HELP,
  468. ACTION_LOAD,
  469. ACTION_SAVE,
  470. ACTION_SAVE_AS
  471. ) = range(6)
  472. ACTIONS = (
  473. ('Select', ACTION_SELECT),
  474. ('Exit', ACTION_EXIT),
  475. ('Help', ACTION_HELP),
  476. ('Load', ACTION_LOAD),
  477. ('Save', ACTION_SAVE),
  478. ('Save as', ACTION_SAVE_AS),
  479. )
  480. def __init__(self, kconfig, __silent=None):
  481. self.kconfig = kconfig
  482. self.__silent = __silent
  483. if self.__silent is True:
  484. return
  485. # Instantiate Tk widgets
  486. self.root = tk.Tk()
  487. self.root.withdraw() #hiden window
  488. dlg = self.root
  489. # Window title
  490. dlg.title('pymenuconfig')
  491. # Some empirical window size
  492. dlg.minsize(500, 300)
  493. dlg.geometry('800x600')
  494. # Label that shows position in menu tree
  495. self.label_position = tk.Label(
  496. dlg,
  497. anchor=tk.W,
  498. justify=tk.LEFT,
  499. font=font.nametofont('TkFixedFont')
  500. )
  501. self.label_position.pack(fill=tk.X, padx=2)
  502. # 'Tip' frame and text
  503. self.frame_tip = tk.LabelFrame(
  504. dlg,
  505. text='Tip'
  506. )
  507. self.label_tip = tk.Label(
  508. self.frame_tip,
  509. anchor=tk.W,
  510. justify=tk.LEFT,
  511. font=font.nametofont('TkFixedFont')
  512. )
  513. self.label_tip['text'] = '\n'.join([
  514. 'Arrow keys navigate the menu. <Enter> performs selected operation (set of buttons at the bottom)',
  515. 'Pressing <Y> includes, <N> excludes, <M> modularizes features',
  516. 'Press <Esc> to go one level up. Press <Esc> at top level to exit',
  517. 'Legend: [*] built-in [ ] excluded <M> module < > module capable'
  518. ])
  519. self.label_tip.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
  520. self.frame_tip.pack(fill=tk.X, padx=2)
  521. # Main ListBox where all the magic happens
  522. self.list = tk.Listbox(
  523. dlg,
  524. selectmode=tk.SINGLE,
  525. activestyle=tk.NONE,
  526. font=font.nametofont('TkFixedFont'),
  527. height=1,
  528. )
  529. self.list['foreground'] = 'Blue'
  530. self.list['background'] = 'Gray95'
  531. # Make selection invisible
  532. self.list['selectbackground'] = self.list['background']
  533. self.list['selectforeground'] = self.list['foreground']
  534. self.list.pack(fill=tk.BOTH, expand=1, padx=20, ipadx=2)
  535. # Frame with radio buttons
  536. self.frame_radio = tk.Frame(dlg)
  537. self.radio_buttons = []
  538. self.tk_selected_action = tk.IntVar()
  539. for text, value in MenuConfig.ACTIONS:
  540. btn = tk.Radiobutton(
  541. self.frame_radio,
  542. variable=self.tk_selected_action,
  543. value=value
  544. )
  545. btn['text'] = '< {} >'.format(text)
  546. btn['font'] = font.nametofont('TkFixedFont')
  547. btn['indicatoron'] = 0
  548. btn.pack(side=tk.LEFT)
  549. self.radio_buttons.append(btn)
  550. self.frame_radio.pack(anchor=tk.CENTER, pady=4)
  551. # Label with status information
  552. self.tk_status = tk.StringVar()
  553. self.label_status = tk.Label(
  554. dlg,
  555. textvariable=self.tk_status,
  556. anchor=tk.W,
  557. justify=tk.LEFT,
  558. font=font.nametofont('TkFixedFont')
  559. )
  560. self.label_status.pack(fill=tk.X, padx=4, pady=4)
  561. # Center window
  562. _center_window(self.root, dlg)
  563. self.root.deiconify() # show window
  564. # Disable keyboard focus on all widgets ...
  565. self._set_option_to_all_children(dlg, 'takefocus', 0)
  566. # ... except for main ListBox
  567. self.list['takefocus'] = 1
  568. self.list.focus_set()
  569. # Bind keys
  570. dlg.bind('<Escape>', self.handle_keypress)
  571. dlg.bind('<space>', self.handle_keypress)
  572. dlg.bind('<Return>', self.handle_keypress)
  573. dlg.bind('<Right>', self.handle_keypress)
  574. dlg.bind('<Left>', self.handle_keypress)
  575. dlg.bind('<Up>', self.handle_keypress)
  576. dlg.bind('<Down>', self.handle_keypress)
  577. dlg.bind('n', self.handle_keypress)
  578. dlg.bind('m', self.handle_keypress)
  579. dlg.bind('y', self.handle_keypress)
  580. # Register callback that's called when window closes
  581. dlg.wm_protocol('WM_DELETE_WINDOW', self._close_window)
  582. # Init fields
  583. self.node = None
  584. self.node_stack = []
  585. self.all_entries = []
  586. self.shown_entries = []
  587. self.config_path = None
  588. self.unsaved_changes = False
  589. self.status_string = 'NEW CONFIG'
  590. self.update_status()
  591. # Display first child of top level node (the top level node is 'mainmenu')
  592. self.show_node(self.kconfig.top_node)
  593. def _set_option_to_all_children(self, widget, option, value):
  594. widget[option] = value
  595. for n,c in widget.children.items():
  596. self._set_option_to_all_children(c, option, value)
  597. def _invert_colors(self, idx):
  598. self.list.itemconfig(idx, {'bg' : self.list['foreground']})
  599. self.list.itemconfig(idx, {'fg' : self.list['background']})
  600. @property
  601. def _selected_entry(self):
  602. # type: (...) -> ListEntry
  603. active_idx = self.list.index(tk.ACTIVE)
  604. if active_idx >= 0 and active_idx < len(self.shown_entries):
  605. return self.shown_entries[active_idx]
  606. return None
  607. def _select_node(self, node):
  608. # type: (kconfiglib.MenuNode) -> None
  609. """
  610. Attempts to select entry that corresponds to given MenuNode in main listbox
  611. """
  612. idx = None
  613. for i, e in enumerate(self.shown_entries):
  614. if e.node is node:
  615. idx = i
  616. break
  617. if idx is not None:
  618. self.list.activate(idx)
  619. self.list.see(idx)
  620. self._invert_colors(idx)
  621. def handle_keypress(self, ev):
  622. keysym = ev.keysym
  623. if keysym == 'Left':
  624. self._select_action(prev=True)
  625. elif keysym == 'Right':
  626. self._select_action(prev=False)
  627. elif keysym == 'Up':
  628. self.refresh_display(reset_selection=False)
  629. elif keysym == 'Down':
  630. self.refresh_display(reset_selection=False)
  631. elif keysym == 'space':
  632. self._selected_entry.toggle()
  633. elif keysym in ('n', 'm', 'y'):
  634. self._selected_entry.set_tristate_value(kconfiglib.STR_TO_TRI[keysym])
  635. elif keysym == 'Return':
  636. action = self.tk_selected_action.get()
  637. if action == self.ACTION_SELECT:
  638. self._selected_entry.select()
  639. elif action == self.ACTION_EXIT:
  640. self._action_exit()
  641. elif action == self.ACTION_HELP:
  642. self._selected_entry.show_help()
  643. elif action == self.ACTION_LOAD:
  644. if self.prevent_losing_changes():
  645. self.open_config()
  646. elif action == self.ACTION_SAVE:
  647. self.save_config()
  648. elif action == self.ACTION_SAVE_AS:
  649. self.save_config(force_file_dialog=True)
  650. elif keysym == 'Escape':
  651. self._action_exit()
  652. pass
  653. def _close_window(self):
  654. if self.prevent_losing_changes():
  655. print('Exiting..')
  656. if self.__silent is True:
  657. return
  658. self.root.destroy()
  659. def _action_exit(self):
  660. if self.node_stack:
  661. self.show_parent()
  662. else:
  663. self._close_window()
  664. def _select_action(self, prev=False):
  665. # Determine the radio button that's activated
  666. action = self.tk_selected_action.get()
  667. if prev:
  668. action -= 1
  669. else:
  670. action += 1
  671. action %= len(MenuConfig.ACTIONS)
  672. self.tk_selected_action.set(action)
  673. def _collect_list_entries(self, start_node, indent=0):
  674. """
  675. Given first MenuNode of nodes list at some level in menu hierarchy,
  676. collects nodes that may be displayed when viewing and editing that
  677. hierarchy level. Includes implicit menu nodes, i.e. the ones dependent
  678. on 'config' entry via 'if' statement which are internally represented
  679. as children of their dependency
  680. """
  681. entries = []
  682. n = start_node
  683. while n is not None:
  684. entries.append(ListEntry(self, n, indent))
  685. # If node refers to a symbol (X) and has children, it is either
  686. # 'config' or 'menuconfig'. The children are items inside 'if X'
  687. # block that immediately follows 'config' or 'menuconfig' entry.
  688. # If it's a 'menuconfig' then corresponding MenuNode is shown as a
  689. # regular menu entry. But if it's a 'config', then its children need
  690. # to be shown in the same list with their texts indented
  691. if (n.list is not None
  692. and isinstance(n.item, kconfiglib.Symbol)
  693. and n.is_menuconfig == False):
  694. entries.extend(
  695. self._collect_list_entries(n.list, indent=indent + 1)
  696. )
  697. n = n.next
  698. return entries
  699. def refresh_display(self, reset_selection=False):
  700. # Refresh list entries' attributes
  701. for e in self.all_entries:
  702. e.refresh()
  703. # Try to preserve selection upon refresh
  704. selected_entry = self._selected_entry
  705. # Also try to preserve listbox scroll offset
  706. # If not preserved, the see() method will make wanted item to appear
  707. # at the bottom of the list, even if previously it was in center
  708. scroll_offset = self.list.yview()[0]
  709. # Show only visible entries
  710. self.shown_entries = [e for e in self.all_entries if e.visible]
  711. # Refresh listbox contents
  712. self.list.delete(0, tk.END)
  713. self.list.insert(0, *self.shown_entries)
  714. if selected_entry and not reset_selection:
  715. # Restore scroll position
  716. self.list.yview_moveto(scroll_offset)
  717. # Activate previously selected node
  718. self._select_node(selected_entry.node)
  719. else:
  720. # Select the topmost entry
  721. self.list.activate(0)
  722. self._invert_colors(0)
  723. # Select ACTION_SELECT on each refresh (mimic C menuconfig)
  724. self.tk_selected_action.set(self.ACTION_SELECT)
  725. # Display current location in configuration tree
  726. pos = []
  727. for n in self.node_stack + [self.node]:
  728. pos.append(n.prompt[0] if n.prompt else '[none]')
  729. self.label_position['text'] = u'# ' + u' -> '.join(pos)
  730. def show_node(self, node):
  731. self.node = node
  732. if node.list is not None:
  733. self.all_entries = self._collect_list_entries(node.list)
  734. else:
  735. self.all_entries = []
  736. self.refresh_display(reset_selection=True)
  737. def show_submenu(self, node):
  738. self.node_stack.append(self.node)
  739. self.show_node(node)
  740. def show_parent(self):
  741. if self.node_stack:
  742. select_node = self.node
  743. parent_node = self.node_stack.pop()
  744. self.show_node(parent_node)
  745. # Restore previous selection
  746. self._select_node(select_node)
  747. self.refresh_display(reset_selection=False)
  748. def ask_for_string(self, ident=None, title='Enter string', value=None):
  749. """
  750. Raises dialog with text entry widget and asks user to enter string
  751. Return:
  752. - str - user entered string
  753. - None - entry was cancelled
  754. """
  755. text = 'Please enter a string value\n' \
  756. 'User <Enter> key to accept the value\n' \
  757. 'Use <Esc> key to cancel entry\n'
  758. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  759. self.root.wait_window(d.dlg)
  760. self.list.focus_set()
  761. return d.value
  762. def ask_for_int(self, ident=None, title='Enter integer value', value=None, ranges=()):
  763. """
  764. Raises dialog with text entry widget and asks user to enter decimal number
  765. Ranges should be iterable of tuples (start, end),
  766. where 'start' and 'end' specify allowed value range (inclusively)
  767. Return:
  768. - int - when valid number that falls within any one of specified ranges is entered
  769. - None - invalid number or entry was cancelled
  770. """
  771. text = 'Please enter a decimal value. Fractions will not be accepted\n' \
  772. 'User <Enter> key to accept the value\n' \
  773. 'Use <Esc> key to cancel entry\n'
  774. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  775. self.root.wait_window(d.dlg)
  776. self.list.focus_set()
  777. ivalue = None
  778. if d.value:
  779. try:
  780. ivalue = int(d.value)
  781. except ValueError:
  782. messagebox.showerror('Bad value', 'Entered value \'{}\' is not an integer'.format(d.value))
  783. if ivalue is not None and ranges:
  784. allowed = False
  785. for start, end in ranges:
  786. allowed = allowed or start <= ivalue and ivalue <= end
  787. if not allowed:
  788. messagebox.showerror(
  789. 'Bad value',
  790. 'Entered value \'{:d}\' is out of range\n'
  791. 'Allowed:\n{}'.format(
  792. ivalue,
  793. '\n'.join([' {:d} - {:d}'.format(s,e) for s,e in ranges])
  794. )
  795. )
  796. ivalue = None
  797. return ivalue
  798. def ask_for_hex(self, ident=None, title='Enter hexadecimal value', value=None, ranges=()):
  799. """
  800. Raises dialog with text entry widget and asks user to enter decimal number
  801. Ranges should be iterable of tuples (start, end),
  802. where 'start' and 'end' specify allowed value range (inclusively)
  803. Return:
  804. - int - when valid number that falls within any one of specified ranges is entered
  805. - None - invalid number or entry was cancelled
  806. """
  807. text = 'Please enter a hexadecimal value\n' \
  808. 'User <Enter> key to accept the value\n' \
  809. 'Use <Esc> key to cancel entry\n'
  810. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  811. self.root.wait_window(d.dlg)
  812. self.list.focus_set()
  813. hvalue = None
  814. if d.value:
  815. try:
  816. hvalue = int(d.value, base=16)
  817. except ValueError:
  818. messagebox.showerror('Bad value', 'Entered value \'{}\' is not a hexadecimal value'.format(d.value))
  819. if hvalue is not None and ranges:
  820. allowed = False
  821. for start, end in ranges:
  822. allowed = allowed or start <= hvalue and hvalue <= end
  823. if not allowed:
  824. messagebox.showerror(
  825. 'Bad value',
  826. 'Entered value \'0x{:x}\' is out of range\n'
  827. 'Allowed:\n{}'.format(
  828. hvalue,
  829. '\n'.join([' 0x{:x} - 0x{:x}'.format(s,e) for s,e in ranges])
  830. )
  831. )
  832. hvalue = None
  833. return hvalue
  834. def show_text(self, text, title='Info'):
  835. """
  836. Raises dialog with read-only text view that contains supplied text
  837. """
  838. d = TextDialog(self.root, text, title)
  839. self.root.wait_window(d.dlg)
  840. self.list.focus_set()
  841. def mark_as_changed(self):
  842. """
  843. Marks current config as having unsaved changes
  844. Should be called whenever config value is changed
  845. """
  846. self.unsaved_changes = True
  847. self.update_status()
  848. def set_status_string(self, status):
  849. """
  850. Sets status string displayed at the bottom of the window
  851. """
  852. self.status_string = status
  853. self.update_status()
  854. def update_status(self):
  855. """
  856. Updates status bar display
  857. Status bar displays:
  858. - unsaved status
  859. - current config path
  860. - status string (see set_status_string())
  861. """
  862. if self.__silent is True:
  863. return
  864. self.tk_status.set('{} [{}] {}'.format(
  865. '<UNSAVED>' if self.unsaved_changes else '',
  866. self.config_path if self.config_path else '',
  867. self.status_string
  868. ))
  869. def _check_is_visible(self, node):
  870. v = True
  871. v = v and node.prompt is not None
  872. # It should be enough to check if prompt expression is not false and
  873. # for menu nodes whether 'visible if' is not false
  874. v = v and kconfiglib.expr_value(node.prompt[1]) > 0
  875. if node.item == kconfiglib.MENU:
  876. v = v and kconfiglib.expr_value(node.visibility) > 0
  877. # If node references Symbol, then we also account for symbol visibility
  878. # TODO: need to re-think whether this is needed
  879. if isinstance(node.item, kconfiglib.Symbol):
  880. if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  881. v = v and len(node.item.assignable) > 0
  882. else:
  883. v = v and node.item.visibility > 0
  884. return v
  885. def config_is_changed(self):
  886. is_changed = False
  887. node = self.kconfig.top_node.list
  888. if not node:
  889. # Empty configuration
  890. return is_changed
  891. while 1:
  892. item = node.item
  893. if isinstance(item, kconfiglib.Symbol) and item.user_value is None and self._check_is_visible(node):
  894. is_changed = True
  895. print("Config \"# {}\" has changed, need save config file\n".format(node.prompt[0]))
  896. break;
  897. # Iterative tree walk using parent pointers
  898. if node.list:
  899. node = node.list
  900. elif node.next:
  901. node = node.next
  902. else:
  903. while node.parent:
  904. node = node.parent
  905. if node.next:
  906. node = node.next
  907. break
  908. else:
  909. break
  910. return is_changed
  911. def prevent_losing_changes(self):
  912. """
  913. Checks if there are unsaved changes and asks user to save or discard them
  914. This routine should be called whenever current config is going to be discarded
  915. Raises the usual 'Yes', 'No', 'Cancel' prompt.
  916. Return:
  917. - True: caller may safely drop current config state
  918. - False: user needs to continue work on current config ('Cancel' pressed or saving failed)
  919. """
  920. if self.config_is_changed() == True:
  921. self.mark_as_changed()
  922. if not self.unsaved_changes:
  923. return True
  924. if self.__silent:
  925. saved = self.save_config()
  926. return saved
  927. res = messagebox.askyesnocancel(
  928. parent=self.root,
  929. title='Unsaved changes',
  930. message='Config has unsaved changes. Do you want to save them?'
  931. )
  932. if res is None:
  933. return False
  934. elif res is False:
  935. return True
  936. # Otherwise attempt to save config and succeed only if config has been saved successfully
  937. saved = self.save_config()
  938. return saved
  939. def open_config(self, path=None):
  940. if path is None:
  941. # Create open dialog. Either existing file is selected or no file is selected as a result
  942. path = filedialog.askopenfilename(
  943. parent=self.root,
  944. title='Open config..',
  945. initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
  946. filetypes=(('.config files', '*.config'), ('All files', '*.*'))
  947. )
  948. if not path or not os.path.isfile(path):
  949. return False
  950. path = os.path.abspath(path)
  951. print('Loading config: \'{}\''.format(path))
  952. # Try to open given path
  953. # If path does not exist, we still set current config path to it but don't load anything
  954. self.unsaved_changes = False
  955. self.config_path = path
  956. if not os.path.exists(path):
  957. self.set_status_string('New config')
  958. self.mark_as_changed()
  959. return True
  960. # Load config and set status accordingly
  961. try:
  962. self.kconfig.load_config(path)
  963. except IOError as e:
  964. self.set_status_string('Failed to load: \'{}\''.format(path))
  965. if not self.__silent:
  966. self.refresh_display()
  967. print('Failed to load config \'{}\': {}'.format(path, e))
  968. return False
  969. self.set_status_string('Opened config')
  970. if not self.__silent:
  971. self.refresh_display()
  972. return True
  973. def save_config(self, force_file_dialog=False):
  974. path = self.config_path
  975. if path is None or force_file_dialog:
  976. path = filedialog.asksaveasfilename(
  977. parent=self.root,
  978. title='Save config as..',
  979. initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
  980. initialfile=os.path.basename(self.config_path) if self.config_path else None,
  981. defaultextension='.config',
  982. filetypes=(('.config files', '*.config'), ('All files', '*.*'))
  983. )
  984. if not path:
  985. return False
  986. path = os.path.abspath(path)
  987. print('Saving config: \'{}\''.format(path))
  988. # Try to save config to selected path
  989. try:
  990. self.kconfig.write_config(path, header="#\n# Automatically generated file; DO NOT EDIT.\n")
  991. self.unsaved_changes = False
  992. self.config_path = path
  993. self.set_status_string('Saved config')
  994. except IOError as e:
  995. self.set_status_string('Failed to save: \'{}\''.format(path))
  996. print('Save failed: {}'.format(e), file=sys.stderr)
  997. return False
  998. return True
  999. def _center_window(root, window):
  1000. # type: (tk.Tk, tk.Toplevel) -> None
  1001. """
  1002. Attempts to center window on screen
  1003. """
  1004. root.update_idletasks()
  1005. # root.eval('tk::PlaceWindow {!s} center'.format(
  1006. # window.winfo_pathname(window.winfo_id())
  1007. # ))
  1008. w = window.winfo_width()
  1009. h = window.winfo_height()
  1010. ws = window.winfo_screenwidth()
  1011. hs = window.winfo_screenheight()
  1012. x = (ws / 2) - (w / 2)
  1013. y = (hs / 2) - (h / 2)
  1014. window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
  1015. window.lift()
  1016. window.focus_force()
  1017. def _center_window_above_parent(root, window):
  1018. # type: (tk.Tk, tk.Toplevel) -> None
  1019. """
  1020. Attempts to center window above its parent window
  1021. """
  1022. # root.eval('tk::PlaceWindow {!s} center'.format(
  1023. # window.winfo_pathname(window.winfo_id())
  1024. # ))
  1025. root.update_idletasks()
  1026. parent = window.master
  1027. w = window.winfo_width()
  1028. h = window.winfo_height()
  1029. px = parent.winfo_rootx()
  1030. py = parent.winfo_rooty()
  1031. pw = parent.winfo_width()
  1032. ph = parent.winfo_height()
  1033. x = px + (pw / 2) - (w / 2)
  1034. y = py + (ph / 2) - (h / 2)
  1035. window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
  1036. window.lift()
  1037. window.focus_force()
  1038. def main(argv=None):
  1039. if argv is None:
  1040. argv = sys.argv[1:]
  1041. # Instantiate cmd options parser
  1042. parser = argparse.ArgumentParser(
  1043. description='Interactive Kconfig configuration editor'
  1044. )
  1045. parser.add_argument(
  1046. '--kconfig',
  1047. metavar='FILE',
  1048. type=str,
  1049. default='Kconfig',
  1050. help='path to root Kconfig file'
  1051. )
  1052. parser.add_argument(
  1053. '--config',
  1054. metavar='FILE',
  1055. type=str,
  1056. help='path to .config file to load'
  1057. )
  1058. if "--silent" in argv:
  1059. parser.add_argument(
  1060. '--silent',
  1061. dest = '_silent_',
  1062. type=str,
  1063. help='silent mode, not show window'
  1064. )
  1065. args = parser.parse_args(argv)
  1066. kconfig_path = args.kconfig
  1067. config_path = args.config
  1068. # Verify that Kconfig file exists
  1069. if not os.path.isfile(kconfig_path):
  1070. raise RuntimeError('\'{}\': no such file'.format(kconfig_path))
  1071. # Parse Kconfig files
  1072. kconf = kconfiglib.Kconfig(filename=kconfig_path)
  1073. if "--silent" not in argv:
  1074. print("In normal mode. Will show menuconfig window.")
  1075. mc = MenuConfig(kconf)
  1076. # If config file was specified, load it
  1077. if config_path:
  1078. mc.open_config(config_path)
  1079. print("Enter mainloop. Waiting...")
  1080. tk.mainloop()
  1081. else:
  1082. print("In silent mode. Don`t show menuconfig window.")
  1083. mc = MenuConfig(kconf, True)
  1084. # If config file was specified, load it
  1085. if config_path:
  1086. mc.open_config(config_path)
  1087. mc._close_window()
  1088. if __name__ == '__main__':
  1089. main()