Better than curses/ncurses, here is Notty. Written in OCaml, this library is based on the notion of composable images.
An image is a rectangle displayed in the terminal that contains styled characters. An image can be:
- a single character with display attributes,
- or some text with display attributes,
- or a combinaison of both beside, above or over each other.
After its construction, the image can be rendered. Basically, it is tranformed to a string we can display.
Print a red "Wow!" above its right-shifted copy:
open Notty
open Notty_unix
(*
* ocamlbuild -pkg notty -pkg notty.unix basics_wow.native
*)
let () =
let wow = I.string A.(fg lightred) "Wow!" in
I.(wow <-> (void 2 0 <|> wow))
|> Notty_unix.eol
|> Notty_unix.output_image
In this program we create an image that is based on a string "Wow!" with some
attributes A.(fg lightred)
. Then we compose a bigger image and display twice
the wow
image at different position. The function Notty_unix.output_image_endline
allow us to display the generated image.
https://pqwy.github.io/notty/Notty.I.html
Basics images can be created with :
I.string
: require an attribute (style) and a stringI.uchars
: require an attribute and an array of unicode valueI.char
: require an attribute, a char and 2 int for the width and the height of the grid.I.uchar
: same asI.char
but for unicode value.
or 2 specials primitives:
I.empty
: which is a zero sized image.I.void
: require a width and an height, it is a transparent image.
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basics_I_string.native *)
let () =
I.string A.(fg lightred) "Wow!"
|> eol
|> Notty_unix.output_image
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basics_I_uchars.native *)
let () =
let my_unicode_chars =
[|0x2500; 0x2502; 0x2022; 0x2713; 0x25cf;
0x256d; 0x256e; 0x256f; 0x2570; 0x253c|] in
I.uchars A.(fg lightred) (Array.map Uchar.of_int my_unicode_chars)
|> Notty_unix.eol
|> Notty_unix.output_image
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basics_I_char.native *)
let () =
I.char A.(fg lightred) 'o' 4 4
|> Notty_unix.eol
|> Notty_unix.output_image
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basics_I_uchar.native *)
let () =
I.uchar A.(fg lightred) (Uchar.of_int 0x2022) 4 4
|> Notty_unix.eol
|> Notty_unix.output_image
There are 3 composition modes which allow you to blend simple images into complexe ones.
I.(<|>)
: puts one image after anotherI.(<->)
: puts one image below anotherI.(</>)
: puts one image on another.
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basic_I_side_by_side *)
let () =
let bar = I.uchar A.(fg lightred) 0x2502 3 1 in
let img1 = I.string A.(fg lightgreen) "image1" in
I.(img1 <|> bar) |> Notty_unix.output_image_endline
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basics_I_above_another.native *)
let () =
let bar = I.uchar A.(fg lightred) (Uchar.of_int 0x2502) 3 1 in
let img1 = I.string A.(fg lightgreen) "image1" in
I.(img1 <-> bar) |> Notty_unix.eol |> Notty_unix.output_image
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basic_I_overlay.native *)
let () =
let bar = I.uchar A.(fg lightred) 0x2502 3 1 in
let img1 = I.string A.(fg lightgreen) "image1" in
I.(img1 </> bar) |> Notty_unix.output_image_endline
Basic notty images can be modified by cropping them or by adding them padding.
I.hcrop
I.vcrop
I.crop
open Notty
open Notty_unix
(* ocamlfind ocamlc -o basics_hcropping -package notty,notty.unix -linkpkg -g basics_I_hcropping.ml *)
let long_line_str = "This is a line that will be cropped 2 unit left and 5 unit right"
let () =
let long_line = I.string A.(fg lightgreen ++ bg black) long_line_str in
let long_line_cropped = I.hcrop 2 5 long_line in
I.(long_line <-> long_line_cropped) |> Notty_unix.output_image_endline
open Notty
open Notty_unix
(* ocamlfind ocamlc -o basics_vcropping -package notty,notty.unix -linkpkg -g basics_I_vcropping.ml *)
let line_str num =
"line number " ^ (string_of_int num)
let build_5_lines () =
let rec _build img remain =
if remain = 0 then img
else let str = line_str (6 - remain) in
_build I.(img <-> string A.(fg lightgreen ++ bg black) str) (remain - 1)
in _build (I.string A.(fg lightgreen ++ bg black) (line_str 1)) 4
let description =
I.string A.(fg lightyellow ++ bg lightblack) "crop 2 at top and 1 at bottom"
let () =
I.(build_5_lines () <->
description <->
vcrop 2 1 (build_5_lines ())) |> Notty_unix.output_image_endline
open Notty
open Notty_unix
(* ocamlfind ocamlc -o basics_negative_vcropping -package notty,notty.unix -linkpkg -g basics_I_negative_vcropping.ml *)
let line_str num =
"line number " ^ (string_of_int num)
let build_5_lines () =
let rec _build img remain =
if remain = 0 then img
else let str = line_str (6 - remain) in
_build I.(img <-> string A.(fg lightgreen ++ bg black) str) (remain - 1)
in _build (I.string A.(fg lightgreen ++ bg black) (line_str 1)) 4
let description =
I.string A.(fg lightyellow ++ bg lightblack) "Negative crop -2 at top and -1 at bottom"
let () =
I.(build_5_lines () <->
description <->
vcrop (-2) (-1) (build_5_lines ())) |> Notty_unix.output_image_endline
The same applies to I.hcrop
.
I.hpad
I.vpad
I.pad
open Notty
open Notty_unix
(* ocamlfind ocamlc -o basics_padding -package notty,notty.unix -linkpkg -g basics_I_padding.ml *)
let line_str num =
"line number " ^ (string_of_int num)
let build_5_lines () =
let rec _build img remain =
if remain = 0 then img
else let str = line_str (6 - remain) in
_build I.(img <-> string A.(fg lightgreen ++ bg black) str) (remain - 1)
in _build (I.string A.(fg lightgreen ++ bg black) (line_str 1)) 4
let description =
I.string A.(fg lightyellow ++ bg lightblack) "Padding left = 2, right = 3, top = 4 and 1 at bottom"
let () =
I.(build_5_lines () <->
description <->
pad ~l:2 ~r:3 ~t:4 ~b:1 (build_5_lines ())) |> Notty_unix.output_image_endline
It is an helper for fullscreen, interactive applications.
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basics_Term_simple_terminal.native
* or
* ocamlfind ocamlc -o basics_simple_terminal -package notty,notty.unix -linkpkg -g basics_Term_simple_terminal.ml*)
let rec main_loop t =
let img = I.(string A.(bg lightred ++ fg black) "This is a simple example") in
Term.image t img;
match Term.event t with
| `End | `Key (`Escape, []) | `Key (`Uchar 67, [`Ctrl]) -> ()
| _ -> main_loop t
let () =
let t = Term.create () in main_loop t
This little programm just draw in all the terminal, add a string and wait for key events. Press "Esc" and the program exits.
open Notty
open Notty_unix
(* ocamlbuild -pkg notty -pkg notty.unix basics_Term_simple_terminal_resize.native
* or
* ocamlfind ocamlc -o simple_terminal_resize -package notty.unix -linkpkg -g common.ml basics_Term_simple_terminal_resize.ml*)
let grid xxs = xxs |> List.map I.hcat |> I.vcat
let outline attr t =
let (w, h) = Term.size t in
let chr x = I.uchar attr x 1 1
and hbar = I.uchar attr 0x2500 (w - 2) 1
and vbar = I.uchar attr 0x2502 1 (h - 2) in
let (a, b, c, d) = (chr 0x256d, chr 0x256e, chr 0x256f, chr 0x2570) in
grid [ [a; hbar; b]; [vbar; I.void (w - 2) 1; vbar]; [d; hbar; c] ]
let size_box cols rows =
let cols_str = string_of_int cols in let rows_str = string_of_int rows in
let label = (cols_str ^ "x" ^ rows_str) in
let box = I.string A.(fg lightgreen ++ bg lightblack) label in
let top_margin = (rows - I.height box) / 2 in
let left_margin = (cols - I.width box) / 2 in
I.pad ~t:top_margin ~l:left_margin box
let rec main t (x, y as pos) =
let img = I.((outline A.(fg lightred ) t) </> (size_box x y)) in
Term.image t img;
Term.cursor t (Some pos);
match Term.event t with
| `End | `Key (`Escape, []) | `Key (`Uchar 67, [`Ctrl]) -> ()
| `Resize (cols, rows) -> main t (cols, rows)
| _ -> main t pos
let () =
let t = Term.create () in
main t (Term.size t)
- The grid function takes a list of list of images and compose a bigger image.
- The outline function draws a line at the border of the screen.
- The attr argument allows to configure the style of the line.
The same example but inside a light-weight cooperative thread.
open Notty
open Notty_lwt
open Lwt
(* ocamlfind ocamlc -o simple_lwt_terminal_resize -package notty.lwt -linkpkg -g common.ml basics_Lwt_Term_simple_terminal_resize.ml*)
module LwtTerm = Notty_lwt.Term
let grid xxs = xxs |> List.map I.hcat |> I.vcat
let outline attr t =
let (w, h) = LwtTerm.size t in
let chr x = I.uchar attr x 1 1
and hbar = I.uchar attr 0x2500 (w - 2) 1
and vbar = I.uchar attr 0x2502 1 (h - 2) in
let (a, b, c, d) = (chr 0x256d, chr 0x256e, chr 0x256f, chr 0x2570) in
grid [ [a; hbar; b]; [vbar; I.void (w - 2) 1; vbar]; [d; hbar; c] ]
let size_box cols rows =
let cols_str = string_of_int cols in let rows_str = string_of_int rows in
let label = (cols_str ^ "x" ^ rows_str) in
let box = I.string A.(fg lightgreen ++ bg lightblack) label in
let top_margin = (rows - I.height box) / 2 in
let left_margin = (cols - I.width box) / 2 in
I.pad ~t:top_margin ~l:left_margin box
let rec main t (x, y as pos) =
let img = I.((outline A.(fg lightred ) t) </> (size_box x y)) in
LwtTerm.image t img
>>= fun () ->
LwtTerm.cursor t (Some pos)
>>= fun () ->
Lwt_stream.get ( LwtTerm.events t)
>>= fun event ->
match event with
| None -> LwtTerm.release t >>= fun () -> Lwt.return_unit
| Some (`Resize _ | #Unescape.event as x) -> match x with
| `Key (`Escape, []) | `Key (`Uchar 67, [`Ctrl]) -> LwtTerm.release t >>= fun () -> Lwt.return_unit
| `Resize (cols, rows) -> main t (cols, rows)
| _ ->Lwt.return () >>= fun () -> main t pos
let () =
let t = LwtTerm.create () in
let size = LwtTerm.size t in
Lwt_main.run @@ main t size
The following example illustrate how to :
- create of a Notty.term that is updated with a timeout
- use an Notty.term with another thread, one for the interface and one for another loop.
It is a very simple terminal application that displays a counter. The user can
leave it with Esc
.
(* ocamlfind ocamlc -o basics_Lwt_Term_simple_terminal_timeout -package lwt,notty.lwt -linkpkg -g basics_Lwt_Term_simple_terminal_timeout.ml *)
open Lwt
open Lwt.Infix
open Notty
open Notty_lwt
open Notty.Infix
module Term = Notty_lwt.Term
let counter = ref 0
let rec increase_counter () =
Lwt_unix.sleep 0.1
>>= fun () ->
(
if !counter < max_int then counter := (!counter + 1)
else counter := 0
);
Lwt.return ()
>>= fun () ->
increase_counter ()
let render (w, h) =
I.(strf ~attr:A.(fg lightblack) "[counter %d]" !counter)
let timer () = Lwt_unix.sleep 0.1 >|= fun () -> `Timer
let event term = Lwt_stream.get (Term.events term) >|= function
| Some (`Resize _ | #Unescape.event as x) -> x
| None -> `End
let rec loop term (e, t) dim =
(e <?> t) >>= function
| `End | `Key (`Escape, []) ->
Lwt.return_unit
| `Timer ->
Term.image term (render dim)
>>= fun () ->
loop term (e, timer ()) dim
| `Mouse ((`Press _|`Drag), (x, y), _) ->
loop term (event term, t) dim
| `Resize dim ->
Term.image term (render dim)
>>= fun () ->
loop term (event term, t) dim
| _ -> loop term (event term, t) dim
let interface () =
let tc = Unix.(tcgetattr stdin) in
Unix.(tcsetattr stdin TCSANOW { tc with c_isig = false });
let term = Term.create () in
let size = Term.size term in
loop term (event term, timer ()) size
let main () =
Lwt.choose [
increase_counter ();
interface ();
]
let () = Lwt_main.run (main ())