Skip to content

Commit

Permalink
feat(leaves): Add a table component (#31)
Browse files Browse the repository at this point in the history
* feat(leaves): add a table component
* refactor(leaves): make keybindings responsibility of parent component
  • Loading branch information
sabine authored Feb 16, 2024
1 parent 7a64431 commit bac2173
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
_build
_opam
.DS_Store
3 changes: 2 additions & 1 deletion dune-project
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
(ocaml (>= "5.1"))
(riot (>= "0.0.5"))
(mdx (and :with-test (>= "2.3.1")))
(tty (>= "0.0.2")))
(tty (>= "0.0.2"))
uuseg)
(tags (tui "terminal-ui" framework riot)))

(package
Expand Down
Binary file added examples/table/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions examples/table/demo.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Output demo.gif

Require echo

Set Shell "bash"
Set Framerate 24
Set FontSize 32
Set Width 1200
Set Height 600

Type "dune exec --no-print-directory ./main.exe"
Enter
Sleep 1s
Down
Sleep 0.2s
Down
Sleep 0.2s
Down
Sleep 0.2s
Down
Sleep 0.2s
Down
Sleep 0.2s
Down
Sleep 0.2s
Space
Sleep 0.2s
Space
Sleep 0.2s
Down
Sleep 0.2s
Down
Sleep 0.3s
Up
Sleep 0.3s
Up
Sleep 0.3s
Up
Sleep 0.3s
Up
Sleep 0.3s
Up
Sleep 0.2s
Down
Sleep 0.2s
Down
Sleep 2s
3 changes: 3 additions & 0 deletions examples/table/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(executable
(name main)
(libraries minttea spices leaves))
144 changes: 144 additions & 0 deletions examples/table/main.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
open Minttea
open Leaves

type model = { table : Table.t }

let initial_model =
{
table =
{
columns =
[|
{ title = "Rank"; width = 4 };
{ title = "City"; width = 10 };
{ title = "Country"; width = 10 };
{ title = "Population"; width = 10 };
|];
rows =
[
[ "1"; "Tokyo"; "Japan"; "37,274,000" ];
[ "2"; "Delhi"; "India"; "32,065,760" ];
[ "3"; "Shanghai"; "China"; "28,516,904" ];
[ "4"; "Dhaka"; "Bangladesh"; "22,478,116" ];
[ "5"; "São Paulo"; "Brazil"; "22,429,800" ];
[ "6"; "Mexico City"; "Mexico"; "22,085,140" ];
[ "7"; "Cairo"; "Egypt"; "21,750,020" ];
[ "8"; "Beijing"; "China"; "21,333,332" ];
[ "9"; "Mumbai"; "India"; "20,961,472" ];
[ "10"; "Osaka"; "Japan"; "19,059,856" ];
[ "11"; "Chongqing"; "China"; "16,874,740" ];
[ "12"; "Karachi"; "Pakistan"; "16,839,950" ];
[ "13"; "Istanbul"; "Turkey"; "15,636,243" ];
[ "14"; "Kinshasa"; "DR Congo"; "15,628,085" ];
[ "15"; "Lagos"; "Nigeria"; "15,387,639" ];
[ "16"; "Buenos Aires"; "Argentina"; "15,369,919" ];
[ "17"; "Kolkata"; "India"; "15,133,888" ];
[ "18"; "Manila"; "Philippines"; "14,406,059" ];
[ "19"; "Tianjin"; "China"; "14,011,828" ];
[ "20"; "Guangzhou"; "China"; "13,964,637" ];
[ "21"; "Rio De Janeiro"; "Brazil"; "13,634,274" ];
[ "22"; "Lahore"; "Pakistan"; "13,541,764" ];
[ "23"; "Bangalore"; "India"; "13,193,035" ];
[ "24"; "Shenzhen"; "China"; "12,831,330" ];
[ "25"; "Moscow"; "Russia"; "12,640,818" ];
[ "26"; "Chennai"; "India"; "11,503,293" ];
[ "27"; "Bogota"; "Colombia"; "11,344,312" ];
[ "28"; "Paris"; "France"; "11,142,303" ];
[ "29"; "Jakarta"; "Indonesia"; "11,074,811" ];
[ "30"; "Lima"; "Peru"; "11,044,607" ];
[ "31"; "Bangkok"; "Thailand"; "10,899,698" ];
[ "32"; "Hyderabad"; "India"; "10,534,418" ];
[ "33"; "Seoul"; "South Korea"; "9,975,709" ];
[ "34"; "Nagoya"; "Japan"; "9,571,596" ];
[ "35"; "London"; "United Kingdom"; "9,540,576" ];
[ "36"; "Chengdu"; "China"; "9,478,521" ];
[ "37"; "Nanjing"; "China"; "9,429,381" ];
[ "38"; "Tehran"; "Iran"; "9,381,546" ];
[ "39"; "Ho Chi Minh City"; "Vietnam"; "9,077,158" ];
[ "40"; "Luanda"; "Angola"; "8,952,496" ];
[ "41"; "Wuhan"; "China"; "8,591,611" ];
[ "42"; "Xi An Shaanxi"; "China"; "8,537,646" ];
[ "43"; "Ahmedabad"; "India"; "8,450,228" ];
[ "44"; "Kuala Lumpur"; "Malaysia"; "8,419,566" ];
[ "45"; "New York City"; "United States"; "8,177,020" ];
[ "46"; "Hangzhou"; "China"; "8,044,878" ];
[ "47"; "Surat"; "India"; "7,784,276" ];
[ "48"; "Suzhou"; "China"; "7,764,499" ];
[ "49"; "Hong Kong"; "Hong Kong"; "7,643,256" ];
[ "50"; "Riyadh"; "Saudi Arabia"; "7,538,200" ];
[ "51"; "Shenyang"; "China"; "7,527,975" ];
[ "52"; "Baghdad"; "Iraq"; "7,511,920" ];
[ "53"; "Dongguan"; "China"; "7,511,851" ];
[ "54"; "Foshan"; "China"; "7,497,263" ];
[ "55"; "Dar Es Salaam"; "Tanzania"; "7,404,689" ];
[ "56"; "Pune"; "India"; "6,987,077" ];
[ "57"; "Santiago"; "Chile"; "6,856,939" ];
[ "58"; "Madrid"; "Spain"; "6,713,557" ];
[ "59"; "Haerbin"; "China"; "6,665,951" ];
[ "60"; "Toronto"; "Canada"; "6,312,974" ];
[ "61"; "Belo Horizonte"; "Brazil"; "6,194,292" ];
[ "62"; "Khartoum"; "Sudan"; "6,160,327" ];
[ "63"; "Johannesburg"; "South Africa"; "6,065,354" ];
[ "64"; "Singapore"; "Singapore"; "6,039,577" ];
[ "65"; "Dalian"; "China"; "5,930,140" ];
[ "66"; "Qingdao"; "China"; "5,865,232" ];
[ "67"; "Zhengzhou"; "China"; "5,690,312" ];
[ "68"; "Ji Nan Shandong"; "China"; "5,663,015" ];
[ "69"; "Barcelona"; "Spain"; "5,658,472" ];
[ "70"; "Saint Petersburg"; "Russia"; "5,535,556" ];
[ "71"; "Abidjan"; "Ivory Coast"; "5,515,790" ];
[ "72"; "Yangon"; "Myanmar"; "5,514,454" ];
[ "73"; "Fukuoka"; "Japan"; "5,502,591" ];
[ "74"; "Alexandria"; "Egypt"; "5,483,605" ];
[ "75"; "Guadalajara"; "Mexico"; "5,339,583" ];
[ "76"; "Ankara"; "Turkey"; "5,309,690" ];
[ "77"; "Chittagong"; "Bangladesh"; "5,252,842" ];
[ "78"; "Addis Ababa"; "Ethiopia"; "5,227,794" ];
[ "79"; "Melbourne"; "Australia"; "5,150,766" ];
[ "80"; "Nairobi"; "Kenya"; "5,118,844" ];
[ "81"; "Hanoi"; "Vietnam"; "5,067,352" ];
[ "82"; "Sydney"; "Australia"; "5,056,571" ];
[ "83"; "Monterrey"; "Mexico"; "5,036,535" ];
[ "84"; "Changsha"; "China"; "4,809,887" ];
[ "85"; "Brasilia"; "Brazil"; "4,803,877" ];
[ "86"; "Cape Town"; "South Africa"; "4,800,954" ];
[ "87"; "Jiddah"; "Saudi Arabia"; "4,780,740" ];
[ "88"; "Urumqi"; "China"; "4,710,203" ];
[ "89"; "Kunming"; "China"; "4,657,381" ];
[ "90"; "Changchun"; "China"; "4,616,002" ];
[ "91"; "Hefei"; "China"; "4,496,456" ];
[ "92"; "Shantou"; "China"; "4,490,411" ];
[ "93"; "Xinbei"; "Taiwan"; "4,470,672" ];
[ "94"; "Kabul"; "Afghanistan"; "4,457,882" ];
[ "95"; "Ningbo"; "China"; "4,405,292" ];
[ "96"; "Tel Aviv"; "Israel"; "4,343,584" ];
[ "97"; "Yaounde"; "Cameroon"; "4,336,670" ];
[ "98"; "Rome"; "Italy"; "4,297,877" ];
[ "99"; "Shijiazhuang"; "China"; "4,285,135" ];
[ "100"; "Montreal"; "Canada"; "4,276,526" ];
];
cursor = 0;
styles = Table.default_styles;
start_of_frame = 0;
height_of_frame = 5;
};
}

let init _ = Command.Noop

let update event model =
match event with
| Event.KeyDown (Key "q") -> (model, Command.Quit)
| Event.KeyDown (Key "b") ->
({ table = Table.update model.table Table.PageUp }, Command.Noop)
| Event.KeyDown (Key "f") | Event.KeyDown Space ->
({ table = Table.update model.table Table.PageDown }, Command.Noop)
| Event.KeyDown Up ->
({ table = Table.update model.table Table.Up }, Command.Noop)
| Event.KeyDown Down ->
({ table = Table.update model.table Table.Down }, Command.Noop)
| _ -> (model, Command.Noop)

let view model = Table.view model.table ^ "\n\nhint: up/down b/f/space, quit: q"
let app = Minttea.app ~init ~update ~view ()
let () = Minttea.start ~initial_model app
2 changes: 1 addition & 1 deletion leaves/dune
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
(library
(public_name leaves)
(name leaves)
(libraries minttea spices ptime ptime.clock.os))
(libraries minttea spices ptime ptime.clock.os uuseg))
108 changes: 108 additions & 0 deletions leaves/table.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
type column = { title : string; width : int }
type row = string list

