36
   37:- module(ansi_term,
   38          [ ansi_format/3,                 39            ansi_get_color/2,              40            ansi_hyperlink/2,              41            ansi_hyperlink/3               42          ]).   43:- autoload(library(error), [domain_error/2, must_be/2, instantiation_error/1]).   44:- autoload(library(lists), [append/3]).   45:- autoload(library(utf8), [utf8_codes/3]).   46
   69
   70:- multifile
   71    prolog:console_color/2,                        72    supports_get_color/0,
   73    hyperlink/2,                                   74    tty_url_hook/2.                                75
   76color_term_flag_default(true) :-
   77    stream_property(user_input, tty(true)),
   78    stream_property(user_error, tty(true)),
   79    stream_property(user_output, tty(true)),
   80    \+ getenv('TERM', dumb),
   81    !.
   82color_term_flag_default(false).
   83
   84init_color_term_flag :-
   85    color_term_flag_default(Default),
   86    create_prolog_flag(color_term, Default,
   87                       [ type(boolean),
   88                         keep(true)
   89                       ]),
   90    create_prolog_flag(hyperlink_term, false,
   91                       [ type(boolean),
   92                         keep(true)
   93                       ]).
   94
   95:- init_color_term_flag.   96
   97
   98:- meta_predicate
   99    keep_line_pos(+, 0).  100
  101:- multifile
  102    user:message_property/2.  103
  143
  144ansi_format(Attr, Format, Args) :-
  145    ansi_format(current_output, Attr, Format, Args).
  146
  147ansi_format(Stream, Class, Format, Args) :-
  148    stream_property(Stream, tty(true)),
  149    current_prolog_flag(color_term, true),
  150    class_attrs(Class, Attr),
  151    Attr \== [],
  152    !,
  153    phrase(sgr_codes_ex(Attr), Codes),
  154    atomic_list_concat(Codes, ;, Code),
  155    with_output_to(
  156        Stream,
  157        (   keep_line_pos(current_output, format('\e[~wm', [Code])),
  158            format(Format, Args),
  159            keep_line_pos(current_output, format('\e[0m'))
  160        )
  161    ),
  162    flush_output.
  163ansi_format(Stream, _Attr, Format, Args) :-
  164    format(Stream, Format, Args).
  165
  166sgr_codes_ex(X) -->
  167    { var(X),
  168      !,
  169      instantiation_error(X)
  170    }.
  171sgr_codes_ex([]) -->
  172    !.
  173sgr_codes_ex([H|T]) -->
  174    !,
  175    sgr_codes_ex(H),
  176    sgr_codes_ex(T).
  177sgr_codes_ex(Attr) -->
  178    (   { sgr_code(Attr, Code) }
  179    ->  (   { is_list(Code) }
  180        ->  list(Code)
  181        ;   [Code]
  182        )
  183    ;   { domain_error(sgr_code, Attr) }
  184    ).
  185
  186list([]) --> [].
  187list([H|T]) --> [H], list(T).
  188
  189
  228
  229sgr_code(reset, 0).
  230sgr_code(bold,  1).
  231sgr_code(faint, 2).
  232sgr_code(italic, 3).
  233sgr_code(underline, 4).
  234sgr_code(blink(slow), 5).
  235sgr_code(blink(rapid), 6).
  236sgr_code(negative, 7).
  237sgr_code(conceal, 8).
  238sgr_code(crossed_out, 9).
  239sgr_code(font(primary), 10) :- !.
  240sgr_code(font(N), C) :-
  241    C is 10+N.
  242sgr_code(fraktur, 20).
  243sgr_code(underline(double), 21).
  244sgr_code(intensity(normal), 22).
  245sgr_code(fg(Name), C) :-
  246    (   ansi_color(Name, N)
  247    ->  C is N+30
  248    ;   rgb(Name, R, G, B)
  249    ->  sgr_code(fg(R,G,B), C)
  250    ).
  251sgr_code(bg(Name), C) :-
  252    !,
  253    (   ansi_color(Name, N)
  254    ->  C is N+40
  255    ;   rgb(Name, R, G, B)
  256    ->  sgr_code(bg(R,G,B), C)
  257    ).
  258sgr_code(framed, 51).
  259sgr_code(encircled, 52).
  260sgr_code(overlined, 53).
  261sgr_code(ideogram(underline), 60).
  262sgr_code(right_side_line, 60).
  263sgr_code(ideogram(underline(double)), 61).
  264sgr_code(right_side_line(double), 61).
  265sgr_code(ideogram(overlined), 62).
  266sgr_code(left_side_line, 62).
  267sgr_code(ideogram(stress_marking), 64).
  268sgr_code(-X, Code) :-
  269    off_code(X, Code).
  270sgr_code(hfg(Name), C) :-
  271    ansi_color(Name, N),
  272    C is N+90.
  273sgr_code(hbg(Name), C) :-
  274    !,
  275    ansi_color(Name, N),
  276    C is N+100.
  277sgr_code(fg8(Name), [38,5,N]) :-
  278    ansi_color8(Name, N).
  279sgr_code(bg8(Name), [48,5,N]) :-
  280    ansi_color8(Name, N).
  281sgr_code(fg(R,G,B), [38,2,R,G,B]) :-
  282    between(0, 255, R),
  283    between(0, 255, G),
  284    between(0, 255, B).
  285sgr_code(bg(R,G,B), [48,2,R,G,B]) :-
  286    between(0, 255, R),
  287    between(0, 255, G),
  288    between(0, 255, B).
  289
  290off_code(italic_and_franktur, 23).
  291off_code(underline, 24).
  292off_code(blink, 25).
  293off_code(negative, 27).
  294off_code(conceal, 28).
  295off_code(crossed_out, 29).
  296off_code(framed, 54).
  297off_code(overlined, 55).
  298
  299ansi_color8(h(Name), N) :-
  300    !,
  301    ansi_color(Name, N0),
  302    N is N0+8.
  303ansi_color8(Name, N) :-
  304    atom(Name),
  305    !,
  306    ansi_color(Name, N).
  307ansi_color8(N, N) :-
  308    between(0, 255, N).
  309
  310ansi_color(black,   0).
  311ansi_color(red,     1).
  312ansi_color(green,   2).
  313ansi_color(yellow,  3).
  314ansi_color(blue,    4).
  315ansi_color(magenta, 5).
  316ansi_color(cyan,    6).
  317ansi_color(white,   7).
  318ansi_color(default, 9).
  319
  320rgb(Name, R, G, B) :-
  321    atom_codes(Name, [0'#,R1,R2,G1,G2,B1,B2]),
  322    hex_color(R1,R2,R),
  323    hex_color(G1,G2,G),
  324    hex_color(B1,B2,B).
  325rgb(Name, R, G, B) :-
  326    atom_codes(Name, [0'#,R1,G1,B1]),
  327    hex_color(R1,R),
  328    hex_color(G1,G),
  329    hex_color(B1,B).
  330
  331hex_color(D1,D2,V) :-
  332    code_type(D1, xdigit(V1)),
  333    code_type(D2, xdigit(V2)),
  334    V is 16*V1+V2.
  335
  336hex_color(D1,V) :-
  337    code_type(D1, xdigit(V1)),
  338    V is 16*V1+V1.
  339
  349
  350
  351                   354
  359
  360prolog:message_line_element(S, ansi(Class, Fmt, Args)) :-
  361    class_attrs(Class, Attr),
  362    ansi_format(S, Attr, Fmt, Args).
  363prolog:message_line_element(S, ansi(Class, Fmt, Args, Ctx)) :-
  364    class_attrs(Class, Attr),
  365    ansi_format(S, Attr, Fmt, Args),
  366    (   nonvar(Ctx),
  367        Ctx = ansi(_, RI-RA)
  368    ->  keep_line_pos(S, format(S, RI, RA))
  369    ;   true
  370    ).
  371prolog:message_line_element(S, url(Location)) :-
  372    ansi_hyperlink(S, Location).
  373prolog:message_line_element(S, url(URL, Label)) :-
  374    ansi_hyperlink(S, URL, Label).
  375prolog:message_line_element(S, begin(Level, Ctx)) :-
  376    level_attrs(Level, Attr),
  377    stream_property(S, tty(true)),
  378    current_prolog_flag(color_term, true),
  379    !,
  380    (   is_list(Attr)
  381    ->  sgr_codes(Attr, Codes),
  382        atomic_list_concat(Codes, ;, Code)
  383    ;   sgr_code(Attr, Code)
  384    ),
  385    keep_line_pos(S, format(S, '\e[~wm', [Code])),
  386    Ctx = ansi('\e[0m', '\e[0m\e[~wm'-[Code]).
  387prolog:message_line_element(S, end(Ctx)) :-
  388    nonvar(Ctx),
  389    Ctx = ansi(Reset, _),
  390    keep_line_pos(S, write(S, Reset)).
  391
  392sgr_codes([], []).
  393sgr_codes([H0|T0], [H|T]) :-
  394    sgr_code(H0, H),
  395    sgr_codes(T0, T).
  396
  397level_attrs(Level,         Attrs) :-
  398    user:message_property(Level, color(Attrs)),
  399    !.
  400level_attrs(Level,         Attrs) :-
  401    class_attrs(message(Level), Attrs).
  402
  403class_attrs(Class, Attrs) :-
  404    user:message_property(Class, color(Attrs)),
  405    !.
  406class_attrs(Class, Attrs) :-
  407    prolog:console_color(Class, Attrs),
  408    !.
  409class_attrs(Class, Attrs) :-
  410    '$messages':default_theme(Class, Attrs),
  411    !.
  412class_attrs(Attrs, Attrs).
  413
  425
  426ansi_hyperlink(Stream, Location) :-
  427    hyperlink(Stream, url(Location)),
  428    !.
  429ansi_hyperlink(Stream, Location) :-
  430    location_label(Location, Label),
  431    ansi_hyperlink(Stream, Location, Label).
  432
  433location_label(File:Line:Column, Label) =>
  434    format(string(Label), '~w:~w:~w', [File,Line,Column]).
  435location_label(File:Line, Label) =>
  436    format(string(Label), '~w:~w', [File,Line]).
  437location_label(File, Label) =>
  438    format(string(Label), '~w', [File]).
  439
  440ansi_hyperlink(Stream, Location, Label),
  441    hyperlink(Stream, url(Location, Label)) =>
  442    true.
  443ansi_hyperlink(Stream, Location, Label) =>
  444    (   location_url(Location, URL)
  445    ->  keep_line_pos(Stream,
  446                      format(Stream, '\e]8;;~w\e\\', [URL])),
  447        format(Stream, '~w', [Label]),
  448        keep_line_pos(Stream,
  449                      format(Stream, '\e]8;;\e\\', []))
  450    ;   format(Stream, '~w', [Label])
  451    ).
  452
  453is_url(URL) :-
  454    (   atom(URL)
  455    ->  true
  456    ;   string(URL)
  457    ),
  458    url_prefix(Prefix),
  459    sub_string(URL, 0, _, _, Prefix).
  460
  461url_prefix('http://').
  462url_prefix('https://').
  463url_prefix('file://').
  464
  470
  471location_url(Location, URL),
  472    tty_url_hook(Location, URL0) =>
  473    URL = URL0.
  474location_url(File:Line:Column, URL) =>
  475    url_file_name(FileURL, File),
  476    format(string(URL), '~w#~d:~d', [FileURL, Line, Column]).
  477location_url(File:Line, URL) =>
  478    url_file_name(FileURL, File),
  479    format(string(URL), '~w#~w', [FileURL, Line]).
  480location_url(File, URL) =>
  481    url_file_name(URL, File).
  482
  486
  487
  492
  493url_file_name(URL, File) :-
  494    is_url(File), !,
  495    current_prolog_flag(hyperlink_term, true),
  496    URL = File.
  497url_file_name(URL, File) :-
  498    current_prolog_flag(hyperlink_term, true),
  499    absolute_file_name(File, AbsFile),
  500    ensure_leading_slash(AbsFile, AbsFile1),
  501    url_encode_path(AbsFile1, Encoded),
  502    format(string(URL), 'file://~s', [Encoded]).
  503
  504ensure_leading_slash(Path, SlashPath) :-
  505    (   sub_atom(Path, 0, _, _, /)
  506    ->  SlashPath = Path
  507    ;   atom_concat(/, Path, SlashPath)
  508    ).
  509
  510url_encode_path(Name, Encoded) :-
  511    atom_codes(Name, Codes),
  512    phrase(utf8_codes(Codes), UTF8),
  513    phrase(encode(UTF8), Encoded).
  514
  515encode([]) --> [].
  516encode([H|T]) --> encode1(H), encode(T).
  517
  518encode1(C) -->
  519    { reserved(C),
  520      !,
  521      format(codes([C1,C2]), '~`0t~16r~2|', [C])
  522    },
  523    "%", [C1,C2].
  524encode1(C) -->
  525    [C].
  526
  527reserved(C) :- C =< 0'\s.
  528reserved(C) :- C >= 127.
  529reserved(0'#).
  530
  536
  537keep_line_pos(S, G) :-
  538    stream_property(S, position(Pos)),
  539    !,
  540    setup_call_cleanup(
  541        stream_position_data(line_position, Pos, LPos),
  542        G,
  543        set_stream(S, line_position(LPos))).
  544keep_line_pos(_, G) :-
  545    call(G).
  546
  557
  558ansi_get_color(Which0, RGB) :-
  559    \+ current_prolog_flag(console_menu, true),
  560    stream_property(user_input, tty(true)),
  561    stream_property(user_output, tty(true)),
  562    stream_property(user_error, tty(true)),
  563    supports_get_color,
  564    (   color_alias(Which0, Which)
  565    ->  true
  566    ;   must_be(between(0,15),Which0)
  567    ->  Which = Which0
  568    ),
  569    catch(keep_line_pos(user_output,
  570                        ansi_get_color_(Which, RGB)),
  571          error(timeout_error(_,_), _),
  572          no_xterm).
  573
  574supports_get_color :-
  575    getenv('TERM', Term),
  576    sub_atom(Term, 0, _, _, xterm),
  577    \+ getenv('TERM_PROGRAM', 'Apple_Terminal').
  578
  579color_alias(foreground, 10).
  580color_alias(background, 11).
  581
  582ansi_get_color_(Which, rgb(R,G,B)) :-
  583    format(codes(Id), '~w', [Which]),
  584    hex4(RH),
  585    hex4(GH),
  586    hex4(BH),
  587    phrase(("\e]", Id, ";rgb:", RH, "/", GH, "/", BH, "\a"), Pattern),
  588    stream_property(user_input, timeout(Old)),
  589    setup_call_cleanup(
  590        set_stream(user_input, timeout(0.05)),
  591        with_tty_raw(exchange_pattern(Which, Pattern)),
  592        set_stream(user_input, timeout(Old))),
  593    !,
  594    hex_val(RH, R),
  595    hex_val(GH, G),
  596    hex_val(BH, B).
  597
  598no_xterm :-
  599    print_message(warning, ansi(no_xterm_get_colour)),
  600    fail.
  601
  602hex4([_,_,_,_]).
  603
  604hex_val([D1,D2,D3,D4], V) :-
  605    code_type(D1, xdigit(V1)),
  606    code_type(D2, xdigit(V2)),
  607    code_type(D3, xdigit(V3)),
  608    code_type(D4, xdigit(V4)),
  609    V is (V1<<12)+(V2<<8)+(V3<<4)+V4.
  610
  611exchange_pattern(Which, Pattern) :-
  612    format(user_output, '\e]~w;?\a', [Which]),
  613    flush_output(user_output),
  614    read_pattern(user_input, Pattern, []).
  615
  616read_pattern(From, Pattern, NotMatched0) :-
  617    copy_term(Pattern, TryPattern),
  618    append(Skip, Rest, NotMatched0),
  619    append(Rest, RestPattern, TryPattern),
  620    !,
  621    echo(Skip),
  622    try_read_pattern(From, RestPattern, NotMatched, Done),
  623    (   Done == true
  624    ->  Pattern = TryPattern
  625    ;   read_pattern(From, Pattern, NotMatched)
  626    ).
  627
  629
  630try_read_pattern(_, [], [], true) :-
  631    !.
  632try_read_pattern(From, [H|T], [C|RT], Done) :-
  633    get_code(C),
  634    (   C = H
  635    ->  try_read_pattern(From, T, RT, Done)
  636    ;   RT = [],
  637        Done = false
  638    ).
  639
  640echo([]).
  641echo([H|T]) :-
  642    put_code(user_output, H),
  643    echo(T).
  644
  645:- multifile prolog:message//1.  646
  647prolog:message(ansi(no_xterm_get_colour)) -->
  648    [ 'Terminal claims to be xterm compatible,'-[], nl,
  649      'but does not report colour info'-[]
  650    ]