···2233(* Types *)
4455-type scale = int
66-type width = int
77-type numerator = int
88-type denominator = int
99-1010-type vertical_align =
1111- | Bottom
1212- | Center
1313- | Top
55+type vertical = [ `Bottom | `Center | `Top ]
66+type horizontal = [ `Left | `Center | `Right ]
1471515-type horizontal_align =
1616- | Left
1717- | Center
1818- | Right
1919-2020-type metadata = {
2121- scale : scale option;
2222- width : width option;
2323- numerator : numerator option;
2424- denominator : denominator option;
2525- vertical : vertical_align option;
2626- horizontal : horizontal_align option;
88+type t = {
99+ scale : int option;
1010+ width : int option;
1111+ numerator : int option;
1212+ denominator : int option;
1313+ vertical : vertical option;
1414+ horizontal : horizontal option;
2715}
28162929-(* Constructors with validation *)
1717+(* Validation helpers *)
30183131-let make_scale n =
1919+let validate_scale n =
3220 if n < 1 || n > 7 then
3321 invalid_arg (Printf.sprintf "scale must be in range 1-7, got %d" n)
3434- else
3535- n
36223737-let make_width n =
2323+let validate_width n =
3824 if n < 0 || n > 7 then
3925 invalid_arg (Printf.sprintf "width must be in range 0-7, got %d" n)
4040- else
4141- n
42264343-let make_numerator n =
2727+let validate_numerator n =
4428 if n < 0 || n > 15 then
4529 invalid_arg (Printf.sprintf "numerator must be in range 0-15, got %d" n)
4646- else
4747- n
48304949-let make_denominator n =
3131+let validate_denominator n =
5032 if n < 0 || n > 15 then
5133 invalid_arg (Printf.sprintf "denominator must be in range 0-15, got %d" n)
5252- else
5353- n
54345555-(* Metadata creation *)
3535+(* Constructor *)
56365757-let empty = {
5858- scale = None;
5959- width = None;
6060- numerator = None;
6161- denominator = None;
6262- vertical = None;
6363- horizontal = None;
6464-}
3737+let v ?scale ?width ?fraction ?vertical ?horizontal () =
3838+ Option.iter validate_scale scale;
3939+ Option.iter validate_width width;
4040+ let numerator, denominator = match fraction with
4141+ | None -> None, None
4242+ | Some (num, den) ->
4343+ validate_numerator num;
4444+ validate_denominator den;
4545+ Some num, Some den
4646+ in
4747+ {
4848+ scale;
4949+ width;
5050+ numerator;
5151+ denominator;
5252+ vertical;
5353+ horizontal;
5454+ }
65556666-let with_scale s metadata = { metadata with scale = Some s }
6767-let with_width w metadata = { metadata with width = Some w }
6868-6969-let with_fraction num den metadata =
7070- { metadata with numerator = Some num; denominator = Some den }
7171-7272-let with_vertical_align v metadata = { metadata with vertical = Some v }
7373-let with_horizontal_align h metadata = { metadata with horizontal = Some h }
5656+let empty = v ()
74577558(* Conversion helpers *)
76597777-let vertical_align_to_int = function
7878- | Bottom -> 0
7979- | Center -> 1
8080- | Top -> 2
6060+let vertical_to_int = function
6161+ | `Bottom -> 0
6262+ | `Center -> 1
6363+ | `Top -> 2
81648282-let horizontal_align_to_int = function
8383- | Left -> 0
8484- | Center -> 1
8585- | Right -> 2
6565+let horizontal_to_int = function
6666+ | `Left -> 0
6767+ | `Center -> 1
6868+ | `Right -> 2
86698770(* Escape sequence generation *)
88718989-let metadata_to_string metadata =
7272+let metadata_to_string t =
9073 let parts = [] in
9191- let parts = match metadata.scale with
7474+ let parts = match t.scale with
9275 | Some s -> (Printf.sprintf "s=%d" s) :: parts
9376 | None -> parts
9477 in
9595- let parts = match metadata.width with
7878+ let parts = match t.width with
9679 | Some w -> (Printf.sprintf "w=%d" w) :: parts
9780 | None -> parts
9881 in
9999- let parts = match metadata.numerator with
8282+ let parts = match t.numerator with
10083 | Some n -> (Printf.sprintf "n=%d" n) :: parts
10184 | None -> parts
10285 in
103103- let parts = match metadata.denominator with
8686+ let parts = match t.denominator with
10487 | Some d -> (Printf.sprintf "d=%d" d) :: parts
10588 | None -> parts
10689 in
107107- let parts = match metadata.vertical with
108108- | Some v -> (Printf.sprintf "v=%d" (vertical_align_to_int v)) :: parts
9090+ let parts = match t.vertical with
9191+ | Some v -> (Printf.sprintf "v=%d" (vertical_to_int v)) :: parts
10992 | None -> parts
11093 in
111111- let parts = match metadata.horizontal with
112112- | Some h -> (Printf.sprintf "h=%d" (horizontal_align_to_int h)) :: parts
9494+ let parts = match t.horizontal with
9595+ | Some h -> (Printf.sprintf "h=%d" (horizontal_to_int h)) :: parts
11396 | None -> parts
11497 in
11598 String.concat ":" (List.rev parts)
11699117117-let render metadata text =
100100+let render t text =
118101 (* Validate text length (max 4096 bytes of UTF-8) *)
119102 let text_len = String.length text in
120103 if text_len > 4096 then
121104 invalid_arg (Printf.sprintf "text exceeds 4096 bytes (got %d)" text_len);
122105123123- let metadata_str = metadata_to_string metadata in
106106+ let metadata_str = metadata_to_string t in
124107125108 (* OSC 66 ; metadata ; text BEL *)
126109 (* Using \x1b for ESC and \x07 for BEL *)
···129112 else
130113 Printf.sprintf "\x1b]66;%s;%s\x07" metadata_str text
131114132132-let render_to_channel oc metadata text =
133133- output_string oc (render metadata text);
115115+let render_to_channel oc t text =
116116+ output_string oc (render t text);
134117 flush oc
135118136136-(* Convenience functions *)
119119+(* Fmt-style combinators *)
120120+121121+let pp t ppf text =
122122+ Format.pp_print_string ppf (render t text)
123123+124124+let styled t pp_inner ppf value =
125125+ (* We need to capture the inner formatter's output as a string first *)
126126+ let inner_str = Format.asprintf "%a" pp_inner value in
127127+ Format.pp_print_string ppf (render t inner_str)
128128+129129+(* Convenience Fmt formatters *)
137130138138-let double text =
139139- render (with_scale (make_scale 2) empty) text
131131+let pp_double = pp (v ~scale:2 ())
132132+let pp_triple = pp (v ~scale:3 ())
133133+let pp_quadruple = pp (v ~scale:4 ())
134134+let pp_half = pp (v ~fraction:(1, 2) ())
140135141141-let triple text =
142142- render (with_scale (make_scale 3) empty) text
136136+let pp_superscript = pp (v ~fraction:(1, 2) ~vertical:`Top ())
137137+let pp_subscript = pp (v ~fraction:(1, 2) ~vertical:`Bottom ())
143138144144-let quadruple text =
145145- render (with_scale (make_scale 4) empty) text
139139+let pp_scaled n = pp (v ~scale:n ())
146140147147-let half text =
148148- render
149149- (with_fraction (make_numerator 1) (make_denominator 2) empty)
150150- text
141141+let styled_double pp_inner = styled (v ~scale:2 ()) pp_inner
142142+let styled_triple pp_inner = styled (v ~scale:3 ()) pp_inner
151143152152-let superscript text =
153153- render
154154- (empty
155155- |> with_fraction (make_numerator 1) (make_denominator 2)
156156- |> with_vertical_align Top)
157157- text
144144+let styled_superscript pp_inner = styled (v ~fraction:(1, 2) ~vertical:`Top ()) pp_inner
145145+let styled_subscript pp_inner = styled (v ~fraction:(1, 2) ~vertical:`Bottom ()) pp_inner
158146159159-let subscript text =
160160- render
161161- (empty
162162- |> with_fraction (make_numerator 1) (make_denominator 2)
163163- |> with_vertical_align Bottom)
164164- text
147147+(* Convenience functions *)
165148166166-let scaled n text =
167167- render (with_scale (make_scale n) empty) text
149149+let double text = render (v ~scale:2 ()) text
150150+let triple text = render (v ~scale:3 ()) text
151151+let quadruple text = render (v ~scale:4 ()) text
152152+let half text = render (v ~fraction:(1, 2) ()) text
153153+let superscript text = render (v ~fraction:(1, 2) ~vertical:`Top ()) text
154154+let subscript text = render (v ~fraction:(1, 2) ~vertical:`Bottom ()) text
155155+let scaled n text = render (v ~scale:n ()) text
+74-77
tgp/src/textsize.mli
···11111212(** {1 Types} *)
13131414-(** Scale factor for text rendering (1-7).
1414+(** Vertical alignment for fractionally scaled text. *)
1515+type vertical = [ `Bottom | `Center | `Top ]
15161616- Controls the overall size of the text block. A scale of [s] will render text
1717- in a block of [s * w] by [s] cells (where [w] is the width parameter). *)
1818-type scale = private int
1717+(** Horizontal alignment for fractionally scaled text. *)
1818+type horizontal = [ `Left | `Center | `Right ]
19192020-(** Width in cells for text rendering (0-7).
2020+(** Abstract metadata for text sizing. *)
2121+type t
21222222- When set to 0, width is auto-calculated based on text length. *)
2323-type width = private int
2323+(** {1 Constructor} *)
24242525-(** Fractional scaling numerator (0-15).
2525+(** [v ?scale ?width ?fraction ?vertical ?horizontal ()] creates text sizing metadata.
26262727- Used together with denominator for precise fractional scaling. *)
2828-type numerator = private int
2727+ @param scale Scale factor (1-7)
2828+ @param width Width in cells (0-7)
2929+ @param fraction Fractional scaling as (numerator, denominator) where both are 0-15
3030+ @param vertical Vertical alignment
3131+ @param horizontal Horizontal alignment
3232+ @raise Invalid_argument if any parameter is out of range *)
3333+val v :
3434+ ?scale:int ->
3535+ ?width:int ->
3636+ ?fraction:(int * int) ->
3737+ ?vertical:vertical ->
3838+ ?horizontal:horizontal ->
3939+ unit -> t
29403030-(** Fractional scaling denominator (0-15).
4141+(** Empty metadata (all fields set to [None]). Equivalent to [v ()]. *)
4242+val empty : t
31433232- Used together with numerator for precise fractional scaling. *)
3333-type denominator = private int
4444+(** {1 Escape Sequence Generation} *)
34453535-(** Vertical alignment for fractionally scaled text.
3636- - [Bottom]: Align to bottom (0)
3737- - [Center]: Align to center (1)
3838- - [Top]: Align to top (2) *)
3939-type vertical_align =
4040- | Bottom
4141- | Center
4242- | Top
4646+(** [render t text] generates the complete escape sequence for sized text.
43474444-(** Horizontal alignment for fractionally scaled text.
4545- - [Left]: Align to left (0)
4646- - [Center]: Align to center (1)
4747- - [Right]: Align to right (2) *)
4848-type horizontal_align =
4949- | Left
5050- | Center
5151- | Right
4848+ @param t The sizing metadata
4949+ @param text The text to render (max 4096 bytes of UTF-8)
5050+ @return The escape sequence string
5151+ @raise Invalid_argument if text exceeds 4096 bytes *)
5252+val render : t -> string -> string
52535353-(** Metadata for text sizing. *)
5454-type metadata = {
5555- scale : scale option;
5656- width : width option;
5757- numerator : numerator option;
5858- denominator : denominator option;
5959- vertical : vertical_align option;
6060- horizontal : horizontal_align option;
6161-}
5454+(** [render_to_channel oc t text] writes the escape sequence to a channel.
62556363-(** {1 Constructors} *)
5656+ @param oc Output channel
5757+ @param t The sizing metadata
5858+ @param text The text to render (max 4096 bytes of UTF-8)
5959+ @raise Invalid_argument if text exceeds 4096 bytes *)
6060+val render_to_channel : out_channel -> t -> string -> unit
64616565-(** [make_scale n] creates a scale value.
6666- @raise Invalid_argument if [n] is not in range 1-7. *)
6767-val make_scale : int -> scale
6262+(** {1 Fmt-style Combinators} *)
68636969-(** [make_width n] creates a width value.
7070- @raise Invalid_argument if [n] is not in range 0-7. *)
7171-val make_width : int -> width
6464+(** [pp t] creates a Fmt formatter that wraps text with sizing metadata.
72657373-(** [make_numerator n] creates a numerator value.
7474- @raise Invalid_argument if [n] is not in range 0-15. *)
7575-val make_numerator : int -> numerator
6666+ Example:
6767+ {[
6868+ Fmt.pr "This is %a" (Textsize.pp (Textsize.with_scale 2 Textsize.empty)) "big"
6969+ ]}
7070+*)
7171+val pp : t -> string Fmt.t
76727777-(** [make_denominator n] creates a denominator value.
7878- @raise Invalid_argument if [n] is not in range 0-15. *)
7979-val make_denominator : int -> denominator
7373+(** [styled t pp_inner] wraps any formatter with sizing metadata.
80748181-(** {1 Metadata Creation} *)
7575+ Example:
7676+ {[
7777+ Fmt.pr "Value: %a" (Textsize.styled (Textsize.with_scale 3 Textsize.empty) Fmt.int) 42
7878+ ]}
7979+*)
8080+val styled : t -> 'a Fmt.t -> 'a Fmt.t
82818383-(** Empty metadata (all fields set to [None]). *)
8484-val empty : metadata
8282+(** Fmt formatter for double-sized text. *)
8383+val pp_double : string Fmt.t
85848686-(** [with_scale s metadata] sets the scale. *)
8787-val with_scale : scale -> metadata -> metadata
8585+(** Fmt formatter for triple-sized text. *)
8686+val pp_triple : string Fmt.t
88878989-(** [with_width w metadata] sets the width. *)
9090-val with_width : width -> metadata -> metadata
8888+(** Fmt formatter for quadruple-sized text. *)
8989+val pp_quadruple : string Fmt.t
91909292-(** [with_fraction num den metadata] sets fractional scaling. *)
9393-val with_fraction : numerator -> denominator -> metadata -> metadata
9191+(** Fmt formatter for half-sized text. *)
9292+val pp_half : string Fmt.t
94939595-(** [with_vertical_align v metadata] sets vertical alignment. *)
9696-val with_vertical_align : vertical_align -> metadata -> metadata
9494+(** Fmt formatter for superscript text. *)
9595+val pp_superscript : string Fmt.t
97969898-(** [with_horizontal_align h metadata] sets horizontal alignment. *)
9999-val with_horizontal_align : horizontal_align -> metadata -> metadata
9797+(** Fmt formatter for subscript text. *)
9898+val pp_subscript : string Fmt.t
10099101101-(** {1 Escape Sequence Generation} *)
100100+(** [pp_scaled n] creates a Fmt formatter for text at scale [n].
101101+ @raise Invalid_argument if [n] is not in range 1-7. *)
102102+val pp_scaled : int -> string Fmt.t
102103103103-(** [render metadata text] generates the complete escape sequence for sized text.
104104+(** [styled_double pp_inner] wraps any formatter with double sizing. *)
105105+val styled_double : 'a Fmt.t -> 'a Fmt.t
104106105105- @param metadata The sizing metadata
106106- @param text The text to render (max 4096 bytes of UTF-8)
107107- @return The escape sequence string
108108- @raise Invalid_argument if text exceeds 4096 bytes *)
109109-val render : metadata -> string -> string
107107+(** [styled_triple pp_inner] wraps any formatter with triple sizing. *)
108108+val styled_triple : 'a Fmt.t -> 'a Fmt.t
110109111111-(** [render_to_channel oc metadata text] writes the escape sequence to a channel.
110110+(** [styled_superscript pp_inner] wraps any formatter with superscript styling. *)
111111+val styled_superscript : 'a Fmt.t -> 'a Fmt.t
112112113113- @param oc Output channel
114114- @param metadata The sizing metadata
115115- @param text The text to render (max 4096 bytes of UTF-8)
116116- @raise Invalid_argument if text exceeds 4096 bytes *)
117117-val render_to_channel : out_channel -> metadata -> string -> unit
113113+(** [styled_subscript pp_inner] wraps any formatter with subscript styling. *)
114114+val styled_subscript : 'a Fmt.t -> 'a Fmt.t
118115119116(** {1 Convenience Functions} *)
120117
+8-17
tgp/test/test_textsize.ml
···13131414 (* Test scale validation *)
1515 test "scale validation - valid range" (fun () ->
1616- let _ = Textsize.make_scale 1 in
1717- let _ = Textsize.make_scale 7 in
1616+ let _ = Textsize.v ~scale:1 () in
1717+ let _ = Textsize.v ~scale:7 () in
1818 ()
1919 );
20202121 test "scale validation - rejects invalid" (fun () ->
2222 try
2323- let _ = Textsize.make_scale 0 in
2323+ let _ = Textsize.v ~scale:0 () in
2424 failwith "Should have raised Invalid_argument"
2525 with Invalid_argument _ -> ()
2626 );
27272828 (* Test width validation *)
2929 test "width validation - valid range" (fun () ->
3030- let _ = Textsize.make_width 0 in
3131- let _ = Textsize.make_width 7 in
3030+ let _ = Textsize.v ~width:0 () in
3131+ let _ = Textsize.v ~width:7 () in
3232 ()
3333 );
3434···61616262 (* Test custom metadata *)
6363 test "custom metadata - scale and width" (fun () ->
6464- let metadata =
6565- Textsize.empty
6666- |> Textsize.with_scale (Textsize.make_scale 3)
6767- |> Textsize.with_width (Textsize.make_width 5)
6868- in
6464+ let metadata = Textsize.v ~scale:3 ~width:5 () in
6965 let result = Textsize.render metadata "custom" in
7066 assert (result = "\x1b]66;s=3:w=5;custom\x07")
7167 );
72687369 test "custom metadata - fractional" (fun () ->
7474- let metadata =
7575- Textsize.empty
7676- |> Textsize.with_fraction
7777- (Textsize.make_numerator 2)
7878- (Textsize.make_denominator 3)
7979- in
7070+ let metadata = Textsize.v ~fraction:(2, 3) () in
8071 let result = Textsize.render metadata "frac" in
8172 assert (result = "\x1b]66;n=2:d=3;frac\x07")
8273 );
83748475 test "empty metadata" (fun () ->
8585- let result = Textsize.render Textsize.empty "plain" in
7676+ let result = Textsize.render (Textsize.v ()) "plain" in
8677 assert (result = "\x1b]66;;plain\x07")
8778 );
8879