Skip to content

Commit

Permalink
Allow both "light" and "dark" text colors
Browse files Browse the repository at this point in the history
Introduce a distinction between light and dark text colors. Use different default colors in both cases.

The text color scheme can be set via a command line option (--color=[light | dark]), or Julia can try to detect the terminal's background color. In this case, it roughly follows vim's approach:
1. Look for COLORFGBG environment variable
2. Use an ANSI escape sequence to query the terminal for the background color
3. Make an educated guess from the terminal name

Tested with iTerm2, Terminal, xterm on MacOS, Linux.
  • Loading branch information
eschnett committed Nov 28, 2018
1 parent 7acacfc commit afbbff4
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 26 deletions.
61 changes: 41 additions & 20 deletions base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,30 @@
## and REPL

have_color = false
default_color_warn = :yellow
default_color_error = :light_red
default_color_info = :cyan
default_color_debug = :blue
default_color_input = :normal
default_color_answer = :normal
text_color_dark = false

# Default light and dark colors
default_color_light_warn = :yellow
default_color_light_error = :light_red
default_color_light_info = :cyan
default_color_light_debug = :blue
default_color_light_input = :normal
default_color_light_answer = :normal

default_color_dark_warn = :magenta
default_color_dark_error = :red
default_color_dark_info = :blue
default_color_dark_debug = :cyan
default_color_dark_input = :normal
default_color_dark_answer = :normal

default_color_warn() = text_color_dark ? default_color_dark_warn : default_color_light_warn
default_color_error() = text_color_dark ? default_color_dark_error : default_color_light_error
default_color_info() = text_color_dark ? default_color_dark_info : default_color_light_info
default_color_debug() = text_color_dark ? default_color_dark_debug : default_color_light_debug
default_color_input() = text_color_dark ? default_color_dark_input : default_color_light_input
default_color_answer() = text_color_dark ? default_color_dark_answer : default_color_light_answer

color_normal = text_colors[:normal]

function repl_color(key, default)
Expand All @@ -19,13 +37,13 @@ function repl_color(key, default)
haskey(text_colors, c_conv) ? c_conv : default
end

error_color() = repl_color("JULIA_ERROR_COLOR", default_color_error)
warn_color() = repl_color("JULIA_WARN_COLOR" , default_color_warn)
info_color() = repl_color("JULIA_INFO_COLOR" , default_color_info)
debug_color() = repl_color("JULIA_DEBUG_COLOR" , default_color_debug)
error_color() = repl_color("JULIA_ERROR_COLOR", default_color_error())
warn_color() = repl_color("JULIA_WARN_COLOR" , default_color_warn())
info_color() = repl_color("JULIA_INFO_COLOR" , default_color_info())
debug_color() = repl_color("JULIA_DEBUG_COLOR" , default_color_debug())

input_color() = text_colors[repl_color("JULIA_INPUT_COLOR", default_color_input)]
answer_color() = text_colors[repl_color("JULIA_ANSWER_COLOR", default_color_answer)]
input_color() = text_colors[repl_color("JULIA_INPUT_COLOR", default_color_input())]
answer_color() = text_colors[repl_color("JULIA_ANSWER_COLOR", default_color_answer())]

stackframe_lineinfo_color() = repl_color("JULIA_STACKFRAME_LINEINFO_COLOR", :bold)
stackframe_function_color() = repl_color("JULIA_STACKFRAME_FUNCTION_COLOR", :bold)
Expand Down Expand Up @@ -195,12 +213,14 @@ function exec_options(opts)
idxs = findall(x -> x == "--", ARGS)
length(idxs) > 0 && deleteat!(ARGS, idxs[1])
end
quiet = (opts.quiet != 0)
startup = (opts.startupfile != 2)
history_file = (opts.historyfile != 0)
color_set = (opts.color != 0) # --color!=auto
global have_color = (opts.color == 1) # --color=on
global is_interactive = (opts.isinteractive != 0)
quiet = (opts.quiet != 0)
startup = (opts.startupfile != 2)
history_file = (opts.historyfile != 0)
color_set = (opts.color != 0) # --color!=auto
global have_color = (opts.color >= 2) # --color=[on|light|dark]
text_color_set = (opts.color >= 3) # --color=[light|dark]
global text_color_dark = (opts.color == 4) # --color=dark
global is_interactive = (opts.isinteractive != 0)

# pre-process command line argument list
arg_is_program = !isempty(ARGS)
Expand Down Expand Up @@ -274,7 +294,7 @@ function exec_options(opts)
else
banner = (opts.banner == 1) # --banner=yes
end
run_main_repl(interactiveinput, quiet, banner, history_file, color_set)
run_main_repl(interactiveinput, quiet, banner, history_file, color_set, text_color_set)
end
nothing
end
Expand Down Expand Up @@ -321,7 +341,7 @@ _atreplinit(repl) = invokelatest(__atreplinit, repl)
const REPL_MODULE_REF = Ref{Module}()