type styles = {
cursor : Spices.style;
header : Spices.style;
cell : Spices.style;
}

let default_styles =
{
cursor =
Spices.(
default |> bold true |> fg (color "#FFFFFF") |> bg (color "#053d37"));
header =
Spices.(
default |> bold true
|> fg (color "#FFFFFF")
|> padding_right 1 |> padding_left 1);
cell = Spices.(default |> padding_right 1 |> padding_left 1);
}

type t = {
columns : column array;
rows : row list;
styles : styles;
cursor : int;
start_of_frame : int;
height_of_frame : int;
}

type action = Up | Down | PageUp | PageDown

let update t (e : action) =
let go_up t steps =
let cursor = max (t.cursor - steps) 0 in
let start_of_frame =
if cursor < t.start_of_frame then cursor else t.start_of_frame
in
{ t with cursor; start_of_frame }
in
let go_down t steps =
let cursor = min (t.cursor + steps) (List.length t.rows - 1) in
let start_of_frame =
if cursor > t.start_of_frame + t.height_of_frame - 1 then
cursor - t.height_of_frame + 1
else t.start_of_frame
in
{ t with cursor; start_of_frame }
in
match e with
| PageUp -> go_up t t.height_of_frame
| PageDown -> go_down t t.height_of_frame
| Up -> go_up t 1
| Down -> go_down t 1

