Repository navigation
Keyboard driven pager of data.tables #2893
Description
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)
}
Activity