├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGES.md ├── LICENSE.md ├── README.md ├── doc └── api.odocl ├── dune-project ├── src ├── dune ├── libterminal_size_stubs.clib ├── terminal_size.ml ├── terminal_size.mli ├── terminal_size.mllib └── terminal_size_stubs.c ├── terminal_size.opam └── test ├── dune ├── libmock_ioctl.clib ├── mock_ioctl_linux.c ├── mock_ioctl_mac.c └── testsuite.ml /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | - pull_request 5 | - push 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - macos-latest 14 | - ubuntu-latest 15 | - windows-latest 16 | ocaml-compiler: 17 | - 4.13.x 18 | - 4.08.x 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v2 23 | - name: Use OCaml ${{ matrix.ocaml-compiler }} 24 | uses: ocaml/setup-ocaml@v2 25 | with: 26 | ocaml-compiler: ${{ matrix.ocaml-compiler }} 27 | - run: opam pin add terminal_size.dev . --no-action 28 | - run: opam depext terminal_size --yes --with-test 29 | - run: opam install . --deps-only --with-test 30 | - run: opam exec -- dune build @all @runtest 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | *.install 3 | *.merlin 4 | *.native 5 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | v0.2.0 2 | ------ 3 | 4 | *2022-04-26* 5 | 6 | - Add compatibility with Windows. 7 | - Require Dune >= 2.0 and OCaml >= 4.08. 8 | 9 | v0.1.4 10 | ------ 11 | 12 | *2019-07-05* 13 | 14 | Build system: 15 | 16 | - Upgrade to opam 2 17 | - Build with dune 18 | 19 | v0.1.3 20 | ------ 21 | 22 | *2017-05-02* 23 | 24 | Tests: 25 | 26 | - Use the correct signature for `ioctl`. 27 | This fixes tests on OSX (#8, #9). 28 | 29 | v0.1.2 30 | ------ 31 | 32 | *2017-05-02* 33 | 34 | Build system: 35 | 36 | - Fix profiling information in META (#6) 37 | - Support OCaml 4.05 (#7) 38 | - Use travis-docker 39 | 40 | v0.1.1 41 | ------ 42 | 43 | *2016-09-08* 44 | 45 | Add a test suite. 46 | 47 | v0.1.0 48 | ------ 49 | 50 | *2016-09-08* 51 | 52 | First release. 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Cryptosense SA 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `Terminal_size` 2 | 3 | [![Build Status][build_status_badge]][build_status_link] 4 | [![Documentation][doc_badge]][doc_link] 5 | 6 | ## What is it? 7 | 8 | You can use this small ocaml library to detect the dimensions of the terminal 9 | window attached to a process. It contains the two following functions: 10 | 11 | ```ocaml 12 | val get_rows : unit -> int option 13 | val get_columns : unit -> int option 14 | ``` 15 | 16 | ## How does it work? 17 | 18 | Usually, to get this information, one would open a pipe from `tput cols` or 19 | `stty size` and parsing the output. Instead, this uses the `ioctl` that these 20 | commands use, `TIOCGWINSZ`. 21 | 22 | [build_status_badge]: https://github.com/cryptosense/terminal_size/actions/workflows/main.yml/badge.svg 23 | [build_status_link]: https://github.com/cryptosense/terminal_size/actions/workflows/main.yml 24 | [doc_badge]: https://img.shields.io/badge/doc-online-blue.svg 25 | [doc_link]: https://cryptosense.github.io/terminal_size/doc/ 26 | -------------------------------------------------------------------------------- /doc/api.odocl: -------------------------------------------------------------------------------- 1 | Terminal_size 2 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.0) 2 | (name terminal_size) 3 | -------------------------------------------------------------------------------- /src/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name terminal_size) 3 | (foreign_stubs 4 | (language c) 5 | (names terminal_size_stubs) 6 | ) 7 | ) 8 | -------------------------------------------------------------------------------- /src/libterminal_size_stubs.clib: -------------------------------------------------------------------------------- 1 | terminal_size_stubs.o 2 | -------------------------------------------------------------------------------- /src/terminal_size.ml: -------------------------------------------------------------------------------- 1 | external get : unit -> (int * int) option = "ocaml_terminal_size_get" 2 | 3 | let get_rows () = 4 | match get () with 5 | | Some (rows, _) -> Some rows 6 | | None -> None 7 | 8 | let get_columns () = 9 | match get () with 10 | | Some (_, columns) -> Some columns 11 | | None -> None 12 | -------------------------------------------------------------------------------- /src/terminal_size.mli: -------------------------------------------------------------------------------- 1 | (** Get the dimensions of the terminal *) 2 | 3 | (** Return the number of rows that can be displayed *) 4 | val get_rows : unit -> int option 5 | 6 | (** Return the number of columns that can be displayed *) 7 | val get_columns : unit -> int option 8 | -------------------------------------------------------------------------------- /src/terminal_size.mllib: -------------------------------------------------------------------------------- 1 | Terminal_size 2 | -------------------------------------------------------------------------------- /src/terminal_size_stubs.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct dimensions { 6 | int ok; 7 | int rows; 8 | int columns; 9 | }; 10 | 11 | #ifdef _WIN32 12 | #include 13 | 14 | struct dimensions get_dimensions() { 15 | CONSOLE_SCREEN_BUFFER_INFO info; 16 | 17 | int success = GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); 18 | 19 | if (!success) { 20 | return (struct dimensions) {.ok = 0, .rows = 0, .columns = 0}; 21 | } 22 | 23 | // Inspired from https://stackoverflow.com/questions/6812224/getting-terminal-size-in-c-for-windows. 24 | return (struct dimensions) { 25 | .ok = 1, 26 | .rows = info.srWindow.Bottom - info.srWindow.Top + 1, 27 | .columns = info.srWindow.Right - info.srWindow.Left + 1, 28 | }; 29 | } 30 | #else 31 | #include 32 | #include 33 | 34 | struct dimensions get_dimensions() { 35 | struct winsize ws; 36 | 37 | int z = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); 38 | 39 | if (!z) { 40 | return (struct dimensions) {.ok = 1, .rows = ws.ws_row, .columns = ws.ws_col}; 41 | } 42 | 43 | return (struct dimensions) {.ok = 0, .rows = 0, .columns = 0}; 44 | } 45 | #endif 46 | 47 | CAMLprim value ocaml_terminal_size_get(value unit) { 48 | CAMLparam1(unit); 49 | CAMLlocal2(result, pair); 50 | 51 | struct dimensions dimensions = get_dimensions(); 52 | 53 | if (dimensions.ok) { 54 | result = caml_alloc(1, 0); 55 | pair = caml_alloc(2, 0); 56 | Store_field(result, 0, pair); 57 | Store_field(pair, 0, Val_int(dimensions.rows)); 58 | Store_field(pair, 1, Val_int(dimensions.columns)); 59 | } else { 60 | result = Val_int(0); 61 | } 62 | 63 | CAMLreturn (result); 64 | } 65 | -------------------------------------------------------------------------------- /terminal_size.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | maintainer: "Cryptosense " 3 | authors: [ 4 | "Cryptosense " 5 | "Etienne Millon " 6 | ] 7 | homepage: "https://github.com/cryptosense/terminal_size" 8 | bug-reports: "https://github.com/cryptosense/terminal_size/issues" 9 | license: "BSD-2-Clause" 10 | dev-repo: "git+https://github.com/cryptosense/terminal_size.git" 11 | doc: "https://cryptosense.github.io/terminal_size/doc" 12 | build: [ 13 | ["dune" "build" "-p" name "-j" jobs] 14 | ] 15 | run-test: [ 16 | ["dune" "runtest" "-p" name "-j" jobs] 17 | ] 18 | depends: [ 19 | "alcotest" {with-test} 20 | "dune" {>= "2.0.0"} 21 | "ocaml" {>= "4.08.0"} 22 | ] 23 | synopsis: "Get the dimensions of the terminal" 24 | description: """ 25 | You can use this small library to detect the dimensions of the terminal window 26 | attached to a process. 27 | """ 28 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name testsuite) 3 | (enabled_if (= %{os_type} Unix)) 4 | (libraries 5 | alcotest 6 | terminal_size 7 | ) 8 | (action 9 | (setenv LD_PRELOAD ./%{dep:dllmock_ioctl.so} 10 | (setenv DYLD_INSERT_LIBRARIES ./%{dep:libmock_ioctl.dylib} 11 | (run %{test}) 12 | ) 13 | ) 14 | ) 15 | ) 16 | 17 | (rule 18 | (targets dllmock_ioctl.so) 19 | (enabled_if (= %{os_type} Unix)) 20 | (action (run %{cc} -shared -o %{targets} %{dep:mock_ioctl_linux.c})) 21 | ) 22 | 23 | (rule 24 | (targets libmock_ioctl.dylib) 25 | (enabled_if (= %{system} macosx)) 26 | (action (run %{cc} -dynamiclib -o %{targets} %{dep:mock_ioctl_mac.c})) 27 | ) 28 | 29 | (rule 30 | (targets libmock_ioctl.dylib) 31 | (enabled_if (<> %{system} macosx)) 32 | (action (write-file %{targets} "")) 33 | ) 34 | -------------------------------------------------------------------------------- /test/libmock_ioctl.clib: -------------------------------------------------------------------------------- 1 | mock_ioctl.o 2 | -------------------------------------------------------------------------------- /test/mock_ioctl_linux.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | /* Work around differing ioctl signatures between muscl (alpine), */ 10 | /* glibc (debian) and libsystem (mac) */ 11 | #if defined(__GLIBC__) || defined(__APPLE__) 12 | typedef unsigned long request_t; 13 | #else 14 | typedef int request_t; 15 | #endif 16 | 17 | typedef int (*ioctl_t)(int, request_t, char*); 18 | 19 | static ioctl_t real_ioctl = NULL; 20 | 21 | int ioctl(int fd, request_t request, ...) 22 | { 23 | va_list ap; 24 | va_start(ap, request); 25 | char *argp = va_arg(ap, char *); 26 | int ret = 0; 27 | if (real_ioctl == NULL) { 28 | real_ioctl = (ioctl_t) dlsym(RTLD_NEXT, "ioctl"); 29 | } 30 | if (request == TIOCGWINSZ) { 31 | char* p_rows = getenv("FAKE_ROWS"); 32 | char* p_cols = getenv("FAKE_COLS"); 33 | if (*p_rows == '\0' || *p_cols == '\0') { 34 | ret = -1; 35 | } else { 36 | struct winsize *pws = (struct winsize *) argp; 37 | pws->ws_row = atoi(p_rows); 38 | pws->ws_col = atoi(p_cols); 39 | ret = 0; 40 | } 41 | } else { 42 | ret = real_ioctl(fd, request, argp); 43 | } 44 | va_end(ap); 45 | return ret; 46 | } 47 | -------------------------------------------------------------------------------- /test/mock_ioctl_mac.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define DYLD_INTERPOSE(_replacment,_replacee) \ 10 | __attribute__((used)) static struct{ const void* replacment; const void* replacee; } _interpose_##_replacee \ 11 | __attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&_replacment, (const void*)(unsigned long)&_replacee }; 12 | 13 | 14 | int interposed_ioctl(int fd, unsigned long request, ...) 15 | { 16 | va_list ap; 17 | va_start(ap, request); 18 | char *argp = va_arg(ap, char *); 19 | int ret = 0; 20 | if (request == TIOCGWINSZ) { 21 | char* p_rows = getenv("FAKE_ROWS"); 22 | char* p_cols = getenv("FAKE_COLS"); 23 | if (*p_rows == '\0' || *p_cols == '\0') { 24 | ret = -1; 25 | } else { 26 | struct winsize *pws = (struct winsize *) argp; 27 | pws->ws_row = atoi(p_rows); 28 | pws->ws_col = atoi(p_cols); 29 | ret = 0; 30 | } 31 | } else { 32 | ret = ioctl(fd, request, argp); 33 | } 34 | va_end(ap); 35 | return ret; 36 | } 37 | 38 | DYLD_INTERPOSE(interposed_ioctl, ioctl); 39 | -------------------------------------------------------------------------------- /test/testsuite.ml: -------------------------------------------------------------------------------- 1 | let test_enabled () = 2 | Unix.putenv "FAKE_ROWS" "1234"; 3 | Unix.putenv "FAKE_COLS" "5678"; 4 | Alcotest.check Alcotest.(option int) "rows" (Terminal_size.get_rows ()) (Some 1234); 5 | Alcotest.check Alcotest.(option int) "columns" (Terminal_size.get_columns ()) (Some 5678) 6 | 7 | let test_disabled () = 8 | Unix.putenv "FAKE_ROWS" ""; 9 | Unix.putenv "FAKE_COLS" ""; 10 | Alcotest.check Alcotest.(option int) "rows" (Terminal_size.get_rows ()) None; 11 | Alcotest.check Alcotest.(option int) "columns" (Terminal_size.get_columns ()) None 12 | 13 | let suite = 14 | [ ( "Terminal_size" 15 | , [ ("enabled", `Quick, test_enabled) 16 | ; ("disabled", `Quick, test_disabled) 17 | ] 18 | ) 19 | ] 20 | 21 | let () = 22 | Alcotest.run "terminal_size" suite 23 | --------------------------------------------------------------------------------