hop-2012/server/thirdparty/lwt-2.3.2/src/text/lwt_term.ml

848 lines
21 KiB
OCaml

(* Lightweight thread library for Objective Caml
* http://www.ocsigen.org/lwt
* Module Lwt_term
* Copyright (C) 2009 Jérémie Dimino
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, with linking exceptions;
* either version 2.1 of the License, or (at your option) any later
* version. See COPYING file for details.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
* 02111-1307, USA.
*)
open Lwt
open Lwt_text
(* +-----------------------------------------------------------------+
| Terminal mode |
+-----------------------------------------------------------------+ *)
type state =
| Normal
| Raw of Unix.terminal_io
let state = ref Normal
(* Number of function currently using the raw mode: *)
let raw_count = ref 0
let get_attr () =
try_lwt
lwt attr = Lwt_unix.tcgetattr Lwt_unix.stdin in
return (Some attr)
with _ ->
return None
let set_attr mode =
try_lwt
Lwt_unix.tcsetattr Lwt_unix.stdin Unix.TCSAFLUSH mode
with _ ->
return ()
let drawing_mode = ref false
let enter_drawing_mode () =
drawing_mode := true;
write stdout "\027[?1049h\027[?1h\027=\r"
let leave_drawing_mode () =
drawing_mode := false;
write stdout "\r\027[K\027[?1l\027>\027[r\027[?1049l"
let cursor_visible = ref true
let show_cursor _ =
cursor_visible := true;
write stdout "\x1B[?25h"
let hide_cursor _ =
cursor_visible := false;
write stdout "\x1B[?25l"
let clear_screen _ =
write stdout "\027[2J\027[H"
let clear_line _ =
write stdout "\027[2K"
(* Go-up by [n] lines then to the beginning of the line. Normally
"\027[nF" does exactly this but for some terminal 1 need to be
added... By the way we can relly on the fact that all terminal
react the same way to "\027[F" which is to go to the beginning of
the previous line: *)
let rec goto_beginning_of_line = function
| 0 ->
write_char stdout "\r"
| 1 ->
write stdout "\027[F"
| n ->
lwt () = write stdout "\027[F" in
goto_beginning_of_line (n - 1)
(* Restore terminal mode on exit: *)
let cleanup () =
lwt () =
if not !cursor_visible then
show_cursor ()
else
return ()
in
lwt () =
if !drawing_mode then
leave_drawing_mode ()
else
return ()
in
match !state with
| Normal ->
return ()
| Raw saved_attr ->
set_attr saved_attr
let () = Lwt_main.at_exit cleanup
let raw_mode () = match !state with
| Normal -> false
| Raw _ -> true
let leave_raw_mode () =
decr raw_count;
if !raw_count = 0 then
match !state with
| Normal ->
assert false
| Raw attr ->
state := Normal;
set_attr attr
else
return ()
let with_raw_mode f =
match !state with
| Raw attr ->
incr raw_count;
finalize f leave_raw_mode
| Normal ->
get_attr () >>= function
| Some attr ->
incr raw_count;
state := Raw attr;
lwt () = set_attr {
attr with
(* Inspired from Python-3.0/Lib/tty.py: *)
Unix.c_brkint = false;
Unix.c_inpck = false;
Unix.c_istrip = false;
Unix.c_ixon = false;
Unix.c_csize = 8;
Unix.c_parenb = false;
Unix.c_echo = false;
Unix.c_icanon = false;
Unix.c_isig = false;
Unix.c_vmin = 1;
Unix.c_vtime = 0
} in
try_lwt f () finally leave_raw_mode ()
| None ->
raise_lwt (Failure "Lwt_term.with_raw_mode: input is not a tty")
(* +-----------------------------------------------------------------+
| Terminal informations |
+-----------------------------------------------------------------+ *)
type size = {
lines : int;
columns : int;
}
external get_size : Unix.file_descr -> size = "lwt_text_term_size"
#if windows
let size =
React.S.const
(try
get_size Unix.stdout
with Unix.Unix_error _ ->
{ columns = 80; lines = 25 })
#else
external sigwinch : unit -> int = "lwt_text_sigwinch"
let sigwinch = sigwinch ()
let sigwinch_event =
if sigwinch = 0 then
React.E.never
else
try
let event, push = React.E.create () in
let _ = Lwt_unix.on_signal sigwinch push in
event
with Unix.Unix_error _ | Invalid_argument _ | Sys_error _ ->
React.E.never
let size =
React.S.hold
(try
get_size Unix.stdin
with Unix.Unix_error _ ->
{ columns = 80; lines = 25 })
(React.E.map (fun _ -> get_size Unix.stdin) sigwinch_event)
#endif
let columns = React.S.map (fun { columns = c } -> c) size
let lines = React.S.map (fun { lines = l } -> l) size
(* +-----------------------------------------------------------------+
| Keys input |
+-----------------------------------------------------------------+ *)
exception Exit_sequence
let parse_escape st =
let buf = Buffer.create 10 in
Buffer.add_char buf '\027';
(* Read one character and add it to [buf]: *)
let get () =
match Lwt.state (Lwt_stream.get st) with
| Sleep ->
(* If the rest is not immediatly available, conclude that
this is not an escape sequence but just the escape key: *)
raise_lwt Exit_sequence
| Fail exn ->
raise_lwt exn
| Return None ->
raise_lwt Exit_sequence
| Return(Some ch) ->
(* Is it an ascii character ? *)
if String.length ch = 1 then begin
Buffer.add_string buf ch;
return ch.[0]
end else
(* If it is not, then this is not an escape sequence: *)
raise_lwt Exit_sequence
in
(* Sometimes sequences starts with several escape characters: *)
let rec first count =
get () >>= function
| '\027' when count < 3 ->
first (count + 1)
| ch ->
return ch
in
first 0 >>= function
| '[' | 'O' ->
let rec loop () =
get () >>= function
| '0' .. '9' | ';' ->
loop ()
| ch ->
return (Buffer.contents buf)
in
loop ()
| ch ->
return (Buffer.contents buf)
let parse_key_raw st =
Lwt_stream.next st >>= function
| "\027" ->
begin
try_lwt
Lwt_stream.parse st parse_escape
with
Exit_sequence -> return "\027"
end
| ch ->
return ch
type key =
| Key of string
| Key_up
| Key_down
| Key_left
| Key_right
| Key_f of int
| Key_next_page
| Key_previous_page
| Key_home
| Key_end
| Key_insert
| Key_delete
| Key_control of char
let key_enter = Key_control 'j'
let key_escape = Key_control '['
let key_tab = Key_control 'i'
let key_backspace = Key_control '?'
let string_of_key = function
| Key ch ->
Printf.sprintf "Key %S" ch
| Key_f n ->
Printf.sprintf "Key_f %d" n
| Key_control c ->
Printf.sprintf "Key_control %C" c
| Key_up ->
"Key_up"
| Key_down ->
"Key_down"
| Key_left ->
"Key_left"
| Key_right ->
"Key_right"
| Key_next_page ->
"Key_next_page"
| Key_previous_page ->
"Key_previous_page"
| Key_home ->
"Key_home"
| Key_end ->
"Key_end"
| Key_insert ->
"Key_insert"
| Key_delete ->
"Key_delete"
let sequence_mapping = [
"\027[A", Key_up;
"\027[B", Key_down;
"\027[C", Key_right;
"\027[D", Key_left;
"\027A", Key_up;
"\027B", Key_down;
"\027C", Key_right;
"\027D", Key_left;
"\027OA", Key_up;
"\027OB", Key_down;
"\027OC", Key_right;
"\027OD", Key_left;
"\027[2~", Key_insert;
"\027[3~", Key_delete;
"\027[5~", Key_previous_page;
"\027[6~", Key_next_page;
"\027[7~", Key_home;
"\027[8~", Key_end;
"\027[11~", Key_f 1;
"\027[12~", Key_f 2;
"\027[13~", Key_f 3;
"\027[14~", Key_f 4;
"\027[15~", Key_f 5;
"\027[17~", Key_f 6;
"\027[18~", Key_f 7;
"\027[19~", Key_f 8;
"\027[20~", Key_f 9;
"\027[21~", Key_f 10;
"\027[23~", Key_f 11;
"\027[24~", Key_f 12;
"\027OP", Key_f 1;
"\027OQ", Key_f 2;
"\027OR", Key_f 3;
"\027OS", Key_f 4;
"\027[H", Key_home;
"\027[F", Key_end;
"\027OH", Key_home;
"\027OF", Key_end;
"\027H", Key_home;
"\027F", Key_end;
]
let control_mapping = [
0x00, '@';
0x01, 'a';
0x02, 'b';
0x03, 'c';
0x04, 'd';
0x05, 'e';
0x06, 'f';
0x07, 'g';
0x08, 'h';
0x09, 'i';
0x0A, 'j';
0x0B, 'k';
0x0C, 'l';
0x0D, 'm';
0x0E, 'n';
0x0F, 'o';
0x10, 'p';
0x11, 'q';
0x12, 'r';
0x13, 's';
0x14, 't';
0x15, 'u';
0x16, 'v';
0x17, 'w';
0x18, 'x';
0x19, 'y';
0x1A, 'z';
0x1B, '[';
0x1C, '\\';
0x1D, ']';
0x1E, '^';
0x1F, '_';
0x7F, '?';
]
let decode_key ch =
if ch = "" then invalid_arg "Lwt_term.decode_key";
match ch with
| ch when String.length ch = 1 ->
begin try
Key_control(List.assoc (Char.code ch.[0]) control_mapping)
with
Not_found -> Key ch
end
| ch ->
begin try
List.assoc ch sequence_mapping
with
Not_found -> Key ch
end
let standard_input = Lwt_text.read_chars Lwt_text.stdin
let read_key () =
with_raw_mode (fun _ -> parse_key_raw standard_input >|= decode_key)
(* +-----------------------------------------------------------------+
| Styles |
+-----------------------------------------------------------------+ *)
type color = int
let default = -1
let black = 0
let red = 1
let green = 2
let yellow = 3
let blue = 4
let magenta = 5
let cyan = 6
let white = 7
let lblack = black + 8
let lred = red + 8
let lgreen = green + 8
let lyellow = yellow + 8
let lblue = blue + 8
let lmagenta = magenta + 8
let lcyan = cyan + 8
let lwhite = white + 8
type style = {
bold : bool;
underlined : bool;
blink : bool;
inverse : bool;
hidden : bool;
foreground : color;
background : color;
}
module Codes = struct
let reset = 0
let bold = 1
let underlined = 4
let blink = 5
let inverse = 7
let hidden = 8
let foreground col = 30 + col
let background col = 40 + col
end
let set_color num (r, g, b) =
write stdout (Printf.sprintf "\027]4;%d;rgb:%02x/%02x/%02x;\027\\" num r g b)
(* +-----------------------------------------------------------------+
| Rendering |
+-----------------------------------------------------------------+ *)
type point = {
char : string;
style : style;
}
let blank = {
char = " ";
style = {
bold = false;
underlined = false;
blink = false;
inverse = false;
hidden = false;
foreground = default;
background = default;
};
}
let rec add_int buf = function
| 0 ->
()
| n ->
add_int buf (n / 10);
Buffer.add_char buf (Char.unsafe_chr (48 + (n mod 10)))
let render_char buf oc pt last_style =
lwt () =
if pt.style <> last_style then begin
Buffer.clear buf;
Buffer.add_string buf "\027[0";
let mode n = function
| true ->
Buffer.add_char buf ';';
add_int buf n
| false ->
()
and color f col =
if col = default then
()
else if col < 8 then begin
Buffer.add_char buf ';';
add_int buf (f col)
end else begin
Buffer.add_char buf ';';
add_int buf (f 8);
Buffer.add_string buf ";5;";
add_int buf col;
end
in
mode Codes.bold pt.style.bold;
mode Codes.underlined pt.style.underlined;
mode Codes.blink pt.style.blink;
mode Codes.inverse pt.style.inverse;
mode Codes.hidden pt.style.hidden;
color Codes.foreground pt.style.foreground;
color Codes.background pt.style.background;
Buffer.add_char buf 'm';
write oc (Buffer.contents buf)
end else
return ()
in
write_char oc pt.char
let render_update old m =
let buf = Buffer.create 16 in
Lwt_text.atomic begin fun oc ->
let rec loop_y y last_style =
if y < Array.length m then
let rec loop_x x last_style =
if x < Array.length m.(y) then
let pt = m.(y).(x) in
lwt () = render_char buf oc pt last_style in
loop_x (x + 1) pt.style
else
loop_y (y + 1) last_style
in
if y < Array.length old && old.(y) = m.(y) then begin
if y + 1 < Array.length m then
lwt last_style =
if Array.length m.(y) > 0 then
let pt = m.(y).(0) in
lwt () = render_char buf oc pt last_style in
return pt.style
else
return last_style
in
lwt () = write oc "\r\n" in
loop_y (y + 1) last_style
else
return ()
end else
loop_x 0 last_style
else
return ()
in
(* Go to the top-left corner and reset attributes: *)
lwt () = write oc "\027[H\027[0m" in
lwt () = loop_y 0 blank.style in
write oc "\027[0m"
end stdout
let render m = render_update [||] m
(* +-----------------------------------------------------------------+
| Styled text |
+-----------------------------------------------------------------+ *)
open Printf
type styled_text_instruction =
| Text of Text.t
| Reset
| Bold
| Underlined
| Blink
| Inverse
| Hidden
| Foreground of color
| Background of color
type styled_text = styled_text_instruction list
let textf fmt = Printf.ksprintf (fun txt -> Text txt) fmt
let text txt = Text txt
let reset = Reset
let bold = Bold
let underlined = Underlined
let blink = Blink
let inverse = Inverse
let hidden = Hidden
let fg col = Foreground col
let bg col = Background col
let strip_styles st =
let buf = Buffer.create 42 in
List.iter (function
| Text t -> Buffer.add_string buf t
| _ -> ()) st;
Buffer.contents buf
let write_styled oc st =
let buf = Buffer.create 16
(* Pendings style codes: *)
and codes = Queue.create () in
(* Output pending codes using only one escape sequence: *)
let output_pendings () =
Buffer.clear buf;
Buffer.add_string buf "\027[";
add_int buf (Queue.take codes);
Queue.iter (fun code ->
Buffer.add_char buf ';';
add_int buf code) codes;
Queue.clear codes;
Buffer.add_char buf 'm';
write oc (Buffer.contents buf)
in
let rec loop = function
| [] ->
if not (Queue.is_empty codes) then
output_pendings ()
else
return ()
| instr :: rest ->
match instr with
| Text t ->
if not (Queue.is_empty codes) then
lwt () = output_pendings () in
lwt () = write oc t in
loop rest
else
lwt () = write oc t in
loop rest
| Reset ->
Queue.add 0 codes;
loop rest
| Bold ->
Queue.add Codes.bold codes;
loop rest
| Underlined ->
Queue.add Codes.underlined codes;
loop rest
| Blink ->
Queue.add Codes.blink codes;
loop rest
| Inverse ->
Queue.add Codes.inverse codes;
loop rest
| Hidden ->
Queue.add Codes.hidden codes;
loop rest
| Foreground col ->
if col = default then
Queue.add (Codes.foreground 9) codes
else if col < 8 then
Queue.add (Codes.foreground col) codes
else begin
Queue.add (Codes.foreground 8) codes;
Queue.add 5 codes;
Queue.add col codes
end;
loop rest
| Background col ->
if col = default then
Queue.add (Codes.background 9) codes
else if col < 8 then
Queue.add (Codes.background col) codes
else begin
Queue.add (Codes.background 8) codes;
Queue.add 5 codes;
Queue.add col codes
end;
loop rest
in
loop st
let styled_length st =
let rec loop len = function
| [] -> len
| Text t :: l -> loop (len + Text.length t) l
| _ :: l -> loop len l
in
loop 0 st
let printc st =
Lwt_unix.isatty Lwt_unix.stdout >>= function
| true ->
atomic (fun oc -> write_styled oc st) stdout
| false ->
write stdout (strip_styles st)
let eprintc st =
Lwt_unix.isatty Lwt_unix.stderr >>= function
| true ->
atomic (fun oc -> write_styled oc st) stderr
| false ->
write stderr (strip_styles st)
let fprintlc oc fd st =
Lwt_unix.isatty fd >>= function
| true ->
atomic (fun oc ->
lwt () = write_styled oc st in
lwt () = write oc "\027[m" in
write_char oc "\n") oc
| false ->
write_line oc (strip_styles st)
let printlc st = fprintlc stdout Lwt_unix.stdout st
let eprintlc st = fprintlc stderr Lwt_unix.stderr st
(* +-----------------------------------------------------------------+
| Drawing |
+-----------------------------------------------------------------+ *)
module Zone =
struct
type t = {
points : point array array;
x : int;
y : int;
width : int;
height : int;
}
let points zone = zone.points
let x zone = zone.x
let y zone = zone.y
let width zone = zone.width
let height zone = zone.height
let make ~width ~height =
if width < 0 || height < 0 then invalid_arg "Lwt_term.Zone.make";
{
points = Array.make_matrix height width blank;
x = 0;
y = 0;
width = width;
height = height;
}
let sub ~zone ~x ~y ~width ~height =
if (x < 0 || y < 0 ||
width < 0 || height < 0 ||
x + width > zone.width ||
y + height > zone.height) then
invalid_arg "Lwt_term.Zone.sub";
{
points = zone.points;
x = zone.x + x;
y = zone.y + y;
width = width;
height = height;
}
let inner zone = {
points = zone.points;
x = if zone.width >= 2 then zone.x + 1 else zone.x;
y = if zone.height >= 2 then zone.y + 1 else zone.y;
width = if zone.width >= 2 then zone.width - 2 else zone.width;
height = if zone.height >= 2 then zone.height - 2 else zone.height;
}
end
module Draw =
struct
open Zone
let get ~zone ~x ~y =
if x < 0 || y < 0 || x >= zone.width || y >= zone.height then
invalid_arg "Lwt_term.Draw.get";
zone.points.(zone.y + y).(zone.x + x)
let set ~zone ~x ~y ~point =
if x < 0 || y < 0 || x >= zone.width || y >= zone.height then
()
else
zone.points.(zone.y + y).(zone.x + x) <- point
let map ~zone ~x ~y f =
if x < 0 || y < 0 || x >= zone.width || y >= zone.height then
()
else
let x = zone.x + x and y = zone.y + y in
zone.points.(y).(x) <- f zone.points.(y).(x)
let text ~zone ~x ~y ~text =
let rec loop x ptr = match Text.next ptr with
| Some(ch, ptr) ->
set zone x y { blank with char = ch };
loop (x + 1) ptr
| None ->
()
in
loop x (Text.pointer_l text)
let textf zone x y fmt =
Printf.ksprintf (fun txt -> text zone x y txt) fmt
let textc ~zone ~x ~y ~text =
let rec loop style x = function
| [] ->
()
| instr :: rest ->
match instr with
| Text text ->
loop_text style x (Text.pointer_l text) rest
| Reset ->
loop blank.style x rest
| Bold ->
loop { style with bold = true } x rest
| Underlined ->
loop { style with underlined = true } x rest
| Blink ->
loop { style with blink = true } x rest
| Inverse ->
loop { style with inverse = true } x rest
| Hidden ->
loop { style with hidden = true } x rest
| Foreground color ->
loop { style with foreground = color } x rest
| Background color ->
loop { style with background = color } x rest
and loop_text style x ptr rest =
match Text.next ptr with
| Some(ch, ptr) ->
set zone x y { char = ch; style = style };
loop_text style (x + 1) ptr rest
| None ->
loop style x rest
in
loop blank.style x text
end