pymenuconfig.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  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. dlg.title(title)
  376. # Identifier label
  377. if ident is not None:
  378. self.label_id = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
  379. self.label_id['font'] = font.nametofont('TkFixedFont')
  380. self.label_id['text'] = '# {}'.format(ident)
  381. self.label_id.pack(fill=tk.X, padx=2, pady=2)
  382. # Label
  383. self.label = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
  384. self.label['font'] = font.nametofont('TkFixedFont')
  385. self.label['text'] = text
  386. self.label.pack(fill=tk.X, padx=10, pady=4)
  387. # Entry box
  388. self.entry = tk.Entry(dlg)
  389. self.entry['font'] = font.nametofont('TkFixedFont')
  390. self.entry.pack(fill=tk.X, padx=2, pady=2)
  391. # Frame for buttons
  392. self.frame = tk.Frame(dlg)
  393. self.frame.pack(padx=2, pady=2)
  394. # Button
  395. self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
  396. self.btn_accept['font'] = font.nametofont('TkFixedFont')
  397. self.btn_accept.pack(side=tk.LEFT, padx=2)
  398. self.btn_cancel = tk.Button(self.frame, text='< Cancel >', command=self.cancel)
  399. self.btn_cancel['font'] = font.nametofont('TkFixedFont')
  400. self.btn_cancel.pack(side=tk.LEFT, padx=2)
  401. # Bind Enter and Esc keys
  402. self.dlg.bind('<Return>', self.accept)
  403. self.dlg.bind('<Escape>', self.cancel)
  404. # Dialog is resizable only by width
  405. self.dlg.resizable(1, 0)
  406. # Set supplied value (if any)
  407. if value is not None:
  408. self.entry.insert(0, value)
  409. self.entry.selection_range(0, tk.END)
  410. # By default returned value is None. To caller this means that entry
  411. # process was cancelled
  412. self.value = None
  413. # Modal dialog
  414. dlg.transient(master)
  415. dlg.grab_set()
  416. # Center dialog window
  417. _center_window_above_parent(master, dlg)
  418. # Focus entry field
  419. self.entry.focus_set()
  420. def accept(self, ev=None):
  421. self.value = self.entry.get()
  422. self.dlg.destroy()
  423. def cancel(self, ev=None):
  424. self.dlg.destroy()
  425. class TextDialog(object):
  426. def __init__(self, master, text, title):
  427. self.master = master
  428. dlg = self.dlg = tk.Toplevel(master)
  429. dlg.title(title)
  430. dlg.minsize(600,400)
  431. # Text
  432. self.text = tk.Text(dlg, height=1)
  433. self.text['font'] = font.nametofont('TkFixedFont')
  434. self.text.insert(tk.END, text)
  435. # Make text read-only
  436. self.text['state'] = tk.DISABLED
  437. self.text.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
  438. # Frame for buttons
  439. self.frame = tk.Frame(dlg)
  440. self.frame.pack(padx=2, pady=2)
  441. # Button
  442. self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
  443. self.btn_accept['font'] = font.nametofont('TkFixedFont')
  444. self.btn_accept.pack(side=tk.LEFT, padx=2)
  445. # Bind Enter and Esc keys
  446. self.dlg.bind('<Return>', self.accept)
  447. self.dlg.bind('<Escape>', self.cancel)
  448. # Modal dialog
  449. dlg.transient(master)
  450. dlg.grab_set()
  451. # Center dialog window
  452. _center_window_above_parent(master, dlg)
  453. # Focus entry field
  454. self.text.focus_set()
  455. def accept(self, ev=None):
  456. self.dlg.destroy()
  457. def cancel(self, ev=None):
  458. self.dlg.destroy()
  459. class MenuConfig(object):
  460. (
  461. ACTION_SELECT,
  462. ACTION_EXIT,
  463. ACTION_HELP,
  464. ACTION_LOAD,
  465. ACTION_SAVE,
  466. ACTION_SAVE_AS
  467. ) = range(6)
  468. ACTIONS = (
  469. ('Select', ACTION_SELECT),
  470. ('Exit', ACTION_EXIT),
  471. ('Help', ACTION_HELP),
  472. ('Load', ACTION_LOAD),
  473. ('Save', ACTION_SAVE),
  474. ('Save as', ACTION_SAVE_AS),
  475. )
  476. def __init__(self, kconfig):
  477. self.kconfig = kconfig
  478. # Instantiate Tk widgets
  479. self.root = tk.Tk()
  480. dlg = self.root
  481. # Window title
  482. dlg.title('pymenuconfig')
  483. # Some empirical window size
  484. dlg.minsize(500, 300)
  485. dlg.geometry('800x600')
  486. # Label that shows position in menu tree
  487. self.label_position = tk.Label(
  488. dlg,
  489. anchor=tk.W,
  490. justify=tk.LEFT,
  491. font=font.nametofont('TkFixedFont')
  492. )
  493. self.label_position.pack(fill=tk.X, padx=2)
  494. # 'Tip' frame and text
  495. self.frame_tip = tk.LabelFrame(
  496. dlg,
  497. text='Tip'
  498. )
  499. self.label_tip = tk.Label(
  500. self.frame_tip,
  501. anchor=tk.W,
  502. justify=tk.LEFT,
  503. font=font.nametofont('TkFixedFont')
  504. )
  505. self.label_tip['text'] = '\n'.join([
  506. 'Arrow keys navigate the menu. <Enter> performs selected operation (set of buttons at the bottom)',
  507. 'Pressing <Y> includes, <N> excludes, <M> modularizes features',
  508. 'Press <Esc> to go one level up. Press <Esc> at top level to exit',
  509. 'Legend: [*] built-in [ ] excluded <M> module < > module capable'
  510. ])
  511. self.label_tip.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
  512. self.frame_tip.pack(fill=tk.X, padx=2)
  513. # Main ListBox where all the magic happens
  514. self.list = tk.Listbox(
  515. dlg,
  516. selectmode=tk.SINGLE,
  517. activestyle=tk.NONE,
  518. font=font.nametofont('TkFixedFont'),
  519. height=1,
  520. )
  521. self.list['foreground'] = 'Blue'
  522. self.list['background'] = 'Gray95'
  523. # Make selection invisible
  524. self.list['selectbackground'] = self.list['background']
  525. self.list['selectforeground'] = self.list['foreground']
  526. self.list.pack(fill=tk.BOTH, expand=1, padx=20, ipadx=2)
  527. # Frame with radio buttons
  528. self.frame_radio = tk.Frame(dlg)
  529. self.radio_buttons = []
  530. self.tk_selected_action = tk.IntVar()
  531. for text, value in MenuConfig.ACTIONS:
  532. btn = tk.Radiobutton(
  533. self.frame_radio,
  534. variable=self.tk_selected_action,
  535. value=value
  536. )
  537. btn['text'] = '< {} >'.format(text)
  538. btn['font'] = font.nametofont('TkFixedFont')
  539. btn['indicatoron'] = 0
  540. btn.pack(side=tk.LEFT)
  541. self.radio_buttons.append(btn)
  542. self.frame_radio.pack(anchor=tk.CENTER, pady=4)
  543. # Label with status information
  544. self.tk_status = tk.StringVar()
  545. self.label_status = tk.Label(
  546. dlg,
  547. textvariable=self.tk_status,
  548. anchor=tk.W,
  549. justify=tk.LEFT,
  550. font=font.nametofont('TkFixedFont')
  551. )
  552. self.label_status.pack(fill=tk.X, padx=4, pady=4)
  553. # Center window
  554. _center_window(self.root, dlg)
  555. # Disable keyboard focus on all widgets ...
  556. self._set_option_to_all_children(dlg, 'takefocus', 0)
  557. # ... except for main ListBox
  558. self.list['takefocus'] = 1
  559. self.list.focus_set()
  560. # Bind keys
  561. dlg.bind('<Escape>', self.handle_keypress)
  562. dlg.bind('<space>', self.handle_keypress)
  563. dlg.bind('<Return>', self.handle_keypress)
  564. dlg.bind('<Right>', self.handle_keypress)
  565. dlg.bind('<Left>', self.handle_keypress)
  566. dlg.bind('<Up>', self.handle_keypress)
  567. dlg.bind('<Down>', self.handle_keypress)
  568. dlg.bind('n', self.handle_keypress)
  569. dlg.bind('m', self.handle_keypress)
  570. dlg.bind('y', self.handle_keypress)
  571. # Register callback that's called when window closes
  572. dlg.wm_protocol('WM_DELETE_WINDOW', self._close_window)
  573. # Init fields
  574. self.node = None
  575. self.node_stack = []
  576. self.all_entries = []
  577. self.shown_entries = []
  578. self.config_path = None
  579. self.unsaved_changes = False
  580. self.status_string = 'NEW CONFIG'
  581. self.update_status()
  582. # Display first child of top level node (the top level node is 'mainmenu')
  583. self.show_node(self.kconfig.top_node)
  584. def _set_option_to_all_children(self, widget, option, value):
  585. widget[option] = value
  586. for n,c in widget.children.items():
  587. self._set_option_to_all_children(c, option, value)
  588. def _invert_colors(self, idx):
  589. self.list.itemconfig(idx, {'bg' : self.list['foreground']})
  590. self.list.itemconfig(idx, {'fg' : self.list['background']})
  591. @property
  592. def _selected_entry(self):
  593. # type: (...) -> ListEntry
  594. active_idx = self.list.index(tk.ACTIVE)
  595. if active_idx >= 0 and active_idx < len(self.shown_entries):
  596. return self.shown_entries[active_idx]
  597. return None
  598. def _select_node(self, node):
  599. # type: (kconfiglib.MenuNode) -> None
  600. """
  601. Attempts to select entry that corresponds to given MenuNode in main listbox
  602. """
  603. idx = None
  604. for i, e in enumerate(self.shown_entries):
  605. if e.node is node:
  606. idx = i
  607. break
  608. if idx is not None:
  609. self.list.activate(idx)
  610. self.list.see(idx)
  611. self._invert_colors(idx)
  612. def handle_keypress(self, ev):
  613. keysym = ev.keysym
  614. if keysym == 'Left':
  615. self._select_action(prev=True)
  616. elif keysym == 'Right':
  617. self._select_action(prev=False)
  618. elif keysym == 'Up':
  619. self.refresh_display(reset_selection=False)
  620. elif keysym == 'Down':
  621. self.refresh_display(reset_selection=False)
  622. elif keysym == 'space':
  623. self._selected_entry.toggle()
  624. elif keysym in ('n', 'm', 'y'):
  625. self._selected_entry.set_tristate_value(kconfiglib.STR_TO_TRI[keysym])
  626. elif keysym == 'Return':
  627. action = self.tk_selected_action.get()
  628. if action == self.ACTION_SELECT:
  629. self._selected_entry.select()
  630. elif action == self.ACTION_EXIT:
  631. self._action_exit()
  632. elif action == self.ACTION_HELP:
  633. self._selected_entry.show_help()
  634. elif action == self.ACTION_LOAD:
  635. if self.prevent_losing_changes():
  636. self.open_config()
  637. elif action == self.ACTION_SAVE:
  638. self.save_config()
  639. elif action == self.ACTION_SAVE_AS:
  640. self.save_config(force_file_dialog=True)
  641. elif keysym == 'Escape':
  642. self._action_exit()
  643. pass
  644. def _close_window(self):
  645. if self.prevent_losing_changes():
  646. print('Exiting..')
  647. self.root.destroy()
  648. def _action_exit(self):
  649. if self.node_stack:
  650. self.show_parent()
  651. else:
  652. self._close_window()
  653. def _select_action(self, prev=False):
  654. # Determine the radio button that's activated
  655. action = self.tk_selected_action.get()
  656. if prev:
  657. action -= 1
  658. else:
  659. action += 1
  660. action %= len(MenuConfig.ACTIONS)
  661. self.tk_selected_action.set(action)
  662. def _collect_list_entries(self, start_node, indent=0):
  663. """
  664. Given first MenuNode of nodes list at some level in menu hierarchy,
  665. collects nodes that may be displayed when viewing and editing that
  666. hierarchy level. Includes implicit menu nodes, i.e. the ones dependent
  667. on 'config' entry via 'if' statement which are internally represented
  668. as children of their dependency
  669. """
  670. entries = []
  671. n = start_node
  672. while n is not None:
  673. entries.append(ListEntry(self, n, indent))
  674. # If node refers to a symbol (X) and has children, it is either
  675. # 'config' or 'menuconfig'. The children are items inside 'if X'
  676. # block that immediately follows 'config' or 'menuconfig' entry.
  677. # If it's a 'menuconfig' then corresponding MenuNode is shown as a
  678. # regular menu entry. But if it's a 'config', then its children need
  679. # to be shown in the same list with their texts indented
  680. if (n.list is not None
  681. and isinstance(n.item, kconfiglib.Symbol)
  682. and n.is_menuconfig == False):
  683. entries.extend(
  684. self._collect_list_entries(n.list, indent=indent + 1)
  685. )
  686. n = n.next
  687. return entries
  688. def refresh_display(self, reset_selection=False):
  689. # Refresh list entries' attributes
  690. for e in self.all_entries:
  691. e.refresh()
  692. # Try to preserve selection upon refresh
  693. selected_entry = self._selected_entry
  694. # Also try to preserve listbox scroll offset
  695. # If not preserved, the see() method will make wanted item to appear
  696. # at the bottom of the list, even if previously it was in center
  697. scroll_offset = self.list.yview()[0]
  698. # Show only visible entries
  699. self.shown_entries = [e for e in self.all_entries if e.visible]
  700. # Refresh listbox contents
  701. self.list.delete(0, tk.END)
  702. self.list.insert(0, *self.shown_entries)
  703. if selected_entry and not reset_selection:
  704. # Restore scroll position
  705. self.list.yview_moveto(scroll_offset)
  706. # Activate previously selected node
  707. self._select_node(selected_entry.node)
  708. else:
  709. # Select the topmost entry
  710. self.list.activate(0)
  711. self._invert_colors(0)
  712. # Select ACTION_SELECT on each refresh (mimic C menuconfig)
  713. self.tk_selected_action.set(self.ACTION_SELECT)
  714. # Display current location in configuration tree
  715. pos = []
  716. for n in self.node_stack + [self.node]:
  717. pos.append(n.prompt[0] if n.prompt else '[none]')
  718. self.label_position['text'] = u'# ' + u' -> '.join(pos)
  719. def show_node(self, node):
  720. self.node = node
  721. if node.list is not None:
  722. self.all_entries = self._collect_list_entries(node.list)
  723. else:
  724. self.all_entries = []
  725. self.refresh_display(reset_selection=True)
  726. def show_submenu(self, node):
  727. self.node_stack.append(self.node)
  728. self.show_node(node)
  729. def show_parent(self):
  730. if self.node_stack:
  731. select_node = self.node
  732. parent_node = self.node_stack.pop()
  733. self.show_node(parent_node)
  734. # Restore previous selection
  735. self._select_node(select_node)
  736. self.refresh_display(reset_selection=False)
  737. def ask_for_string(self, ident=None, title='Enter string', value=None):
  738. """
  739. Raises dialog with text entry widget and asks user to enter string
  740. Return:
  741. - str - user entered string
  742. - None - entry was cancelled
  743. """
  744. text = 'Please enter a string value\n' \
  745. 'User <Enter> key to accept the value\n' \
  746. 'Use <Esc> key to cancel entry\n'
  747. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  748. self.root.wait_window(d.dlg)
  749. self.list.focus_set()
  750. return d.value
  751. def ask_for_int(self, ident=None, title='Enter integer value', value=None, ranges=()):
  752. """
  753. Raises dialog with text entry widget and asks user to enter decimal number
  754. Ranges should be iterable of tuples (start, end),
  755. where 'start' and 'end' specify allowed value range (inclusively)
  756. Return:
  757. - int - when valid number that falls within any one of specified ranges is entered
  758. - None - invalid number or entry was cancelled
  759. """
  760. text = 'Please enter a decimal value. Fractions will not be accepted\n' \
  761. 'User <Enter> key to accept the value\n' \
  762. 'Use <Esc> key to cancel entry\n'
  763. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  764. self.root.wait_window(d.dlg)
  765. self.list.focus_set()
  766. ivalue = None
  767. if d.value:
  768. try:
  769. ivalue = int(d.value)
  770. except ValueError:
  771. messagebox.showerror('Bad value', 'Entered value \'{}\' is not an integer'.format(d.value))
  772. if ivalue is not None and ranges:
  773. allowed = False
  774. for start, end in ranges:
  775. allowed = allowed or start <= ivalue and ivalue <= end
  776. if not allowed:
  777. messagebox.showerror(
  778. 'Bad value',
  779. 'Entered value \'{:d}\' is out of range\n'
  780. 'Allowed:\n{}'.format(
  781. ivalue,
  782. '\n'.join([' {:d} - {:d}'.format(s,e) for s,e in ranges])
  783. )
  784. )
  785. ivalue = None
  786. return ivalue
  787. def ask_for_hex(self, ident=None, title='Enter hexadecimal value', value=None, ranges=()):
  788. """
  789. Raises dialog with text entry widget and asks user to enter decimal number
  790. Ranges should be iterable of tuples (start, end),
  791. where 'start' and 'end' specify allowed value range (inclusively)
  792. Return:
  793. - int - when valid number that falls within any one of specified ranges is entered
  794. - None - invalid number or entry was cancelled
  795. """
  796. text = 'Please enter a hexadecimal value\n' \
  797. 'User <Enter> key to accept the value\n' \
  798. 'Use <Esc> key to cancel entry\n'
  799. d = EntryDialog(self.root, text, title, ident=ident, value=value)
  800. self.root.wait_window(d.dlg)
  801. self.list.focus_set()
  802. hvalue = None
  803. if d.value:
  804. try:
  805. hvalue = int(d.value, base=16)
  806. except ValueError:
  807. messagebox.showerror('Bad value', 'Entered value \'{}\' is not a hexadecimal value'.format(d.value))
  808. if hvalue is not None and ranges:
  809. allowed = False
  810. for start, end in ranges:
  811. allowed = allowed or start <= hvalue and hvalue <= end
  812. if not allowed:
  813. messagebox.showerror(
  814. 'Bad value',
  815. 'Entered value \'0x{:x}\' is out of range\n'
  816. 'Allowed:\n{}'.format(
  817. hvalue,
  818. '\n'.join([' 0x{:x} - 0x{:x}'.format(s,e) for s,e in ranges])
  819. )
  820. )
  821. hvalue = None
  822. return hvalue
  823. def show_text(self, text, title='Info'):
  824. """
  825. Raises dialog with read-only text view that contains supplied text
  826. """
  827. d = TextDialog(self.root, text, title)
  828. self.root.wait_window(d.dlg)
  829. self.list.focus_set()
  830. def mark_as_changed(self):
  831. """
  832. Marks current config as having unsaved changes
  833. Should be called whenever config value is changed
  834. """
  835. self.unsaved_changes = True
  836. self.update_status()
  837. def set_status_string(self, status):
  838. """
  839. Sets status string displayed at the bottom of the window
  840. """
  841. self.status_string = status
  842. self.update_status()
  843. def update_status(self):
  844. """
  845. Updates status bar display
  846. Status bar displays:
  847. - unsaved status
  848. - current config path
  849. - status string (see set_status_string())
  850. """
  851. self.tk_status.set('{} [{}] {}'.format(
  852. '<UNSAVED>' if self.unsaved_changes else '',
  853. self.config_path if self.config_path else '',
  854. self.status_string
  855. ))
  856. def _check_is_visible(self, node):
  857. v = True
  858. v = v and node.prompt is not None
  859. # It should be enough to check if prompt expression is not false and
  860. # for menu nodes whether 'visible if' is not false
  861. v = v and kconfiglib.expr_value(node.prompt[1]) > 0
  862. if node.item == kconfiglib.MENU:
  863. v = v and kconfiglib.expr_value(node.visibility) > 0
  864. # If node references Symbol, then we also account for symbol visibility
  865. # TODO: need to re-think whether this is needed
  866. if isinstance(node.item, kconfiglib.Symbol):
  867. if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
  868. v = v and len(node.item.assignable) > 0
  869. else:
  870. v = v and node.item.visibility > 0
  871. return v
  872. def config_is_changed(self):
  873. is_changed = False
  874. node = self.kconfig.top_node.list
  875. if not node:
  876. # Empty configuration
  877. return is_changed
  878. while 1:
  879. item = node.item
  880. if isinstance(item, kconfiglib.Symbol) and item.user_value is None and self._check_is_visible(node):
  881. is_changed = True
  882. print("Config \"# {}\" has changed, need save config file\n".format(node.prompt[0]))
  883. break;
  884. # Iterative tree walk using parent pointers
  885. if node.list:
  886. node = node.list
  887. elif node.next:
  888. node = node.next
  889. else:
  890. while node.parent:
  891. node = node.parent
  892. if node.next:
  893. node = node.next
  894. break
  895. else:
  896. break
  897. return is_changed
  898. def prevent_losing_changes(self):
  899. """
  900. Checks if there are unsaved changes and asks user to save or discard them
  901. This routine should be called whenever current config is going to be discarded
  902. Raises the usual 'Yes', 'No', 'Cancel' prompt.
  903. Return:
  904. - True: caller may safely drop current config state
  905. - False: user needs to continue work on current config ('Cancel' pressed or saving failed)
  906. """
  907. if self.config_is_changed() == True:
  908. self.mark_as_changed()
  909. if not self.unsaved_changes:
  910. return True
  911. res = messagebox.askyesnocancel(
  912. parent=self.root,
  913. title='Unsaved changes',
  914. message='Config has unsaved changes. Do you want to save them?'
  915. )
  916. if res is None:
  917. return False
  918. elif res is False:
  919. return True
  920. # Otherwise attempt to save config and succeed only if config has been saved successfully
  921. saved = self.save_config()
  922. return saved
  923. def open_config(self, path=None):
  924. if path is None:
  925. # Create open dialog. Either existing file is selected or no file is selected as a result
  926. path = filedialog.askopenfilename(
  927. parent=self.root,
  928. title='Open config..',
  929. initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
  930. filetypes=(('.config files', '*.config'), ('All files', '*.*'))
  931. )
  932. if not path or not os.path.isfile(path):
  933. return False
  934. path = os.path.abspath(path)
  935. print('Loading config: \'{}\''.format(path))
  936. # Try to open given path
  937. # If path does not exist, we still set current config path to it but don't load anything
  938. self.unsaved_changes = False
  939. self.config_path = path
  940. if not os.path.exists(path):
  941. self.set_status_string('New config')
  942. self.mark_as_changed()
  943. return True
  944. # Load config and set status accordingly
  945. try:
  946. self.kconfig.load_config(path)
  947. except IOError as e:
  948. self.set_status_string('Failed to load: \'{}\''.format(path))
  949. self.refresh_display()
  950. print('Failed to load config \'{}\': {}'.format(path, e))
  951. return False
  952. self.set_status_string('Opened config')
  953. self.refresh_display()
  954. return True
  955. def save_config(self, force_file_dialog=False):
  956. path = self.config_path
  957. if path is None or force_file_dialog:
  958. path = filedialog.asksaveasfilename(
  959. parent=self.root,
  960. title='Save config as..',
  961. initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
  962. initialfile=os.path.basename(self.config_path) if self.config_path else None,
  963. defaultextension='.config',
  964. filetypes=(('.config files', '*.config'), ('All files', '*.*'))
  965. )
  966. if not path:
  967. return False
  968. path = os.path.abspath(path)
  969. print('Saving config: \'{}\''.format(path))
  970. # Try to save config to selected path
  971. try:
  972. self.kconfig.write_config(path, header="#\n# Automatically generated file; DO NOT EDIT.\n")
  973. self.unsaved_changes = False
  974. self.config_path = path
  975. self.set_status_string('Saved config')
  976. except IOError as e:
  977. self.set_status_string('Failed to save: \'{}\''.format(path))
  978. print('Save failed: {}'.format(e), file=sys.stderr)
  979. return False
  980. return True
  981. def _center_window(root, window):
  982. # type: (tk.Tk, tk.Toplevel) -> None
  983. """
  984. Attempts to center window on screen
  985. """
  986. root.update_idletasks()
  987. # root.eval('tk::PlaceWindow {!s} center'.format(
  988. # window.winfo_pathname(window.winfo_id())
  989. # ))
  990. w = window.winfo_width()
  991. h = window.winfo_height()
  992. ws = window.winfo_screenwidth()
  993. hs = window.winfo_screenheight()
  994. x = (ws / 2) - (w / 2)
  995. y = (hs / 2) - (h / 2)
  996. window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
  997. window.lift()
  998. window.focus_force()
  999. def _center_window_above_parent(root, window):
  1000. # type: (tk.Tk, tk.Toplevel) -> None
  1001. """
  1002. Attempts to center window above its parent window
  1003. """
  1004. # root.eval('tk::PlaceWindow {!s} center'.format(
  1005. # window.winfo_pathname(window.winfo_id())
  1006. # ))
  1007. root.update_idletasks()
  1008. parent = window.master
  1009. w = window.winfo_width()
  1010. h = window.winfo_height()
  1011. px = parent.winfo_rootx()
  1012. py = parent.winfo_rooty()
  1013. pw = parent.winfo_width()
  1014. ph = parent.winfo_height()
  1015. x = px + (pw / 2) - (w / 2)
  1016. y = py + (ph / 2) - (h / 2)
  1017. window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
  1018. window.lift()
  1019. window.focus_force()
  1020. def main(argv=None):
  1021. if argv is None:
  1022. argv = sys.argv[1:]
  1023. # Instantiate cmd options parser
  1024. parser = argparse.ArgumentParser(
  1025. description='Interactive Kconfig configuration editor'
  1026. )
  1027. parser.add_argument(
  1028. '--kconfig',
  1029. metavar='FILE',
  1030. type=str,
  1031. default='Kconfig',
  1032. help='path to root Kconfig file'
  1033. )
  1034. parser.add_argument(
  1035. '--config',
  1036. metavar='FILE',
  1037. type=str,
  1038. help='path to .config file to load'
  1039. )
  1040. args = parser.parse_args(argv)
  1041. kconfig_path = args.kconfig
  1042. config_path = args.config
  1043. # Verify that Kconfig file exists
  1044. if not os.path.isfile(kconfig_path):
  1045. raise RuntimeError('\'{}\': no such file'.format(kconfig_path))
  1046. # Parse Kconfig files
  1047. kconf = kconfiglib.Kconfig(filename=kconfig_path)
  1048. mc = MenuConfig(kconf)
  1049. # If config file was specified, load it
  1050. if config_path:
  1051. mc.open_config(config_path)
  1052. tk.mainloop()
  1053. if __name__ == '__main__':
  1054. main()