镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Keyboard driven pager of data.tables #2893

Open

Description

@jsams

I wrote this because R's built-in page is SLOW for large data.tables (also, apparently, page doesn't do what I thought it did). Combine that with the fact that I'm not a big user of the various R gui's (e.g. RStudio), but I do, occasionally, want to see my actual data, and I decided to create a little pager for data.tables. I've been using this hack, which is a less-like interface for data.tables for a while now. Unfortunately, it only scrolls up and down, not left and right, which works for me as my data is usually long and narrow. But it's definitely a shortcoming.

To get horizontal scrolling working, what should probably be done is only print the columns that fit in the width of the terminal with some indicator that there are more columns to the left/right, with maybe an option to "freeze" the key columns so they don't scroll off the page when moving right (though that assumes the key columns are already organized to be to the left of the data.table). This would prevent the extra columns from being printed below the others, which otherwise really screws with the utility of the pager.

In any case, I don't think this is clean enough to warrant a pull request (I suspect the way I'm getting the number of rows/columns for the terminal is linux specific), and I have no idea how to make test cases for it. But it's been sitting in my collection of scripts for a while and thought I should share it. If someone has some ideas on how to get the width of that data.table in at least the current range and horizontal scrolling working, that might make it more worthy of inclusion.

num_lines = function(default=24) {
    x = Sys.getenv("LINES")
    if (grepl('^[0-9]+$', x)) {
        return(as.integer(x))
    } else {
        x = system2('tput', 'lines', stdout=T)
        if (grepl('^[0-9]+$', x)) {
            return(as.integer(x))
        } else {
            return(default)
        }
    }
}

num_cols = function(default=80) {
    x = Sys.getenv("COLUMNS")
    if (grepl('^[0-9]+$', x)) {
        return(as.integer(x))
    } else {
        x = system2('tput', 'cols', stdout=T)
        if (grepl('^[0-9]+$', x)) {
            return(as.integer(x))
        } else {
            return(default)
        }
    }
}

#' A pager built for data.table
#'
#' There are 4 basic commands: j, k, G, g.
#' k/j: move up and down a page at a time
#' G/g: move to the bottom/top of the object
#' q: quit
#'
#' k/j can be modified with a number. by default, the pager will go down one
#' page. If you provide a number along with j/k it will move that number of
#' rows down. So "1j" would move just 1 row down instead of a full page.
#' "500j" would move down 500 rows. The counting starts from the top row.
#' Similary for up using 'k'. This will not wrap around/go beyond the beginning
#' or end of the object.
#' 
#' A simple enter implies the previous command, or just 'j' if no command has
#' yet been entered
pager = function(dt) {
    cur_width = options()$width
    options(width=num_cols())
    bufferlines = 3 # take into account header row and such
    termheight = num_lines() - bufferlines
    ndt = nrow(dt)
    if (ndt < termheight) {
        print(dt)
        return(NULL)
    }
    state = 1
    command = 'j'
    num = termheight
    while (TRUE) {
        # use data.frame so that the row names with the actual row numbers are printed
        dt2 = as.data.frame(dt[state:(state+termheight),])
        rownames(dt2) = state:(state+termheight)
        print(dt2)
        if (state + termheight >= ndt) {
            r = readline("End of Object. ")
        } else {
            r = readline(': ')
        }
        if (r == '') { # repeat previous command
            r = sprintf('%s%s', num, command)
        }
        termheight = num_lines() - bufferlines # in case terminal resized
        # set state for next print
        if (tolower(r) == 'q') {
            break
        } else if (r == 'g') {
            state = 1
            command = 'g'
            num = ''
        } else if (r == 'G') {
            state = ndt - termheight
            command = 'G'
            num = ''
        } else if (grepl('j', r, ignore.case=T)) {
            command = 'j'
            num = gsub('j', '', r, ignore.case=T)
            if (num == '') {
                num = termheight + 1
            } else {
                num = as.integer(num)
            }
            if (num + state + termheight >= ndt) {
                state = ndt - termheight
            } else {
                state = state + num
            }
        } else if (grepl('k', r, ignore.case=T)) {
            command = 'k'
            num = gsub('k', '', r, ignore.case=T)
            if (num == '') {
                num = termheight + 1
            } else {
                num = as.integer(num)
            }
            if (state - num < 1) {
                state = 1
            } else {
                state = state - num
            }
        } else {
            cat('illegal option.\n')
            break
        }
    }
    options(width=cur_width)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions