diff --git a/mathicsscript/__main__.py b/mathicsscript/__main__.py index 20348fa..2d65795 100755 --- a/mathicsscript/__main__.py +++ b/mathicsscript/__main__.py @@ -27,7 +27,7 @@ from mathics.core.evaluation import Evaluation, Output from mathics.core.expression import from_python from mathics.core.parser import MathicsFileLineFeeder -from mathics.core.symbols import Symbol, SymbolFalse, SymbolTrue +from mathics.core.symbols import Symbol, SymbolNull, SymbolFalse, SymbolTrue from mathics.core.systemsymbols import SymbolTeXForm from mathics.session import autoload_files @@ -113,7 +113,10 @@ def ensure_settings(): return settings_file -def load_settings(shell): +def load_settings_file(shell): + """ + Read in or "autoload" Mathics3 code to initialize some settings. + """ autoload_files(shell.definitions, get_srcdir(), "autoload") settings_file = ensure_settings() if settings_file == "": @@ -182,6 +185,7 @@ def fmt_fun(query: Any) -> Any: style = style.get_string_value() if shell.terminal_formatter: fmt = fmt_fun + shell.pygments_style = style or "None" evaluation = Evaluation(shell.definitions, output=TerminalOutput(shell)) @@ -200,7 +204,10 @@ def fmt_fun(query: Any) -> Any: ): current_pos = GNU_readline.get_current_history_length() for pos in range(last_pos, current_pos - 1): - GNU_readline.remove_history_item(pos) + try: + GNU_readline.remove_history_item(pos) + except ValueError: + pass wl_input = source_code.rstrip() if unicode: wl_input = replace_wl_with_plain_text(wl_input) @@ -434,6 +441,10 @@ def main( f"Settings`{setting_name}", from_python(True if setting_value else False) ) + if os.environ.get("NO_COLOR", False) and style not in (None, "None"): + print('Environment variable NO_COLOR set when "style" option given.') + print("NO_COLOR setting ignored.") + if post_mortem: try: from trepan.post_mortem import post_mortem_excepthook @@ -448,15 +459,20 @@ def main( readline = "none" if (code or file and not persist) else readline.lower() if readline == "prompt": shell = TerminalShellPromptToolKit( - definitions, style, completion, charset, prompt, edit_mode + definitions, completion, charset, prompt, edit_mode ) else: want_readline = readline == "gnu" shell = TerminalShellGNUReadline( - definitions, style, want_readline, completion, charset, prompt + definitions, want_readline, completion, charset, prompt ) - load_settings(shell) + load_settings_file(shell) + style_from_settings_file = definitions.get_ownvalue("Settings`$PygmentsStyle") + if style_from_settings_file is SymbolNull and style is None: + style = style_from_settings_file + shell.setup_pygments_style(style) + if file: with open(file, "r") as ifile: feeder = MathicsFileLineFeeder(ifile) diff --git a/mathicsscript/bindkeys.py b/mathicsscript/bindkeys.py index 6e0896b..3e54eb0 100644 --- a/mathicsscript/bindkeys.py +++ b/mathicsscript/bindkeys.py @@ -22,14 +22,11 @@ from prompt_toolkit.enums import EditingMode from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.filters import Condition -from prompt_toolkit.layout import Float, FloatContainer -from prompt_toolkit.widgets import Dialog, Button, Label from sys import version_info import contextlib import pathlib import re -from mathicsscript.termshell import ALL_PYGMENTS_STYLES from mathicsscript.settings import definitions from mathics.session import get_settings_value @@ -149,36 +146,6 @@ def _group_autocomplete_toggle(event): app.group_autocomplete = not app.group_autocomplete -# Add an additional key binding for toggling this flag. -@bindings.add("f5") -def _next_pygments_style(event): - """Set Pygments style to the next sytle in ALL_PYGMENTS_STYLE.""" - app = event.app - - try: - i = ALL_PYGMENTS_STYLES.index(app.pygments_style) - except ValueError: - pass - else: - i = (i + 1) % len(ALL_PYGMENTS_STYLES) - app.pygments_style = ALL_PYGMENTS_STYLES[i] - - -# Add an additional key binding for toggling this flag. -@bindings.add("f6") -def _prev_pygments_style(event): - """Set Pygments style to the previous sytle in ALL_PYGMENTS_STYLE.""" - app = event.app - - try: - i = ALL_PYGMENTS_STYLES.index(app.pygments_style) - except ValueError: - pass - else: - i = (i - 1) % len(ALL_PYGMENTS_STYLES) - app.pygments_style = ALL_PYGMENTS_STYLES[i] - - def read_inputrc(read_init_file_fn: Callable, use_unicode: bool) -> None: """ Read GNU Readline style inputrc diff --git a/mathicsscript/termshell.py b/mathicsscript/termshell.py index f467308..131b0f2 100644 --- a/mathicsscript/termshell.py +++ b/mathicsscript/termshell.py @@ -5,14 +5,12 @@ import os import os.path as osp import pathlib -import sys -from typing import Any, Optional, Union +from typing import Any, Union import mathics_scanner.location -from colorama import init as colorama_init from columnize import columnize -from mathics.core.atoms import String, Symbol +from mathics.core.atoms import Symbol from mathics.core.attributes import attribute_string_to_number from mathics.core.expression import Expression, from_python # strip_context, from mathics.core.rules import Rule @@ -32,7 +30,7 @@ mma_lexer = MathematicaLexer() -ALL_PYGMENTS_STYLES = list(get_all_styles()) +ALL_PYGMENTS_STYLES = list(get_all_styles()) + ["None"] color_scheme = TERMINAL_COLORS.copy() color_scheme[MToken.SYMBOL] = ("yellow", "ansibrightyellow") @@ -79,8 +77,7 @@ class TerminalShellCommon(MathicsLineFeeder): def __init__( self, definitions, - style: Optional[str], - _: bool, + want_completion: bool, use_unicode: bool, prompt: bool, ): @@ -94,38 +91,16 @@ def __init__( self.lineno = 0 self.terminal_formatter = None self.prompt = prompt + self.want_completion = want_completion - colorama_init() - if style == "None": - self.terminal_formatter = None - self.incolors = self.outcolors = ["", "", "", ""] - else: - # self.incolors = ["\033[34m", "\033[1m", "\033[22m", "\033[39m"] - self.incolors = ["\033[32m", "\033[1m", "\033[22m", "\033[39m"] - self.outcolors = ["\033[31m", "\033[1m", "\033[22m", "\033[39m"] - if style is not None and not is_pygments_style(style): - style = None - - if style is None: - dark_background = is_dark_background() - if dark_background: - style = "inkpot" - else: - style = "colorful" - try: - self.terminal_formatter = Terminal256Formatter(style=style) - except ClassNotFound: - print(f"Pygments style name '{style}' not found; No pygments style set") - - self.pygments_style = style self.definitions = definitions self.definitions.set_ownvalue( "Settings`$PygmentsShowTokens", from_python(False) ) - self.definitions.set_ownvalue("Settings`$PygmentsStyle", from_python(style)) self.definitions.set_ownvalue("Settings`$UseUnicode", from_python(use_unicode)) self.definitions.set_ownvalue( - "Settings`PygmentsStylesAvailable", from_python(ALL_PYGMENTS_STYLES) + "Settings`PygmentsStylesAvailable", + from_python(ALL_PYGMENTS_STYLES), ) self.definitions.add_message( @@ -149,6 +124,7 @@ def __init__( "Settings`PygmentsStylesAvailable", attribute_string_to_number["System`Locked"], ) + self.definitions.set_attribute( "Settings`$UseUnicode", attribute_string_to_number["System`Locked"] ) @@ -156,13 +132,20 @@ def __init__( def change_pygments_style(self, style: str): if not style or style == self.pygments_style: return False + if style == "None": + self.terminal_formatter = None + self.pygments_style = style + self.incolors = self.outcolors = ["", "", "", ""] + return True if is_pygments_style(style): + self.incolors = ["\033[32m", "\033[1m", "\033[22m", "\033[39m"] + self.outcolors = ["\033[31m", "\033[1m", "\033[22m", "\033[39m"] self.terminal_formatter = Terminal256Formatter(style=style) self.pygments_style = style return True - else: - print("Pygments style not changed") - return False + + print("Pygments style not changed") + return False def empty(self): return False @@ -189,19 +172,30 @@ def get_out_prompt(self, form: str) -> Union[str, Any]: default form, or the name of the Form which was used in output preceded by "//" """ line_number = self.last_line_number - return "{2}Out[{3}{0}{4}]{5}{1}= ".format(line_number, form, *self.outcolors) + if self.is_styled: + return "{2}Out[{3}{0}{4}]{5}{1}= ".format( + line_number, form, *self.outcolors + ) + else: + return f"Out[{line_number}]= " @property def in_prompt(self) -> Union[str, Any]: next_line_number = self.last_line_number + 1 if self.lineno > 0: return " " * len(f"In[{next_line_number}]:= ") - else: + elif self.is_styled: return "{1}In[{2}{0}{3}]:= {4}".format(next_line_number, *self.incolors) - # if have_full_readline: - # return "{1}In[{2}{0}{3}]:= {4}".format(next_line_number, *self.incolors) - # else: - # return f"In[{next_line_number}]:= " + else: + return f"In[{next_line_number}]:= " + + @property + def is_styled(self): + """ + Returns True if a Pygments style (other than SymbolNull or "None" has been set. + """ + style = self.definitions.get_ownvalue("Settings`$PygmentsStyle") + return not (style is SymbolNull or style.value == "None") @property def last_line_number(self) -> int: @@ -213,7 +207,8 @@ def last_line_number(self) -> int: def out_callback(self, out): print(self.to_output(str(out), form="")) - def read_line(self, prompt, completer=None, use_html=None): + # noinspection PyUnusedLocal + def read_line(self, prompt, _completer=None, _use_html: bool = False): if self.using_readline: line = self.rl_read_line(prompt) else: @@ -253,6 +248,19 @@ def print_result( use_highlight = False else: out_str = '"' + out_str.replace('"', r"\"") + '"' + + show_pygments_tokens = self.definitions.get_ownvalue( + "Settings`$PygmentsShowTokens" + ).to_python() + pygments_style = self.definitions.get_ownvalue( + "Settings`$PygmentsStyle" + ).get_string_value() + if pygments_style != self.pygments_style: + if not self.change_pygments_style(pygments_style): + self.definitions.set_ownvalue( + "Settings`$PygmentsStyle", String(self.pygments_style) + ) + if eval_type == "System`Graph": out_str = "-Graph-" elif self.terminal_formatter: # pygmentize @@ -273,7 +281,8 @@ def print_result( if show_pygments_tokens: print(list(lex(out_str, mma_lexer))) if use_highlight: - out_str = highlight(out_str, mma_lexer, self.terminal_formatter) + if self.terminal_formatter is not None: + out_str = highlight(out_str, mma_lexer, self.terminal_formatter) form = ( "" if not hasattr(result, "form") or result.form is None @@ -296,10 +305,49 @@ def rl_read_line(self, prompt): def reset_lineno(self): self.lineno = 0 + def setup_pygments_style(self, style): + """Goes through what we need to do to setup or change a + Pygments style. + """ + if ( + isinstance(style, str) + and style.lower() == "none" + or style is None + and os.environ.get("NO_COLOR", False) + ): + style = "None" # Canonicalize spelling + self.terminal_formatter = None + self.incolors = self.outcolors = ["", "", "", ""] + else: + # self.incolors = ["\033[34m", "\033[1m", "\033[22m", "\033[39m"] + self.incolors = ["\033[32m", "\033[1m", "\033[22m", "\033[39m"] + self.outcolors = ["\033[31m", "\033[1m", "\033[22m", "\033[39m"] + if style is not None and not is_pygments_style(style): + style = None + + # If no style given, choose one based on the background. + if style is None: + dark_background = is_dark_background() + if dark_background: + style = "inkpot" + else: + style = "colorful" + try: + self.terminal_formatter = Terminal256Formatter(style=style) + except ClassNotFound: + print(f"Pygments style name '{style}' not found; No pygments style set") + style = "None" + + self.definitions.set_ownvalue("Settings`$PygmentsStyle", from_python(style)) + self.pygments_style = style + def to_output(self, text: str, form: str) -> str: """ Format an 'Out=' line that it lines after the first one indent properly. """ line_number = self.last_line_number - newline = "\n" + " " * len(f"Out[{line_number}]{form}= ") + if self.is_styled: + newline = "\n" + " " * len(f"Out[{line_number}]{form}= ") + else: + newline = "\n" + " " * len(f"Out[{line_number}]= ") return newline.join(text.splitlines()) diff --git a/mathicsscript/termshell_gnu.py b/mathicsscript/termshell_gnu.py index ee5e378..51bf6ae 100644 --- a/mathicsscript/termshell_gnu.py +++ b/mathicsscript/termshell_gnu.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2020-2022 Rocky Bernstein +# Copyright (C) 2020-2022, 2025 Rocky Bernstein import atexit import os @@ -59,15 +59,12 @@ class TerminalShellGNUReadline(TerminalShellCommon): def __init__( self, definitions, - style: str, want_readline: bool, want_completion: bool, use_unicode: bool, prompt: bool, ): - super(TerminalShellGNUReadline, self).__init__( - definitions, style, want_completion, use_unicode, prompt - ) + super().__init__(definitions, want_completion, use_unicode, prompt) # Try importing readline to enable arrow keys support etc. self.using_readline = False diff --git a/mathicsscript/termshell_prompt.py b/mathicsscript/termshell_prompt.py index f35925a..8bc3748 100644 --- a/mathicsscript/termshell_prompt.py +++ b/mathicsscript/termshell_prompt.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (C) 2021-2022, 2025 Rocky Bernstein -import locale import os import os.path as osp import re @@ -10,13 +9,8 @@ from colorama import init as colorama_init from mathics.core.atoms import String -from mathics.core.attributes import attribute_string_to_number -from mathics.core.expression import Expression, from_python -from mathics.core.rules import Rule from mathics.core.symbols import SymbolNull, SymbolFalse, SymbolTrue -from mathics.core.systemsymbols import SymbolMessageName from mathics_pygments.lexer import MathematicaLexer, MToken -from mathics_scanner.location import ContainerKind from prompt_toolkit import HTML, PromptSession, print_formatted_text from prompt_toolkit.application.current import get_app from prompt_toolkit.enums import EditingMode @@ -24,60 +18,34 @@ from prompt_toolkit.lexers import PygmentsLexer from prompt_toolkit.styles.pygments import style_from_pygments_cls from pygments import format, highlight, lex -from pygments.formatters import Terminal256Formatter -from pygments.formatters.terminal import TERMINAL_COLORS from pygments.styles import get_style_by_name -from pygments.util import ClassNotFound - -# FIXME: __main__ shouldn't be needed. Fix term_background -from term_background.__main__ import is_dark_background from mathicsscript.bindkeys import bindings, read_init_file, read_inputrc from mathicsscript.completion import MathicsCompleter from mathicsscript.termshell import ( - ALL_PYGMENTS_STYLES, - CONFIGDIR, + HISTFILE, HISTSIZE, USER_INPUTRC, ShellEscapeException, - SymbolPygmentsStylesAvailable, TerminalShellCommon, - is_pygments_style, + mma_lexer, ) from mathicsscript.version import __version__ -mma_lexer = MathematicaLexer() - -color_scheme = TERMINAL_COLORS.copy() -color_scheme[MToken.SYMBOL] = ("yellow", "ansibrightyellow") -color_scheme[MToken.BUILTIN] = ("ansigreen", "ansibrightgreen") -color_scheme[MToken.OPERATOR] = ("magenta", "ansibrightmagenta") -color_scheme[MToken.NUMBER] = ("ansiblue", "ansibrightblue") - -HISTFILE = osp.join(CONFIGDIR, "history-ptk") - class TerminalShellPromptToolKit(TerminalShellCommon): def __init__( self, definitions, - style: Optional[str], want_completion: bool, use_unicode: bool, prompt: bool, edit_mode: Optional[str], ): - super(TerminalShellCommon, self).__init__([], ContainerKind.STREAM) - self.input_encoding = locale.getpreferredencoding() - - # is_inside_interrupt is set True when shell has been - # interrupted via an interrupt handler. - self.is_inside_interrupt = False + super().__init__(definitions, want_completion, use_unicode, prompt) - self.lineno = 0 - self.terminal_formatter = None + colorama_init() self.mma_pygments_lexer = PygmentsLexer(MathematicaLexer) - self.prompt = prompt self.session = PromptSession(history=FileHistory(HISTFILE)) if edit_mode is not None: @@ -91,41 +59,6 @@ def __init__( self.using_readline = sys.stdin.isatty() and sys.stdout.isatty() self.ansi_color_re = re.compile("\033\\[[0-9;]+m") - colorama_init() - if style == "None": - self.terminal_formatter = None - self.incolors = self.outcolors = ["", "", "", ""] - else: - # self.incolors = ["\033[34m", "\033[1m", "\033[22m", "\033[39m"] - self.incolors = ["\033[32m", "\033[1m", "\033[22m", "\033[39m"] - self.outcolors = ["\033[31m", "\033[1m", "\033[22m", "\033[39m"] - if style is not None and not is_pygments_style(style): - style = None - - if style is None: - dark_background = is_dark_background() - if dark_background: - style = "inkpot" - else: - style = "colorful" - try: - self.terminal_formatter = Terminal256Formatter(style=style) - except ClassNotFound: - sys.stderr.write( - f"Pygments style name '{style}' not found; No pygments style set\n" - ) - - self.pygments_style = style - self.definitions = definitions - self.definitions.set_ownvalue( - "Settings`$PygmentsShowTokens", from_python(False) - ) - self.definitions.set_ownvalue("Settings`$PygmentsStyle", from_python(style)) - self.definitions.set_ownvalue("Settings`$UseUnicode", from_python(use_unicode)) - self.definitions.set_ownvalue( - "Settings`PygmentsStylesAvailable", from_python(ALL_PYGMENTS_STYLES) - ) - read_inputrc(read_init_file, use_unicode=use_unicode) if osp.isfile(USER_INPUTRC): if os.access(USER_INPUTRC, os.R_OK): @@ -135,30 +68,6 @@ def __init__( f"Can't read user inputrc file {USER_INPUTRC}; skipping\n" ) - self.definitions.add_message( - "Settings`PygmentsStylesAvailable", - Rule( - Expression( - SymbolMessageName, - SymbolPygmentsStylesAvailable, - from_python("usage"), - ), - from_python( - "A list of Pygments style that are valid in Settings`$PygmentsStyle." - ), - ), - ) - self.definitions.set_attribute( - "Settings`PygmentsStylesAvailable", - attribute_string_to_number["System`Protected"], - ) - self.definitions.set_attribute( - "Settings`PygmentsStylesAvailable", - attribute_string_to_number["System`Locked"], - ) - self.definitions.set_attribute( - "Settings`$UseUnicode", attribute_string_to_number["System`Locked"] - ) self.completer = MathicsCompleter(self.definitions) if want_completion else None def bottom_toolbar(self): @@ -170,9 +79,7 @@ def bottom_toolbar(self): app.help_mode = False if app.help_mode: - return HTML( - "f1: help, f3: toggle autocomplete, f4: toggle edit mode, f5: next style, f6: previous style" - ) + return HTML("f1: help, f3: toggle autocomplete, f4: toggle edit mode") # The first time around, app.group_autocomplete has not been set, # so use the value from Settings`GroupAutocomplete. @@ -193,28 +100,26 @@ def bottom_toolbar(self): app.group_autocomplete = True self.definitions.set_ownvalue("Settings`$GroupAutocomplete", SymbolTrue) - if hasattr(app, "pygments_style"): - self.definitions.set_ownvalue( - "Settings`$PygmentsStyle", String(app.pygments_style) - ) - elif self.definitions.get_ownvalue("Settings`$PygmentsStyle") is not SymbolNull: - app.pygments_style = self.definitions.get_ownvalue( + if self.definitions.get_ownvalue("Settings`$PygmentsStyle") is not SymbolNull: + value = self.definitions.get_ownvalue( "Settings`$PygmentsStyle" - ) + ).get_string_value() + if value is not None and len(value) and value[0] == value[-1] == '"': + value = value[1:-1] + pygments_style = value else: - # First time around and there is no value set via - app.pygments_style = self.pygments_style - self.definitions.set_ownvalue( - "Settings`$PygmentsStyle", String(app.pygments_style) - ) + pygments_style = self.pygments_style edit_mode = "Vi" if app.editing_mode == EditingMode.VI else "Emacs" return HTML( - f" mathicsscript: {__version__}, Style: {app.pygments_style}, Mode: {edit_mode}, Autobrace: {app.group_autocomplete}" + f" mathicsscript: {__version__}, Style: {pygments_style}, Mode: {edit_mode}, Autobrace: {app.group_autocomplete}, f1: Help" ) def errmsg(self, message: str): - print_formatted_text(HTML(f"{message}")) + if self.is_styled: + print_formatted_text(HTML(f"{message}")) + else: + print_formatted_text(HTML(f"{message}")) def get_out_prompt(self, form: str) -> Union[str, HTML]: """ @@ -223,7 +128,10 @@ def get_out_prompt(self, form: str) -> Union[str, HTML]: default form. """ line_number = self.last_line_number - return HTML(f"Out[{line_number}]{form}= ") + if self.is_styled: + return HTML(f"Out[{line_number}]{form}= ") + else: + return HTML(f"Out[{line_number}]= ") @property def in_prompt(self) -> Union[str, HTML]: @@ -231,7 +139,10 @@ def in_prompt(self) -> Union[str, HTML]: if self.lineno > 0: return " " * len(f"In[{next_line_number}]:= ") else: - return HTML(f"In[{next_line_number}]:= ") + if self.is_styled: + return HTML(f"In[{next_line_number}]:= ") + else: + return HTML(f"In[{next_line_number}]:= ") def print_result( self, result, prompt: bool, output_style="", strict_wl_output=False @@ -263,25 +174,29 @@ def print_result( use_highlight = False else: out_str = '"' + out_str.replace('"', r"\"") + '"' + + show_pygments_tokens = self.definitions.get_ownvalue( + "Settings`$PygmentsShowTokens" + ).to_python() + pygments_style = self.definitions.get_ownvalue( + "Settings`$PygmentsStyle" + ).get_string_value() + if pygments_style != self.pygments_style: + if not self.change_pygments_style(pygments_style): + self.definitions.set_ownvalue( + "Settings`$PygmentsStyle", String(self.pygments_style) + ) + if eval_type == "System`Graph": out_str = "-Graph-" elif self.terminal_formatter: # pygmentize - show_pygments_tokens = self.definitions.get_ownvalue( - "Settings`$PygmentsShowTokens" - ).to_python() - pygments_style = self.definitions.get_ownvalue( - "Settings`$PygmentsStyle" - ).get_string_value() - if pygments_style != self.pygments_style: - if not self.change_pygments_style(pygments_style): - self.definitions.set_ownvalue( - "Settings`$PygmentsStyle", String(self.pygments_style) - ) if show_pygments_tokens: print(list(lex(out_str, mma_lexer))) if use_highlight: - out_str = highlight(out_str, mma_lexer, self.terminal_formatter) + if self.terminal_formatter is not None: + out_str = highlight(out_str, mma_lexer, self.terminal_formatter) + output = self.to_output(out_str, form="") if output_style == "text" or not prompt: print(output) @@ -294,12 +209,17 @@ def print_result( print_formatted_text(self.get_out_prompt(form=form), end="") print(output + "\n") else: - print(self.get_out_prompt() + output + "\n") + print(str(self.get_out_prompt(form="")) + output + "\n") def read_line(self, prompt, completer=None, use_html: bool = False): # FIXME set and update inside self. - style = style_from_pygments_cls(get_style_by_name(self.pygments_style)) + style = ( + style_from_pygments_cls(get_style_by_name(self.pygments_style)) + if self.pygments_style != "None" + else None + ) + if completer is None: completer = self.completer diff --git a/test/test_completion.py b/test/test_completion.py index 40788d4..b50a1cc 100644 --- a/test/test_completion.py +++ b/test/test_completion.py @@ -15,7 +15,6 @@ def test_completion_gnu(): definitions = Definitions(add_builtin=True, extension_modules=[]) term = TerminalShellGNUReadline( definitions=definitions, - style=None, want_readline=True, want_completion=True, use_unicode=False,