(* 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