Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get Terminal width Haskell

How to get the width of the terminal in Haskell?

Things I tried

System.Posix.IOCtl (could not figure out how to get it to work) 

This only has to work unix.

Thanks

like image 943
Bilal Syed Hussain Avatar asked Oct 09 '12 18:10

Bilal Syed Hussain


2 Answers

If you don't want a dependency on ncurses, here's a wrapper of the appropriate ioctl() request using the FFI, based on the accepted answer of Getting terminal width in C?

TermSize.hsc

{-# LANGUAGE ForeignFunctionInterface #-}

module TermSize (getTermSize) where

import Foreign
import Foreign.C.Error
import Foreign.C.Types

#include <sys/ioctl.h>
#include <unistd.h>

-- Trick for calculating alignment of a type, taken from
-- http://www.haskell.org/haskellwiki/FFICookBook#Working_with_structs
#let alignment t = "%lu", (unsigned long)offsetof(struct {char x__; t (y__); }, y__)

-- The ws_xpixel and ws_ypixel fields are unused, so I've omitted them here.
data WinSize = WinSize { wsRow, wsCol :: CUShort }

instance Storable WinSize where
  sizeOf _ = (#size struct winsize)
  alignment _ = (#alignment struct winsize) 
  peek ptr = do
    row <- (#peek struct winsize, ws_row) ptr
    col <- (#peek struct winsize, ws_col) ptr
    return $ WinSize row col
  poke ptr (WinSize row col) = do
    (#poke struct winsize, ws_row) ptr row
    (#poke struct winsize, ws_col) ptr col

foreign import ccall "sys/ioctl.h ioctl"
  ioctl :: CInt -> CInt -> Ptr WinSize -> IO CInt

-- | Return current number of (rows, columns) of the terminal.
getTermSize :: IO (Int, Int)
getTermSize = 
  with (WinSize 0 0) $ \ws -> do
    throwErrnoIfMinus1 "ioctl" $
      ioctl (#const STDOUT_FILENO) (#const TIOCGWINSZ) ws
    WinSize row col <- peek ws
    return (fromIntegral row, fromIntegral col)

This uses the hsc2hs preprocessor to figure out the correct constants and offsets based on the C headers rather than hardcoding them. I think it's packaged with either GHC or the Haskell Platform, so chances are you'll have it already.

If you're using Cabal, you can add TermSize.hs to your .cabal file and it'll automatically know how to generate it from TermSize.hsc. Otherwise, you can run hsc2hs TermSize.hsc manually to generate a .hs file which you can then compile with GHC.

like image 137
hammar Avatar answered Sep 23 '22 12:09

hammar


You could use hcurses. Once you've initialized the library, you can use scrSize to get the number of rows and columns on the screen.

To use System.Posix.IOCtl, you have to define a data type to represent the TIOCGWINSZ request, which fills in the following structure:

struct winsize {
    unsigned short ws_row;
    unsigned short ws_col;
    unsigned short ws_xpixel;   /* unused */
    unsigned short ws_ypixel;   /* unused */
};

You'll need to define a Haskell data type to hold this information, and make it an instance of Storable:

{-# LANGUAGE RecordWildCards #-}
import Foreign.Storable
import Foreign.Ptr
import Foreign.C

data Winsize = Winsize { ws_row    :: CUShort
                       , ws_col    :: CUShort
                       , ws_xpixel :: CUShort
                       , ws_ypixel :: CUShort
                       }

instance Storable Winsize where
  sizeOf _ = 8
  alignment _ = 2
  peek p = do { ws_row    <- peekByteOff p 0
              ; ws_col    <- peekByteOff p 2
              ; ws_xpixel <- peekByteOff p 4
              ; ws_ypixel <- peekByteOff p 6
              ; return $ Winsize {..}
              }
  poke p Winsize {..} = do { pokeByteOff p 0 ws_row
                           ; pokeByteOff p 2 ws_col
                           ; pokeByteOff p 4 ws_xpixel
                           ; pokeByteOff p 6 ws_ypixel
                           }

Now, you need to create a dummy data type to represent your request:

data TIOCGWINSZ = TIOCGWINSZ

Finally, you need to make your request type an instance of IOControl, and associate it with the Winsize data type.

instance IOControl TIOCGWINSZ Winsize where
  ioctlReq _ = ??

You will need to replace the ?? with the constant represented by TIOCGWINSZ in your header files (0x5413 on my system).

Now, you are ready to issue the ioctl. This command does not care about the input data, so you want to use the ioctl' form:

main = do { ws <- ioctl' 1 TIOCGWINSZ
          ; putStrLn $ "My terminal is " ++ show (ws_col ws) ++ " columns wide"
          }

Note that the 1 refers to STDOUT.

Phew!

like image 30
pat Avatar answered Sep 24 '22 12:09

pat