diff --git a/doc/specs/stdlib_system.md b/doc/specs/stdlib_system.md index 96eebb2e8..04a98397d 100644 --- a/doc/specs/stdlib_system.md +++ b/doc/specs/stdlib_system.md @@ -532,3 +532,54 @@ The file is removed from the filesystem if the operation is successful. If the o ```fortran {!example/system/example_delete_file.f90!} ``` + +## `get_terminal_size` - Get terminal window size in characters + +### Status + +Experimental + +### Description + +Queries the terminal window size in characters (columns × lines). + +This routine performs the following checks: +1. Verifies stdout is connected to a terminal (not redirected); +2. Queries terminal dimensions via platform-specific APIs. + +Typical execution time: <100μs on modern systems. + +### Syntax + +`call [[stdlib_system(module):get_terminal_size(subroutine)]](columns, lines[, err])` + +### Class + +Subroutine + +### Arguments + +`columns`: `integer, intent(out)`. + Number of columns in the terminal window. Set to `-1` on error. + +`lines`: `integer, intent(out)`. + Number of lines in the terminal window. Set to `-1` on error. + +`err`: `type(state_type), intent(out), optional`. + Error state object. If absent, errors terminate execution. + +### Error Handling + +- **Success**: `columns` and `lines` contain valid dimensions. +- **Failure**: + - Both arguments set to `-1`. + - If `err` present, `stat` contains error code: + - Unix: Contains `errno` (typically `ENOTTY` when redirected); + - Windows: Contains `GetLastError()` code. + - If `err` absent: Program stops with error message. + +### Example + +```fortran +{!./example/system/example_get_terminal_size.f90}! +``` diff --git a/example/system/CMakeLists.txt b/example/system/CMakeLists.txt index a2a7525c9..2cb6a7da5 100644 --- a/example/system/CMakeLists.txt +++ b/example/system/CMakeLists.txt @@ -1,4 +1,5 @@ ADD_EXAMPLE(get_runtime_os) +ADD_EXAMPLE(get_terminal_size) ADD_EXAMPLE(delete_file) ADD_EXAMPLE(is_directory) ADD_EXAMPLE(null_device) diff --git a/example/system/example_get_terminal_size.f90 b/example/system/example_get_terminal_size.f90 new file mode 100644 index 000000000..3e331ab4f --- /dev/null +++ b/example/system/example_get_terminal_size.f90 @@ -0,0 +1,16 @@ +program example_get_terminal_size + + use stdlib_system, only: get_terminal_size + use stdlib_error, only: state_type + implicit none + + integer :: columns, lines + type(state_type) :: err + + !> Get terminal size + call get_terminal_size(columns, lines, err) + + print "(2(a,i0))", "Terminal size is ", columns, 'x', lines + if (err%ok()) print "(a)", repeat("*", columns) + +end program example_get_terminal_size diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d82aae118..37bb195c5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -112,6 +112,7 @@ set(SRC stdlib_hashmap_open.f90 stdlib_logger.f90 stdlib_sorting_radix_sort.f90 + stdlib_system_get_terminal_size.c stdlib_system_subprocess.c stdlib_system_subprocess.F90 stdlib_system.F90 diff --git a/src/stdlib_system.F90 b/src/stdlib_system.F90 index a9c3e4d55..48ab925fc 100644 --- a/src/stdlib_system.F90 +++ b/src/stdlib_system.F90 @@ -83,7 +83,9 @@ module stdlib_system public :: kill public :: elapsed public :: is_windows - + +public :: get_terminal_size + !! version: experimental !! !! Tests if a given path matches an existing directory. @@ -547,11 +549,39 @@ module function process_get_ID(process) result(ID) !> Return a process ID integer(process_ID) :: ID end function process_get_ID - -end interface + +end interface contains +!! Returns terminal window size in characters. +!! +!! ### Returns: +!! - **columns**: The number of columns in the terminal window. +!! - **lines**: The number of lines in the terminal window. +!! - **err**: An optional error object. +!! +!! Note: This function performs a detailed runtime inspection, so it has non-negligible overhead. +subroutine get_terminal_size(columns, lines, err) + integer, intent(out) :: columns, lines + type(state_type), intent(out), optional :: err + type(state_type) :: err0 + integer :: stat + interface + subroutine c_get_terminal_size(columns, lines, stat) bind(C, name="get_terminal_size") + integer, intent(out) :: columns, lines, stat + end subroutine c_get_terminal_size + end interface + + call c_get_terminal_size(columns, lines, stat) + if (stat /= 0) then + err0 = state_type('get_terminal_size',STDLIB_FS_ERROR,'Failed to get terminal size,','stat =',stat) + call err0%handle(err) + end if + +end subroutine get_terminal_size + + integer function get_runtime_os() result(os) !! The function identifies the OS by inspecting environment variables and filesystem attributes. !! diff --git a/src/stdlib_system_get_terminal_size.c b/src/stdlib_system_get_terminal_size.c new file mode 100644 index 000000000..4cd077061 --- /dev/null +++ b/src/stdlib_system_get_terminal_size.c @@ -0,0 +1,63 @@ +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + + +// Get terminal size +// @param[out] columns Pointer to store terminal width +// @param[out] lines Pointer to store terminal height +// @param[out] stat Pointer to store error code +// (0: Success, otherwise: Error, Windows: GetLastError(), Unix: ENOTTY/errno) +void get_terminal_size(int *columns, int *lines, int *stat) +{ + /* Initialize outputs to error state */ + *columns = -1; + *lines = -1; + *stat = 0; + +#ifdef _WIN32 + /* Windows implementation using Console API */ + HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE); + if (hConsole == INVALID_HANDLE_VALUE) + { + *stat = (int)GetLastError(); // Return Windows system error code + return; + } + + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (!GetConsoleScreenBufferInfo(hConsole, &csbi)) + { + *stat = (int)GetLastError(); // Failed to get console info + return; + } + + /* Calculate visible window dimensions */ + *columns = csbi.srWindow.Right - csbi.srWindow.Left + 1; + *lines = csbi.srWindow.Bottom - csbi.srWindow.Top + 1; + +#else + /* Unix implementation using termios ioctl */ + if (!isatty(STDOUT_FILENO)) + { + *stat = ENOTTY; + return; + } + + struct winsize w; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) + { + *stat = errno; // Return POSIX system error code + return; + } + + /* Directly use reported terminal dimensions */ + *columns = w.ws_col; + *lines = w.ws_row; +#endif +} \ No newline at end of file diff --git a/test/system/test_os.f90 b/test/system/test_os.f90 index 1607b7ec1..8d9513774 100644 --- a/test/system/test_os.f90 +++ b/test/system/test_os.f90 @@ -1,7 +1,8 @@ module test_os use testdrive, only : new_unittest, unittest_type, error_type, check, skip_test - use stdlib_system, only: get_runtime_os, OS_WINDOWS, OS_UNKNOWN, OS_TYPE, is_windows, null_device - + use stdlib_system, only: get_runtime_os, OS_WINDOWS, OS_UNKNOWN, OS_TYPE, is_windows, null_device, & + get_terminal_size + use stdlib_error, only: state_type implicit none contains @@ -12,12 +13,38 @@ subroutine collect_suite(testsuite) type(unittest_type), allocatable, intent(out) :: testsuite(:) testsuite = [ & + new_unittest('test_get_terminal_size', test_get_terminal_size), & new_unittest('test_get_runtime_os', test_get_runtime_os), & new_unittest('test_is_windows', test_is_windows), & new_unittest('test_null_device', test_null_device) & ] end subroutine collect_suite + subroutine test_get_terminal_size(error) + type(error_type), allocatable, intent(out) :: error + integer :: columns, lines + type(state_type) :: err + + !> Get terminal size + call get_terminal_size(columns, lines, err) + + if (err%ok()) then + call check(error, columns > 0, "Terminal width is not positive") + if (allocated(error)) return + + call check(error, lines > 0, "Terminal height is not positive") + + !> In Github Actions, the terminal size is not available, standard output is redirected to a file + else + call check(error, columns, -1) + if (allocated(error)) return + + call check(error, lines, -1) + + end if + + end subroutine test_get_terminal_size + subroutine test_get_runtime_os(error) type(error_type), allocatable, intent(out) :: error integer :: os