41

How to get tty size with Golang? I am trying do this with executing stty size command, but I can't craft code right.

package main

import (
  "os/exec"
  "fmt"
  "log"
)

func main() {
  out, err := exec.Command("stty", "size").Output()
  fmt.Printf("out: %#v\n", out)
  fmt.Printf("err: %#v\n", err)
  if err != nil {
    log.Fatal(err)
  }
}

Output:

out: []byte{}
err: &exec.ExitError{ProcessState:(*os.ProcessState)(0xc200066520)}
2013/05/16 02:35:57 exit status 1
exit status 1

I think this is because Go spawns a process not related to the current tty, with which it is working. How can I relate the command to current terminal in order to get its size?

wasmup
  • 14,541
  • 6
  • 42
  • 58
Ilia Sidorenko
  • 2,157
  • 3
  • 26
  • 30

7 Answers7

37

I just wanted to add a new answer since I ran into this problem recently. There is a terminal package which lives inside the official ssh package https://godoc.org/golang.org/x/crypto/ssh/terminal.

This package provides a method to easily get the size of a terminal.

width, height, err := terminal.GetSize(0)

0 would be the file descriptor of the terminal you want the size of. To get the fd or you current terminal you can always do int(os.Stdin.Fd())

Under the covers it uses a syscall to get the terminal size for the given fd.

pech0rin
  • 4,588
  • 3
  • 18
  • 22
  • 2
    If `os.Stdin` is not attached to the terminal, like when reading from a pipe, you can use `int(os.Stdout.Fd())` to find the dimensions instead. – mndrix May 24 '18 at 17:19
  • It didn't worked for me on Windows, unfortunately. No matter what I do it sends me error `The handle is invalid.` – shytikov Nov 08 '20 at 15:51
  • To me, ```int(os.Stdin.Fd())``` not working but ```int(os.Stdout.Fd())``` working properly. - Working on WSL2, Ubuntu 20.04, VSCode, with command ```$ go test -run TestTerminalSize``` – TyeolRik Jun 30 '21 at 16:02
28

I was stuck on a similar problem. Here is what I ended up with.

It doesn't use a subprocess, so might be desirable in some situations.

import (
    "syscall"
    "unsafe"
)

type winsize struct {
    Row    uint16
    Col    uint16
    Xpixel uint16
    Ypixel uint16
}

func getWidth() uint {
    ws := &winsize{}
    retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL,
        uintptr(syscall.Stdin),
        uintptr(syscall.TIOCGWINSZ),
        uintptr(unsafe.Pointer(ws)))

    if int(retCode) == -1 {
        panic(errno)
    }
    return uint(ws.Col)
}
Chipaca
  • 387
  • 3
  • 11
deft_code
  • 57,255
  • 29
  • 141
  • 224
  • 1
    Another alternative without using "unsafe" package: https://github.com/golang/crypto/blob/e4dc69e5b2fd71dcaf8bd5d054eb936deb78d1fa/ssh/terminal/util.go#L80 – Cenk Alti Nov 08 '18 at 07:58
  • Unfortunately that alternative still ends up using unsafe under the hood. https://github.com/golang/sys/blob/572b51eaf7221935bdec454796989ba8318fa6f3/unix/syscall_linux.go#L112 – Strom Mar 31 '19 at 09:42
  • 1
    @Strom: the way I see it, it's a safe wrapper around an unsafe function. Kinda like with Rust :) – grooveplex Apr 25 '19 at 23:10
  • This works for me, I was using this version: `unix.IoctlGetWinsize(syscall.Stdout, unix.TIOCGWINSZ)` – elulcao Mar 23 '22 at 23:33
25

You can use golang.org/x/term package (https://pkg.go.dev/golang.org/x/term)

Example

package main

import "golang.org/x/term"

func main() {
    if term.IsTerminal(0) {
        println("in a term")
    } else {
        println("not in a term")
    }
    width, height, err := term.GetSize(0)
    if err != nil {
        return
    }
    println("width:", width, "height:", height)
}

Output

in a term
width: 228 height: 27
c.ga
  • 321
  • 3
  • 6
24

It works if you give the child process access to the parent's stdin:

package main

import (
  "os/exec"
  "fmt"
  "log"
  "os"
)

func main() {
  cmd := exec.Command("stty", "size")
  cmd.Stdin = os.Stdin
  out, err := cmd.Output()
  fmt.Printf("out: %#v\n", string(out))
  fmt.Printf("err: %#v\n", err)
  if err != nil {
    log.Fatal(err)
  }
}

Yields:

out: "36 118\n"
err: <nil>
lnmx
  • 10,846
  • 3
  • 40
  • 36
  • You can try to put these lines into a function, and set a unit test by `testing` module to test the function, you will find that you always get empty array. – Kingname Jan 01 '18 at 14:20
  • I have tried it but I keep getting an empty array as @Kingname stated when testing. – Nik Jan 26 '22 at 16:24
  • @Kingname Is that not because go test turns into a separate process with a separate stdin ? – Alex SHP Aug 02 '22 at 18:51
9

Since no one else here has yet to present a cross-platform solution that will work on both Windows and Unix, I went ahead and put together a library that supports both.

https://github.com/nathan-fiscaletti/consolesize-go

package main

import (
    "fmt"

    "github.com/nathan-fiscaletti/consolesize-go"
)

func main() {
    cols, rows := consolesize.GetConsoleSize()
    fmt.Printf("Rows: %v, Cols: %v\n", rows, cols)
}
Nathan F.
  • 3,250
  • 3
  • 35
  • 69
  • I wanted to confirm that it actually works on Windows. I have tried different scenarios, including exotic ones, like open vim and split window to get different sized terminal (inside another terminal, that runs vim). It works. I did get incorrect sized several times, but not ready to report that, since I'm not sure was it library fault or my sloppy testing. – shytikov Nov 08 '20 at 16:07
  • @shytikov if the solution works for you, consider up-voting the answer :) – Nathan F. Nov 08 '20 at 20:45
  • 1
    Yes, absolutely! – shytikov Nov 08 '20 at 22:39
5

If anyone's interested I made a package to make this easier.

https://github.com/wayneashleyberry/terminal-dimensions

package main

import (
    "fmt"

    terminal "github.com/wayneashleyberry/terminal-dimensions"
)

func main() {
    x, _ := terminal.Width()
    y, _ := terminal.Height()
    fmt.Printf("Terminal is %d wide and %d high", x, y)
}
  • 2
    No windows support, this renders the package useless for cross-platform development. – shytikov Nov 08 '20 at 15:14
  • @shytikov take a look at my answer here which does have windows support: https://stackoverflow.com/a/63401154/1720829 – Nathan F. Apr 27 '22 at 04:43
1

I have one implementation that uses tcell module, under the hood it will still use approach that based on calling native dlls, but if you're searching for terminal dimensions there is a great chance that you would need that package anyway:

package main

import (
    "fmt"
    "github.com/gdamore/tcell"
)

func main() {
    screen, _ := tcell.NewScreen()
    screen.Init()
    
    w, h := screen.Size()
    fmt.Println(w, h)
}
shytikov
  • 9,155
  • 8
  • 56
  • 103