1from __future__ import absolute_import, division, print_function
2
3__copyright__ = """
4Copyright (C) 2009-2017 Andreas Kloeckner
5Copyright (C) 2014-2017 Aaron Meurer
6"""
7
8__license__ = """
9Permission is hereby granted, free of charge, to any person obtaining a copy
10of this software and associated documentation files (the "Software"), to deal
11in the Software without restriction, including without limitation the rights
12to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13copies of the Software, and to permit persons to whom the Software is
14furnished to do so, subject to the following conditions:
15
16The above copyright notice and this permission notice shall be included in
17all copies or substantial portions of the Software.
18
19THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25THE SOFTWARE.
26"""
27
28import os
29import sys
30
31from pudb.py3compat import ConfigParser
32from pudb.lowlevel import (lookup_module, get_breakpoint_invalid_reason,
33                           settings_log)
34
35# minor LGPL violation: stolen from python-xdg
36
37_home = os.environ.get("HOME", None)
38xdg_data_home = os.environ.get("XDG_DATA_HOME",
39            os.path.join(_home, ".local", "share") if _home else None)
40
41
42XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME",
43                                 os.path.join(_home, ".config") if _home else None)
44
45if XDG_CONFIG_HOME:
46    XDG_CONFIG_DIRS = [XDG_CONFIG_HOME]
47else:
48    XDG_CONFIG_DIRS = os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":")
49
50
51def get_save_config_path(*resource):
52    if XDG_CONFIG_HOME is None:
53        return None
54    if not resource:
55        resource = [XDG_CONF_RESOURCE]
56    resource = os.path.join(*resource)
57    assert not resource.startswith("/")
58    path = os.path.join(XDG_CONFIG_HOME, resource)
59    if not os.path.isdir(path):
60        os.makedirs(path, 448)  # 0o700
61    return path
62
63# end LGPL violation
64
65
66CONF_SECTION = "pudb"
67XDG_CONF_RESOURCE = "pudb"
68CONF_FILE_NAME = "pudb.cfg"
69
70SAVED_BREAKPOINTS_FILE_NAME = "saved-breakpoints-%d.%d" % sys.version_info[:2]
71BREAKPOINTS_FILE_NAME = "breakpoints-%d.%d" % sys.version_info[:2]
72
73
74_config_ = [None]
75
76
77def load_config():
78    # Only ever do this once
79    if _config_[0] is not None:
80        return _config_[0]
81
82    from os.path import join, isdir
83
84    cparser = ConfigParser()
85
86    conf_dict = {}
87    try:
88        cparser.read([
89            join(cdir, XDG_CONF_RESOURCE, CONF_FILE_NAME)
90            for cdir in XDG_CONFIG_DIRS if isdir(cdir)])
91
92        if cparser.has_section(CONF_SECTION):
93            conf_dict.update(dict(cparser.items(CONF_SECTION)))
94    except Exception:
95        settings_log.exception("Failed to load config")
96
97    conf_dict.setdefault("shell", "internal")
98    conf_dict.setdefault("theme", "classic")
99    conf_dict.setdefault("line_numbers", "False")
100    conf_dict.setdefault("seen_welcome", "a")
101
102    conf_dict.setdefault("sidebar_width", 0.5)
103    conf_dict.setdefault("variables_weight", 1)
104    conf_dict.setdefault("stack_weight", 1)
105    conf_dict.setdefault("breakpoints_weight", 1)
106
107    conf_dict.setdefault("current_stack_frame", "top")
108
109    conf_dict.setdefault("stringifier", "type")
110
111    conf_dict.setdefault("custom_theme", "")
112    conf_dict.setdefault("custom_stringifier", "")
113    conf_dict.setdefault("custom_shell", "")
114
115    conf_dict.setdefault("wrap_variables", "True")
116    conf_dict.setdefault("default_variables_access_level", "public")
117
118    conf_dict.setdefault("display", "auto")
119
120    conf_dict.setdefault("prompt_on_quit", "True")
121
122    conf_dict.setdefault("hide_cmdline_win", "False")
123
124    def normalize_bool_inplace(name):
125        try:
126            if conf_dict[name].lower() in ["0", "false", "off"]:
127                conf_dict[name] = False
128            else:
129                conf_dict[name] = True
130        except Exception:
131            settings_log.exception("Failed to process config")
132
133    normalize_bool_inplace("line_numbers")
134    normalize_bool_inplace("wrap_variables")
135    normalize_bool_inplace("prompt_on_quit")
136    normalize_bool_inplace("hide_cmdline_win")
137
138    _config_[0] = conf_dict
139    return conf_dict
140
141
142def save_config(conf_dict):
143    from os.path import join
144
145    cparser = ConfigParser()
146    cparser.add_section(CONF_SECTION)
147
148    for key in sorted(conf_dict):
149        cparser.set(CONF_SECTION, key, str(conf_dict[key]))
150
151    try:
152        save_path = get_save_config_path()
153        if not save_path:
154            return
155        outf = open(join(save_path, CONF_FILE_NAME), "w")
156        cparser.write(outf)
157        outf.close()
158    except Exception:
159        settings_log.exception("Failed to save config")
160
161
162def edit_config(ui, conf_dict):
163    import urwid
164
165    old_conf_dict = conf_dict.copy()
166
167    def _update_theme():
168        ui.setup_palette(ui.screen)
169        ui.screen.clear()
170
171    def _update_line_numbers():
172        for sl in ui.source:
173            sl._invalidate()
174
175    def _update_prompt_on_quit():
176        pass
177
178    def _update_hide_cmdline_win():
179        ui.update_cmdline_win()
180
181    def _update_current_stack_frame():
182        ui.update_stack()
183
184    def _update_stringifier():
185        import pudb.var_view
186        pudb.var_view.custom_stringifier_dict = {}
187        ui.update_var_view()
188
189    def _update_default_variables_access_level():
190        ui.update_var_view()
191
192    def _update_wrap_variables():
193        ui.update_var_view()
194
195    def _update_config(check_box, new_state, option_newvalue):
196        option, newvalue = option_newvalue
197        new_conf_dict = {option: newvalue}
198        if option == "theme":
199            # only activate if the new state of the radio button is 'on'
200            if new_state:
201                if newvalue is None:
202                    # Select the custom theme entry dialog
203                    lb.set_focus(lb_contents.index(theme_edit_list_item))
204                    return
205
206                conf_dict.update(theme=newvalue)
207                _update_theme()
208
209        elif option == "line_numbers":
210            new_conf_dict["line_numbers"] = not check_box.get_state()
211            conf_dict.update(new_conf_dict)
212            _update_line_numbers()
213
214        elif option == "prompt_on_quit":
215            new_conf_dict["prompt_on_quit"] = not check_box.get_state()
216            conf_dict.update(new_conf_dict)
217            _update_prompt_on_quit()
218
219        elif option == "hide_cmdline_win":
220            new_conf_dict["hide_cmdline_win"] = not check_box.get_state()
221            conf_dict.update(new_conf_dict)
222            _update_hide_cmdline_win()
223
224        elif option == "current_stack_frame":
225            # only activate if the new state of the radio button is 'on'
226            if new_state:
227                conf_dict.update(new_conf_dict)
228                _update_current_stack_frame()
229
230        elif option == "stringifier":
231            # only activate if the new state of the radio button is 'on'
232            if new_state:
233                if newvalue is None:
234                    lb.set_focus(lb_contents.index(stringifier_edit_list_item))
235                    return
236
237                conf_dict.update(stringifier=newvalue)
238                _update_stringifier()
239
240        elif option == "default_variables_access_level":
241            # only activate if the new state of the radio button is 'on'
242            if new_state:
243                conf_dict.update(default_variables_access_level=newvalue)
244                _update_default_variables_access_level()
245
246        elif option == "wrap_variables":
247            new_conf_dict["wrap_variables"] = not check_box.get_state()
248            conf_dict.update(new_conf_dict)
249            _update_wrap_variables()
250
251    heading = urwid.Text("This is the preferences screen for PuDB. "
252        "Hit Ctrl-P at any time to get back to it.\n\n"
253        "Configuration settings are saved in "
254        "$HOME/.config/pudb or $XDG_CONFIG_HOME/pudb "
255        "environment variable. If both variables are not set "
256        "configurations settings will not be saved.\n")
257
258    cb_line_numbers = urwid.CheckBox("Show Line Numbers",
259            bool(conf_dict["line_numbers"]), on_state_change=_update_config,
260                user_data=("line_numbers", None))
261
262    cb_prompt_on_quit = urwid.CheckBox("Prompt before quitting",
263            bool(conf_dict["prompt_on_quit"]), on_state_change=_update_config,
264                user_data=("prompt_on_quit", None))
265
266    hide_cmdline_win = urwid.CheckBox("Hide command line (Ctrl-X) window "
267                                      "when not in use",
268            bool(conf_dict["hide_cmdline_win"]), on_state_change=_update_config,
269                user_data=("hide_cmdline_win", None))
270
271    # {{{ shells
272
273    shell_info = urwid.Text("This is the shell that will be "
274            "used when you hit '!'.\n")
275    shells = ["internal", "classic", "ipython", "bpython", "ptpython", "ptipython"]
276    known_shell = conf_dict["shell"] in shells
277    shell_edit = urwid.Edit(edit_text=conf_dict["custom_shell"])
278    shell_edit_list_item = urwid.AttrMap(shell_edit, "value")
279
280    shell_rb_group = []
281    shell_rbs = [
282            urwid.RadioButton(shell_rb_group, name,
283                conf_dict["shell"] == name)
284            for name in shells]+[
285                urwid.RadioButton(shell_rb_group, "Custom:",
286                not known_shell, on_state_change=_update_config,
287                user_data=("shell", None)),
288                shell_edit_list_item,
289                urwid.Text("\nTo use a custom shell, see example-shell.py "
290                    "in the pudb distribution. Enter the full path to a "
291                    "file like it in the box above. '~' will be expanded "
292                    "to your home directory. The file should contain a "
293                    "function called pudb_shell(_globals, _locals) "
294                    "at the module level. See the PuDB documentation for "
295                    "more information."),
296            ]
297
298    # }}}
299
300    # {{{ themes
301
302    from pudb.theme import THEMES
303
304    known_theme = conf_dict["theme"] in THEMES
305
306    theme_rb_group = []
307    theme_edit = urwid.Edit(edit_text=conf_dict["custom_theme"])
308    theme_edit_list_item = urwid.AttrMap(theme_edit, "value")
309    theme_rbs = [
310            urwid.RadioButton(theme_rb_group, name,
311                conf_dict["theme"] == name, on_state_change=_update_config,
312                user_data=("theme", name))
313            for name in THEMES]+[
314                urwid.RadioButton(theme_rb_group, "Custom:",
315                    not known_theme, on_state_change=_update_config,
316                    user_data=("theme", None)),
317                theme_edit_list_item,
318                urwid.Text("\nTo use a custom theme, see example-theme.py in the "
319                    "pudb distribution. Enter the full path to a file like it in "
320                    "the box above. '~' will be expanded to your home directory. "
321                    "Note that a custom theme will not be applied until you close "
322                    "this dialog."),
323            ]
324
325    # }}}
326
327    # {{{ stack
328
329    stack_rb_group = []
330    stack_opts = ["top", "bottom"]
331    stack_info = urwid.Text("Show the current stack frame at the\n")
332    stack_rbs = [
333            urwid.RadioButton(stack_rb_group, name,
334                conf_dict["current_stack_frame"] == name,
335                on_state_change=_update_config,
336                user_data=("current_stack_frame", name))
337            for name in stack_opts
338            ]
339
340    # }}}
341
342    # {{{ stringifier
343
344    stringifier_opts = ["type", "str", "repr"]
345    known_stringifier = conf_dict["stringifier"] in stringifier_opts
346    stringifier_rb_group = []
347    stringifier_edit = urwid.Edit(edit_text=conf_dict["custom_stringifier"])
348    stringifier_info = urwid.Text("This is the default function that will be "
349        "called on variables in the variables list.  Note that you can change "
350        "this on a per-variable basis by selecting a variable and hitting Enter "
351        "or by typing t/s/r.  Note that str and repr will be slower than type "
352        "and have the potential to crash PuDB.\n")
353    stringifier_edit_list_item = urwid.AttrMap(stringifier_edit, "value")
354    stringifier_rbs = [
355            urwid.RadioButton(stringifier_rb_group, name,
356                conf_dict["stringifier"] == name,
357                on_state_change=_update_config,
358                user_data=("stringifier", name))
359            for name in stringifier_opts
360            ]+[
361                urwid.RadioButton(stringifier_rb_group, "Custom:",
362                    not known_stringifier, on_state_change=_update_config,
363                    user_data=("stringifier", None)),
364                stringifier_edit_list_item,
365                urwid.Text("\nTo use a custom stringifier, see "
366                    "example-stringifier.py in the pudb distribution. Enter the "
367                    "full path to a file like it in the box above. "
368                    "'~' will be expanded to your home directory. "
369                    "The file should contain a function called pudb_stringifier() "
370                    "at the module level, which should take a single argument and "
371                    "return the desired string form of the object passed to it. "
372                    "Note that if you choose a custom stringifier, the variables "
373                    "view will not be updated until you close this dialog."),
374            ]
375
376    # }}}
377
378    # {{{ variables access level
379
380    default_variables_access_level_opts = ["public", "private", "all"]
381    default_variables_access_level_rb_group = []
382    default_variables_access_level_info = urwid.Text(
383            "Set the default attribute visibility "
384            "of variables in the variables list.\n"
385            "\nNote that you can change this option on "
386            "a per-variable basis by selecting the "
387            "variable and pressing '*'.")
388    default_variables_access_level_rbs = [
389            urwid.RadioButton(default_variables_access_level_rb_group, name,
390                conf_dict["default_variables_access_level"] == name,
391                on_state_change=_update_config,
392                user_data=("default_variables_access_level", name))
393            for name in default_variables_access_level_opts
394            ]
395
396    # }}}
397
398    # {{{ wrap variables
399
400    cb_wrap_variables = urwid.CheckBox("Wrap variables",
401            bool(conf_dict["wrap_variables"]), on_state_change=_update_config,
402                user_data=("wrap_variables", None))
403
404    wrap_variables_info = urwid.Text("\nNote that you can change this option on "
405                                     "a per-variable basis by selecting the "
406                                     "variable and pressing 'w'.")
407
408    # }}}
409
410    # {{{ display
411
412    display_info = urwid.Text("What driver is used to talk to your terminal. "
413            "'raw' has the most features (colors and highlighting), "
414            "but is only correct for "
415            "XTerm and terminals like it. 'curses' "
416            "has fewer "
417            "features, but it will work with just about any terminal. 'auto' "
418            "will attempt to pick between the two based on availability and "
419            "the $TERM environment variable.\n\n"
420            "Changing this setting requires a restart of PuDB.")
421
422    displays = ["auto", "raw", "curses"]
423
424    display_rb_group = []
425    display_rbs = [
426            urwid.RadioButton(display_rb_group, name,
427                conf_dict["display"] == name)
428            for name in displays]
429
430    # }}}
431
432    lb_contents = (
433            [heading]
434            + [urwid.AttrMap(urwid.Text("General:\n"), "group head")]
435            + [cb_line_numbers]
436            + [cb_prompt_on_quit]
437            + [hide_cmdline_win]
438
439            + [urwid.AttrMap(urwid.Text("\nShell:\n"), "group head")]
440            + [shell_info]
441            + shell_rbs
442
443            + [urwid.AttrMap(urwid.Text("\nTheme:\n"), "group head")]
444            + theme_rbs
445
446            + [urwid.AttrMap(urwid.Text("\nStack Order:\n"), "group head")]
447            + [stack_info]
448            + stack_rbs
449
450            + [urwid.AttrMap(urwid.Text("\nVariable Stringifier:\n"), "group head")]
451            + [stringifier_info]
452            + stringifier_rbs
453
454            + [urwid.AttrMap(urwid.Text("\nVariables Attribute Visibility:\n"),
455                "group head")]
456            + [default_variables_access_level_info]
457            + default_variables_access_level_rbs
458
459            + [urwid.AttrMap(urwid.Text("\nWrap Variables:\n"), "group head")]
460            + [cb_wrap_variables]
461            + [wrap_variables_info]
462
463            + [urwid.AttrMap(urwid.Text("\nDisplay driver:\n"), "group head")]
464            + [display_info]
465            + display_rbs
466            )
467
468    lb = urwid.ListBox(urwid.SimpleListWalker(lb_contents))
469
470    if ui.dialog(lb,         [
471            ("OK", True),
472            ("Cancel", False),
473            ],
474            title="Edit Preferences"):
475        # Only update the settings here that instant-apply (above) doesn't take
476        # care of.
477
478        # if we had a custom theme, it wasn't updated live
479        if theme_rb_group[-1].state:
480            newvalue = theme_edit.get_edit_text()
481            conf_dict.update(theme=newvalue, custom_theme=newvalue)
482            _update_theme()
483
484        # Ditto for custom stringifiers
485        if stringifier_rb_group[-1].state:
486            newvalue = stringifier_edit.get_edit_text()
487            conf_dict.update(stringifier=newvalue, custom_stringifier=newvalue)
488            _update_stringifier()
489
490        if shell_rb_group[-1].state:
491            newvalue = shell_edit.get_edit_text()
492            conf_dict.update(shell=newvalue, custom_shell=newvalue)
493        else:
494            for shell, shell_rb in zip(shells, shell_rbs):
495                if shell_rb.get_state():
496                    conf_dict["shell"] = shell
497
498        for display, display_rb in zip(displays, display_rbs):
499            if display_rb.get_state():
500                conf_dict["display"] = display
501
502    else:  # The user chose cancel, revert changes
503        conf_dict.update(old_conf_dict)
504        _update_theme()
505        # _update_line_numbers() is equivalent to _update_theme()
506        _update_current_stack_frame()
507        _update_stringifier()
508
509
510# {{{ breakpoint saving
511
512def parse_breakpoints(lines):
513    # b [ (filename:lineno | function) [, "condition"] ]
514
515    breakpoints = []
516    for arg in lines:
517        if not arg:
518            continue
519        arg = arg[1:]
520
521        filename = None
522        lineno = None
523        cond = None
524        comma = arg.find(",")
525
526        if comma > 0:
527            # parse stuff after comma: "condition"
528            cond = arg[comma+1:].lstrip()
529            arg = arg[:comma].rstrip()
530
531        colon = arg.rfind(":")
532        funcname = None
533
534        if colon > 0:
535            filename = arg[:colon].strip()
536
537            f = lookup_module(filename)
538            if not f:
539                continue
540            else:
541                filename = f
542
543            arg = arg[colon+1:].lstrip()
544            try:
545                lineno = int(arg)
546            except ValueError:
547                continue
548        else:
549            continue
550
551        if get_breakpoint_invalid_reason(filename, lineno) is None:
552            breakpoints.append((filename, lineno, False, cond, funcname))
553
554    return breakpoints
555
556
557def get_breakpoints_file_name():
558    from os.path import join
559    save_path = get_save_config_path()
560    if not save_path:
561        return None
562    else:
563        return join(save_path, SAVED_BREAKPOINTS_FILE_NAME)
564
565
566def load_breakpoints():
567    """
568    Loads and check saved breakpoints out from files
569    Returns: list of tuples
570    """
571    from os.path import join, isdir
572
573    file_names = []
574    for cdir in XDG_CONFIG_DIRS:
575        if isdir(cdir):
576            for name in [SAVED_BREAKPOINTS_FILE_NAME, BREAKPOINTS_FILE_NAME]:
577                file_names.append(join(cdir, XDG_CONF_RESOURCE, name))
578
579    lines = []
580    for fname in file_names:
581        try:
582            rc_file = open(fname, "rt")
583        except IOError:
584            pass
585        else:
586            lines.extend([line.strip() for line in rc_file.readlines()])
587            rc_file.close()
588
589    return parse_breakpoints(lines)
590
591
592def save_breakpoints(bp_list):
593    """
594    :arg bp_list: a list of `bdb.Breakpoint` objects
595    """
596    save_path = get_breakpoints_file_name()
597    if not save_path:
598        return
599
600    histfile = open(get_breakpoints_file_name(), "w")
601    bp_list = set([(bp.file, bp.line, bp.cond) for bp in bp_list])
602    for bp in bp_list:
603        line = "b %s:%d" % (bp[0], bp[1])
604        if bp[2]:
605            line += ", %s" % bp[2]
606        line += "\n"
607        histfile.write(line)
608    histfile.close()
609
610# }}}
611
612# vim:foldmethod=marker
613