import os, sys import lesscpy from shutil import copyfile, rmtree from jupyter_core.paths import jupyter_config_dir, jupyter_data_dir from glob import glob from tempfile import mkstemp # path to local site-packages/jupyterthemes package_dir = os.path.dirname(os.path.realpath(__file__)) # path to user jupyter-themes dir user_dir = os.path.join(os.path.expanduser('~'), '.jupyter-themes') # path to save tempfile with style_less before reading/compiling _, tempfile = mkstemp('.less') _, vimtemp = mkstemp('.less') # path to install custom.css file (~/.jupyter/custom/) jupyter_home = jupyter_config_dir() jupyter_data = jupyter_data_dir() jupyter_custom = os.path.join(jupyter_home, 'custom') jupyter_custom_fonts = os.path.join(jupyter_custom, 'fonts') jupyter_customcss = os.path.join(jupyter_custom, 'custom.css') jupyter_customjs = os.path.join(jupyter_custom, 'custom.js') jupyter_nbext = os.path.join(jupyter_data, 'nbextensions') # theme colors, layout, and font directories layouts_dir = os.path.join(package_dir, 'layout') styles_dir = os.path.join(package_dir, 'styles') styles_dir_user = os.path.join(user_dir, 'styles') fonts_dir = os.path.join(package_dir, 'fonts') defaults_dir = os.path.join(package_dir, 'defaults') # default custom.css/js files to override JT on reset defaultCSS = os.path.join(defaults_dir, 'custom.css') defaultJS = os.path.join(defaults_dir, 'custom.js') # layout files for notebook, codemirror, cells, mathjax, & vim ext nb_style = os.path.join(layouts_dir, 'notebook.less') cm_style = os.path.join(layouts_dir, 'codemirror.less') cl_style = os.path.join(layouts_dir, 'cells.less') ex_style = os.path.join(layouts_dir, 'extras.less') vim_style = os.path.join(layouts_dir, 'vim.less') comp_style = os.path.join(layouts_dir, 'completer.less') theme_name_file = os.path.join(jupyter_custom, 'current_theme.txt') def fileOpen(filename, mode): if sys.version_info[0]==3: return open(filename, mode, encoding='utf8', errors='ignore') else: return open(filename, mode) def check_directories(): # Ensure all install dirs exist if not os.path.isdir(jupyter_home): os.makedirs(jupyter_home) if not os.path.isdir(jupyter_custom): os.makedirs(jupyter_custom) if not os.path.isdir(jupyter_custom_fonts): os.makedirs(jupyter_custom_fonts) if not os.path.isdir(jupyter_data): os.makedirs(jupyter_data) if not os.path.isdir(jupyter_nbext): os.makedirs(jupyter_nbext) def less_to_css(style_less): """ write less-compiled css file to jupyter_customcss in jupyter_dir """ with fileOpen(tempfile, 'w') as f: f.write(style_less) os.chdir(package_dir) style_css = lesscpy.compile(tempfile) style_css += '\n\n' return style_css def write_final_css(style_css): # install style_css to .jupyter/custom/custom.css with fileOpen(jupyter_customcss, 'w') as custom_css: custom_css.write(style_css) def install_precompiled_theme(theme): # for Python 3.5, install selected theme from precompiled defaults compiled_dir = os.path.join(styles_dir, 'compiled') compiled_dir_user = os.path.join(styles_dir_user, 'compiled') if (os.path.isdir(compiled_dir_user) and '{}.css'.format(theme) in os.listdir(compiled_dir_user)): theme_src = os.path.join(compiled_dir_user, '{}.css'.format(theme)) else: theme_src = os.path.join(compiled_dir, '{}.css'.format(theme)) theme_dst = os.path.join(jupyter_custom, 'custom.css') copyfile(theme_src, theme_dst) def send_fonts_to_jupyter(font_file_path): fname = font_file_path.split(os.sep)[-1] copyfile(font_file_path, os.path.join(jupyter_custom_fonts, fname)) def delete_font_files(): for fontfile in os.listdir(jupyter_custom_fonts): abspath = os.path.join(jupyter_custom_fonts, fontfile) os.remove(abspath) def convert_fontsizes(fontsizes): # if triple digits, move decimal (105 --> 10.5) fontsizes = [str(fs) for fs in fontsizes] for i, fs in enumerate(fontsizes): if len(fs) >= 3: fontsizes[i] = '.'.join([fs[:-1], fs[-1]]) elif int(fs) > 25: fontsizes[i] = '.'.join([fs[0], fs[-1]]) return fontsizes def set_font_properties(style_less, nbfont=None, tcfont=None, monofont=None, monosize=11, tcfontsize=13, nbfontsize=13, prfontsize=95, dffontsize=93, outfontsize=85, mathfontsize=100, dfonts=False): """Parent function for setting notebook, text/md, and codecell font-properties """ fontsizes = [monosize, nbfontsize, tcfontsize, prfontsize, dffontsize, outfontsize] monosize, nbfontsize, tcfontsize, prfontsize, dffontsize, outfontsize = convert_fontsizes(fontsizes) if dfonts==True: monofont, tcfont, nbfont = ['monospace', 'sans-serif', 'sans-serif'] else: if monofont is not None: monofont, monofpath = stored_font_dicts(monofont) style_less = import_fonts(style_less, monofont, monofpath) else: monofont='monospace' if tcfont is not None: tcfont, tcfontpath = stored_font_dicts(tcfont) style_less = import_fonts(style_less, tcfont, tcfontpath) else: tcfont='sans-serif' if nbfont is not None: if nbfont == 'proxima': nbfont, tcfont = ["'Proxima Nova'"]*2 style_less = proxima_nova_imports(style_less) else: nbfont, nbfontpath = stored_font_dicts(nbfont) style_less = import_fonts(style_less, nbfont, nbfontpath) else: nbfont='sans-serif' style_less += '/* Set Font-Type and Font-Size Variables */\n' # font names and fontfamily info for codecells, notebook & textcells style_less += '@monofont: {}; \n'.format(monofont) style_less += '@notebook-fontfamily: {}; \n'.format(nbfont) style_less += '@text-cell-fontfamily: {}; \n'.format(tcfont) # font size for codecells, main notebook, notebook-sub, & textcells style_less += '@monofontsize: {}pt; \n'.format(monosize) style_less += '@monofontsize-sub: {}pt; \n'.format(float(monosize) - 1) style_less += '@nb-fontsize: {}pt; \n'.format(nbfontsize) style_less += '@nb-fontsize-sub: {}pt; \n'.format(float(nbfontsize) - 1) style_less += '@text-cell-fontsize: {}pt; \n'.format(tcfontsize) style_less += '@df-header-fontsize: {}pt; \n'.format(float(dffontsize) + 1) style_less += '@df-fontsize: {}pt; \n'.format(dffontsize) style_less += '@output-font-size: {}pt; \n'.format(outfontsize) style_less += '@prompt-fontsize: {}pt; \n'.format(prfontsize) style_less += '@mathfontsize: {}%; \n'.format(mathfontsize) style_less += '\n\n' style_less += '/* Import Theme Colors and Define Layout Variables */\n' return style_less def import_fonts(style_less, fontname, font_subdir): """Copy all custom fonts to ~/.jupyter/custom/fonts/ and write import statements to style_less """ ftype_dict = {'woff2': 'woff2', 'woff': 'woff', 'ttf': 'truetype', 'otf': 'opentype', 'svg': 'svg'} define_font = ( "@font-face {{font-family: {fontname};\n\tfont-weight:" "{weight};\n\tfont-style: {style};\n\tsrc: local('{fontname}')," "\n\turl('fonts{sepp}{fontfile}') format('{ftype}');}}\n") fontname = fontname.split(',')[0] fontpath = os.path.join(fonts_dir, font_subdir) for fontfile in os.listdir(fontpath): if '.txt' in fontfile or 'DS_' in fontfile: continue weight = 'normal' style = 'normal' if 'medium' in fontfile: weight = 'medium' elif 'ital' in fontfile: style = 'italic' ft = ftype_dict[fontfile.split('.')[-1]] style_less += define_font.format( fontname=fontname, weight=weight, style=style, sepp='/', fontfile=fontfile, ftype=ft) send_fonts_to_jupyter(os.path.join(fontpath, fontfile)) return style_less def style_layout(style_less, theme='grade3', cursorwidth=2, cursorcolor='default', cellwidth='980', lineheight=170, margins='auto', vimext=False, toolbar=False, nbname=False, kernellogo=False, altprompt=False, altmd=False, altout=False, hideprompt=False): """Set general layout and style properties of text and code cells""" # write theme name to ~/.jupyter/custom/ (referenced by jtplot.py) with fileOpen(theme_name_file, 'w') as f: f.write(theme) if (os.path.isdir(styles_dir_user) and '{}.less'.format(theme) in os.listdir(styles_dir_user)): theme_relpath = os.path.relpath( os.path.join(styles_dir_user, theme), package_dir) else: theme_relpath = os.path.relpath( os.path.join(styles_dir, theme), package_dir) style_less += '@import "{}";\n'.format(theme_relpath) textcell_bg = '@cc-input-bg' promptText = '@input-prompt' promptBG = '@cc-input-bg' promptPadding = '.25em' promptBorder = '2px solid @prompt-line' tcPromptBorder = '2px solid @tc-prompt-std' promptMinWidth = 11.5 outpromptMinWidth = promptMinWidth # remove + 3 since it will overlay output print() text tcPromptWidth = promptMinWidth + 3 tcPromptFontsize = "@prompt-fontsize" ccOutputBG = '@cc-output-bg-default' if theme == 'grade3': textcell_bg = '@notebook-bg' if altprompt: promptPadding = '.1em' promptMinWidth = 8 outpromptMinWidth = promptMinWidth + 3 tcPromptWidth = promptMinWidth + 3 promptText = 'transparent' tcPromptBorder = '2px solid transparent' if altmd: textcell_bg = '@notebook-bg' tcPromptBorder = '2px dotted @tc-border-selected' if altout: ccOutputBG = '@notebook-bg' if margins != 'auto': margins = '{}px'.format(margins) if '%' not in cellwidth: cellwidth = str(cellwidth) + 'px' style_less += '@container-margins: {};\n'.format(margins) style_less += '@cell-width: {}; \n'.format(cellwidth) style_less += '@cc-line-height: {}%; \n'.format(lineheight) style_less += '@text-cell-bg: {}; \n'.format(textcell_bg) style_less += '@cc-prompt-width: {}ex; \n'.format(promptMinWidth) style_less += '@cc-prompt-bg: {}; \n'.format(promptBG) style_less += '@cc-output-bg: {}; \n'.format(ccOutputBG) style_less += '@prompt-text: {}; \n'.format(promptText) style_less += '@prompt-padding: {}; \n'.format(promptPadding) style_less += '@prompt-border: {}; \n'.format(promptBorder) style_less += '@prompt-min-width: {}ex; \n'.format(promptMinWidth) style_less += '@out-prompt-min-width: {}ex; \n'.format(outpromptMinWidth) style_less += '@tc-prompt-width: {}ex; \n'.format(tcPromptWidth) style_less += '@tc-prompt-border: {}; \n'.format(tcPromptBorder) style_less += '@cursor-width: {}px; \n'.format(cursorwidth) style_less += '@cursor-info: @cursor-width solid {}; \n'.format( cursorcolor) style_less += '@tc-prompt-fontsize: {}; \n'.format(tcPromptFontsize) style_less += '\n\n' # read-in notebook.less (general nb style) with fileOpen(nb_style, 'r') as notebook: style_less += notebook.read() + '\n' # read-in cells.less (cell layout) with fileOpen(cl_style, 'r') as cells: style_less += cells.read() + '\n' # read-in extras.less (misc layout) with fileOpen(ex_style, 'r') as extras: style_less += extras.read() + '\n' # read-in codemirror.less (syntax-highlighting) with fileOpen(cm_style, 'r') as codemirror: style_less += codemirror.read() + '\n' with fileOpen(comp_style, 'r') as codemirror: style_less += codemirror.read() + '\n' style_less += toggle_settings( toolbar, nbname, hideprompt, kernellogo) + '\n' if vimext: set_vim_style(theme) return style_less def toggle_settings( toolbar=False, nbname=False, hideprompt=False, kernellogo=False): """Toggle main notebook toolbar (e.g., buttons), filename, and kernel logo.""" toggle = '' if toolbar: toggle += 'div#maintoolbar {margin-left: 8px !important;}\n' toggle += '.toolbar.container {width: 100% !important;}\n' else: toggle += 'div#maintoolbar {display: none !important;}\n' if nbname: toggle += ('span.save_widget span.filename {margin-left: 8px; height: initial;' 'font-size: 100%; color: @nb-name-fg; background-color:' '@cc-input-bg;}\n') toggle += ('span.save_widget span.filename:hover {color:' '@nb-name-hover; background-color: @cc-input-bg;}\n') toggle += ('#menubar {padding-top: 4px; background-color:' '@notebook-bg;}\n') else: toggle += '#header-container {display: none !important;}\n' if hideprompt: toggle += 'div.prompt.input_prompt {display: none !important;}\n' toggle += 'div.prompt.output_prompt {width: 5ex !important;}\n' toggle += 'div.out_prompt_overlay.prompt:hover {width: 5ex !important; min-width: 5ex !important;}\n' toggle += ( '.CodeMirror-gutters, .cm-s-ipython .CodeMirror-gutters' '{ position: absolute; left: 0; top: 0; z-index: 3; width: 2em; ' 'display: inline-block !important; }\n') toggle += ('div.cell.code_cell .input { border-left: 5px solid @cm-gutters !important; border-bottom-left-radius: 5px; border-top-left-radius: 5px; }\n') if kernellogo: toggle += '@kernel-logo-display: block;' else: toggle += '@kernel-logo-display: none;' return toggle def proxima_nova_imports(style_less): style_less += """@font-face { font-family: 'Proxima Nova Bold'; src: url('fonts/Proxima Nova Alt Bold-webfont.eot'); src: url('fonts/Proxima Nova Alt Bold-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/Proxima Nova Alt Bold-webfont.woff2') format('woff2'), url('fonts/Proxima Nova Alt Bold-webfont.woff') format('woff'), url('fonts/Proxima Nova Alt Bold-webfont.ttf') format('truetype'), url('fonts/Proxima Nova Alt Bold-webfont.svg#proxima_nova_altbold') format('svg'); font-weight: 600; font-style: normal; } @font-face { font-family: 'Proxima Nova'; src: url('fonts/Proxima Nova Alt Regular-webfont.eot'); src: url('fonts/Proxima Nova Alt Regular-webfont.eot?#iefix') format('embedded-opentype'), url('fonts/Proxima Nova Alt Regular-webfont.woff') format('woff'), url('fonts/Proxima Nova Alt Regular-webfont.ttf') format('truetype'), url('fonts/Proxima Nova Alt Regular-webfont.svg#proxima_nova_altregular') format('svg'); font-weight: 400; font-style: normal; }""" font_subdir = os.path.join(fonts_dir, "sans-serif/proximasans") fontpath = os.path.join(fonts_dir, font_subdir) for fontfile in os.listdir(font_subdir): send_fonts_to_jupyter(os.path.join(fontpath, fontfile)) return style_less def set_mathjax_style(style_css, mathfontsize): """Write mathjax settings, set math fontsize """ jax_style = """ """ % (int(mathfontsize), '"{}%"'.format(str(mathfontsize))) style_css += jax_style return style_css def set_vim_style(theme): """Add style and compatibility with vim notebook extension""" vim_jupyter_nbext = os.path.join(jupyter_nbext, 'vim_binding') if not os.path.isdir(vim_jupyter_nbext): os.makedirs(vim_jupyter_nbext) vim_less = '@import "styles{}";\n'.format(''.join([os.sep, theme])) with open(vim_style, 'r') as vimstyle: vim_less += vimstyle.read() + '\n' with open(vimtemp, 'w') as vtemp: vtemp.write(vim_less) os.chdir(package_dir) vim_css = lesscpy.compile(vimtemp) vim_css += '\n\n' # install vim_custom_css to ...nbextensions/vim_binding/vim_binding.css vim_custom_css = os.path.join(vim_jupyter_nbext, 'vim_binding.css') with open(vim_custom_css, 'w') as vim_custom: vim_custom.write(vim_css) def reset_default(verbose=False): """Remove custom.css and custom fonts""" paths = [jupyter_custom, jupyter_nbext] for fpath in paths: custom = '{0}{1}{2}.css'.format(fpath, os.sep, 'custom') try: os.remove(custom) except Exception: pass try: delete_font_files() except Exception: check_directories() delete_font_files() copyfile(defaultCSS, jupyter_customcss) copyfile(defaultJS, jupyter_customjs) if os.path.exists(theme_name_file): os.remove(theme_name_file) if verbose: print("Reset css and font defaults in:\n{} &\n{}".format(*paths)) def set_nb_theme(name): """Set theme from within notebook """ from IPython.core.display import HTML styles_dir = os.path.join(package_dir, 'styles/compiled/') css_path = glob('{0}/{1}.css'.format(styles_dir, name))[0] customcss = open(css_path, "r").read() return HTML(''.join([''])) def get_colors(theme='grade3', c='default', get_dict=False): if theme == 'grade3': cdict = {'default': '#ff711a', 'b': '#1e70c7', 'o': '#ff711a', 'r': '#e22978', 'p': '#AA22FF', 'g': '#2ecc71'} else: cdict = {'default': '#0095ff', 'b': '#0095ff', 'o': '#ff914d', 'r': '#DB797C', 'p': '#c776df', 'g': '#94c273'} cdict['x'] = '@cc-input-fg' if get_dict: return cdict return cdict[c] def get_alt_prompt_text_color(theme): altColors = {'grade3': '#FF7823', 'oceans16': '#667FB1', 'chesterish': '#0b98c8', 'onedork': '#94c273', 'monokai': '#94c273'} return altColors[theme] def stored_font_dicts(fontcode, get_all=False): fonts = {'mono': {'anka': ['Anka/Coder', 'anka-coder'], 'anonymous': ['Anonymous Pro', 'anonymous-pro'], 'aurulent': ['Aurulent Sans Mono', 'aurulent'], 'bitstream': ['Bitstream Vera Sans Mono', 'bitstream-vera'], 'bpmono': ['BPmono', 'bpmono'], 'code': ['Code New Roman', 'code-new-roman'], 'consolamono': ['Consolamono', 'consolamono'], 'cousine': ['Cousine', 'cousine'], 'dejavu': ['DejaVu Sans Mono', 'dejavu'], 'droidmono': ['Droid Sans Mono', 'droidmono'], 'fira': ['Fira Mono', 'fira'], 'firacode': ['Fira Code', 'firacode'], 'generic': ['Generic Mono', 'generic'], 'hack': ['Hack', 'hack'], 'hasklig': ['Hasklig', 'hasklig'], 'iosevka' : ['Iosevka', 'iosevka'], 'inputmono': ['Input Mono', 'inputmono'], 'inconsolata': ['Inconsolata-g', 'inconsolata-g'], 'liberation': ['Liberation Mono', 'liberation'], 'meslo': ['Meslo', 'meslo'], 'office': ['Office Code Pro', 'office-code-pro'], 'oxygen': ['Oxygen Mono', 'oxygen'], 'roboto': ['Roboto Mono', 'roboto'], 'saxmono': ['saxMono', 'saxmono'], 'source': ['Source Code Pro', 'source-code-pro'], 'sourcemed': ['Source Code Pro Medium', 'source-code-medium'], 'ptmono': ['PT Mono', 'ptmono'], 'ubuntu': ['Ubuntu Mono', 'ubuntu']}, 'sans': {'droidsans': ['Droid Sans', 'droidsans'], 'opensans': ['Open Sans', 'opensans'], 'ptsans': ['PT Sans', 'ptsans'], 'sourcesans': ['Source Sans Pro', 'sourcesans'], 'robotosans': ['Roboto', 'robotosans'], 'latosans': ['Lato', 'latosans'], 'exosans': ['Exo_2', 'exosans'], 'proxima': ['Proxima Nova', 'proximasans']}, 'serif': {'ptserif': ['PT Serif', 'ptserif'], 'ebserif': ['EB Garamond', 'ebserif'], 'loraserif': ['Lora', 'loraserif'], 'merriserif': ['Merriweather', 'merriserif'], 'crimsonserif': ['Crimson Text', 'crimsonserif'], 'georgiaserif': ['Georgia', 'georgiaserif'], 'neutonserif': ['Neuton', 'neutonserif'], 'cardoserif': ['Cardo Serif', 'cardoserif'], 'goudyserif': ['Goudy Serif', 'goudyserif']}} if get_all: return fonts if fontcode in list(fonts['mono']): fontname, fontdir = fonts['mono'][fontcode] fontfam = 'monospace' elif fontcode in list(fonts['sans']): fontname, fontdir = fonts['sans'][fontcode] fontfam = 'sans-serif' elif fontcode in list(fonts['serif']): fontname, fontdir = fonts['serif'][fontcode] fontfam = 'serif' else: print("\n\tOne of the fonts you requested is not available\n\tSetting all fonts to default") return '' fontdir = os.sep.join([fontfam, fontdir]) return '"{}", {}'.format(fontname, fontfam), fontdir