Skip to content

stdlib_system: add get_terminal_size subroutine #983

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions doc/specs/stdlib_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}!
```
1 change: 1 addition & 0 deletions example/system/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
16 changes: 16 additions & 0 deletions example/system/example_get_terminal_size.f90
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 33 additions & 3 deletions src/stdlib_system.F90
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
!!
Expand Down
63 changes: 63 additions & 0 deletions src/stdlib_system_get_terminal_size.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#include <stdlib.h>

#ifdef _WIN32
#include <windows.h>
#else
#include <sys/ioctl.h>
#include <unistd.h>
#include <errno.h>
#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
}
31 changes: 29 additions & 2 deletions test/system/test_os.f90
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Loading