# run the requested sort of evaluation loop on stdio
function run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_file::Bool, color_set::Bool)
function run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_file::Bool, color_set::Bool, text_color_set::Bool)
global active_repl
# load interactive-only libraries
if !isdefined(Main, :InteractiveUtils)
Expand All @@ -340,6 +360,7 @@ function run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_fil
term_env = get(ENV, "TERM", @static Sys.iswindows() ? "" : "dumb")
term = REPL.Terminals.TTYTerminal(term_env, stdin, stdout, stderr)
color_set || (global have_color = REPL.Terminals.hascolor(term))
text_color_set || (global text_color_dark = REPL.Terminals.has_light_background(term))
banner && Base.banner(term)
if term.term_type == "dumb"
active_repl = REPL.BasicREPL(term)
Expand Down
3 changes: 2 additions & 1 deletion base/loading.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1102,10 +1102,11 @@ function create_expr_cache(input::String, output::String, concrete_deps::typeof(
eval(Meta.parse(code))
end
"""
color = have_color ? (text_color_dark ? "dark" : "light") : "no"
io = open(pipeline(detach(`$(julia_cmd()) -O0
--output-ji $output --output-incremental=yes
--startup-file=no --history-file=no --warn-overwrite=yes
--color=$(have_color ? "yes" : "no")
--color=$color
--eval $code_object`), stderr=stderr),
"w", stdout)
in = io.in
Expand Down
9 changes: 7 additions & 2 deletions src/jloptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ static const char opts[] =
" -i Interactive mode; REPL runs and isinteractive() is true\n"
" -q, --quiet Quiet startup: no banner, suppress REPL warnings\n"
" --banner={yes|no|auto} Enable or disable startup banner\n"
" --color={yes|no|auto} Enable or disable color text\n"
" --color={yes|no|light|dark|auto}\n"
" Enable or disable color text, and/or choose a text color scheme\n"
" --history-file={yes|no} Load or save history\n\n"

// error and warning options
Expand Down Expand Up @@ -402,10 +403,14 @@ JL_DLLEXPORT void jl_parse_opts(int *argcp, char ***argvp)
jl_options.color = JL_OPTIONS_COLOR_ON;
else if (!strcmp(optarg, "no"))
jl_options.color = JL_OPTIONS_COLOR_OFF;
else if (!strcmp(optarg, "light"))
jl_options.color = JL_OPTIONS_COLOR_LIGHT;
else if (!strcmp(optarg, "dark"))
jl_options.color = JL_OPTIONS_COLOR_DARK;
else if (!strcmp(optarg, "auto"))
jl_options.color = JL_OPTIONS_COLOR_AUTO;
else
jl_errorf("julia: invalid argument to --color={yes|no|auto} (%s)", optarg);
jl_errorf("julia: invalid argument to --color={yes|no|light|dark|auto} (%s)", optarg);
break;
case opt_history_file:
if (!strcmp(optarg,"yes"))
Expand Down
6 changes: 4 additions & 2 deletions src/julia.h
Original file line number Diff line number Diff line change
Expand Up @@ -1844,8 +1844,10 @@ JL_DLLEXPORT int jl_generating_output(void) JL_NOTSAFEPOINT;
#define JL_OPTIONS_COMPILE_MIN 3

#define JL_OPTIONS_COLOR_AUTO 0
#define JL_OPTIONS_COLOR_ON 1
#define JL_OPTIONS_COLOR_OFF 2
#define JL_OPTIONS_COLOR_OFF 1
#define JL_OPTIONS_COLOR_ON 2
#define JL_OPTIONS_COLOR_LIGHT 3
#define JL_OPTIONS_COLOR_DARK 4

#define JL_OPTIONS_HISTORYFILE_ON 1
#define JL_OPTIONS_HISTORYFILE_OFF 0
Expand Down
98 changes: 97 additions & 1 deletion stdlib/REPL/src/Terminals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export
getX,
getY,
hascolor,
has_light_background,
pos,
raw!

Expand Down Expand Up @@ -74,6 +75,7 @@ cmove_col(t::TextTerminal, c) = cmove(c, getY(t))

# Defaults
hascolor(::TextTerminal) = false
has_light_background(::TextTerminal) = false

# Utility Functions
width(t::TextTerminal) = displaysize(t)[2]
Expand Down Expand Up @@ -169,7 +171,101 @@ else
end
end

# use cached value of have_color
function has_light_background(term::TTYTerminal)
# Don't bother if there is no support for colors
!hascolor(term) && return false

# This function is called when the user did not explicitly choose
# either "light" or "dark" text color via a command line option.
# We try various ways to determine the terminal's background
# color, and use the first valid result. This is inspired by vim,
# which uses a very similar approach:
#
# 1. First check the environment variable COLORFGBG
# 2. Use the OSC 11 escape sequence to query the terminal
# 3. Make an educated guess from the terminal name

# See
# <https://unix.stackexchange.com/questions/245378/common-environment-variable-to-set-dark-or-light-terminal-background>
# for how to detect the terminal background color

# Check COLORFGBG environment variable, which is set by rxvt and
# derivatives. This variable contains either two or three values
# separated by semicolons; we want the last value in either case.
# If this value is 0-6 or 8, our background is dark.
colorfgbg = get(ENV, "COLORFGBG", "")
sep = findlast(";", colorfgbg)
if sep !== nothing
# See e.g. <https://en.wikipedia.org/wiki/ANSI_escape_code>
# for the 16 possible colors
colorbg = tryparse(Int, colorfgbg[last(sep)+1:end])
if colorbg !== nothing && colorbg >= 0 && colorbg <= 15
return colorbg == 7 || colorbg >= 9
end
end

# Use OSC 11 escape sequence
BEL = "\a" # "\007"
ESC = "\e" # "\033"
OSC = "$(ESC)]"
send = "$(OSC)11;?$(BEL)"
reply = mktemp() do path, io
# Shell script idea taken from
# <https://stackoverflow.com/questions/2507337/is-there-a-way-to-determine-a-terminals-background-color>
cmds = ["printf %s '$send'",
"sleep 0.00000001", # xterm needs the sleep
"read -r recv",
"printf %s \"\$recv\" >'$path'"]
cmd = join(cmds, "; ")

oldstty = chomp(read(open(`stty -g`, "r", term.in_stream).out, String))
try
read(open(`stty raw -echo min 0 time 0`, "r", term.in_stream).out,
String)
run(pipeline(`/bin/sh -c $cmd`;
stdin=term.in_stream, stdout=term.out_stream))
finally
read(open(pipeline(`stty $oldstty`; stderr=devnull),
"r", term.in_stream).out, String)
end
String(read(io))
end
# We expect a reply such as "$(OSC)11;rgb:00/00/00$(BEL)" or
# "$(OSC)11;rgba:0000/0000/0000/0000$(BEL)" or similar
if startswith(reply, "$(OSC)11;rgb")
col = findfirst(":", reply)
bel = findfirst(BEL, reply)
if col !== nothing && bel !== nothing
colors = reply[col.stop+1 : bel.start-1]
rgb = Float64[]
for str in split(colors, "/")
val = tryparse(UInt, str, base=16)
if val !== nothing
maxval = UInt(1) << (4 * length(str)) - UInt(1)
val /= maxval # normalize
push!(rgb, val)
end
end
if length(rgb) >= 3
R, G, B = rgb
Y = 0.299 * R + 0.587 * G + 0.114 * B
return Y >= 0.5
end
end
end

# Guess from terminal name.
# This list of terminal names is taken from vim.
if term.term_type == "linux" || # Linux console
term.term_type == "screen.linux" || # Linux console with screen
startswith(term.term_type, "cygwin") || # Cygwin shell
startswith(term.term_type, "putty") # Putty progrem
return false
else
return true
end
end

Base.in(key_value::Pair, t::TTYTerminal) = in(key_value, pipe_writer(t))
Base.haskey(t::TTYTerminal, key) = haskey(pipe_writer(t), key)
Base.getindex(t::TTYTerminal, key) = getindex(pipe_writer(t), key)
Expand Down
5 changes: 5 additions & 0 deletions test/cmdlineargs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ let exename = `$(Base.julia_cmd()) --sysimage-native-code=yes --startup-file=no`
# --color
@test readchomp(`$exename --color=yes -E "Base.have_color"`) == "true"
@test readchomp(`$exename --color=no -E "Base.have_color"`) == "false"
@test readchomp(`$exename --color=light -E "Base.have_color"`) == "true"
@test readchomp(`$exename --color=dark -E "Base.have_color"`) == "true"
@test readchomp(`$exename --color=light -E "Base.text_color_dark"`) == "false"
@test readchomp(`$exename --color=dark -E "Base.text_color_dark"`) == "true"
@test !success(`$exename --color=true`)
@test !success(`$exename --color=false`)

# --history-file
Expand Down

0 comments on commit afbbff4

Please sign in to comment.