let truncate_or_pad_unicode ~width str =
(* Truncates a Unicode string to a target width with an ellipsis
or pads it with spaces on the right. *)
let uuseg_string_length s =
(* TODO(@sabine): we probably don't want the uuseg dependency
or move this somewhere where it actually belongs. Also for uuseg_string_sub,
we need to write actually good code. *)
Uuseg_string.fold_utf_8 `Grapheme_cluster (fun len _ -> len + 1) 0 s
in
let uuseg_string_sub s start n =
let inner s start n acc =
Uuseg_string.fold_utf_8 `Grapheme_cluster
(fun (pos, collected, s) c ->
if pos >= start && collected < n then (pos + 1, collected + 1, s ^ c)
else (pos + 1, collected, s))
acc s
in
let _, _, r = inner s start n (0, 0, "") in
r
in
let len = uuseg_string_length str in
if len > width then uuseg_string_sub str 0 (width - 1) ^ ""
else str ^ String.make (width - len) ' '

let view t =
let column_titles =
let render_title col =
truncate_or_pad_unicode ~width:col.width col.title
|> (t.styles.header |> Spices.build) "%s"
in
t.columns |> Array.to_list |> List.map render_title |> String.concat ""
in
let rows =
let get_visible_rows t =
t.rows |> List.to_seq |> Seq.drop t.start_of_frame
|> Seq.take t.height_of_frame |> List.of_seq
in
let render_row i row =
let render_cell i item =
truncate_or_pad_unicode ~width:t.columns.(i).width item
|> (t.styles.cell |> Spices.build) "%s"
in
let row = row |> List.mapi render_cell |> String.concat "" in
if i = t.cursor - t.start_of_frame then
(t.styles.cursor |> Spices.build) "%s" row
else row
in
t |> get_visible_rows |> List.mapi render_row |> String.concat "\n"
in
Printf.sprintf {|%s

%s|} column_titles rows
1 change: 1 addition & 0 deletions minttea.opam
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ depends: [
"riot" {>= "0.0.5"}
"mdx" {with-test & >= "2.3.1"}
"tty" {>= "0.0.2"}
"uuseg"
"odoc" {with-doc}
]
build: [
Expand Down

0 comments on commit bac2173

Please sign in to comment.