├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── README.md ├── main.go ├── model └── sheet.go ├── screenshots └── screenshot-0.1.png ├── snapcraft.yaml └── view └── view.go /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /csv123 3 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:cdb899c199f907ac9fb50495ec71212c95cb5b0e0a8ee0800da0238036091033" 6 | name = "github.com/mattn/go-runewidth" 7 | packages = ["."] 8 | pruneopts = "UT" 9 | revision = "ce7b0b5c7b45a81508558cd1dba6bb1e4ddb51bb" 10 | version = "v0.0.3" 11 | 12 | [[projects]] 13 | branch = "master" 14 | digest = "1:01d9e47830ef6077fb6f91033b0e83f324ad5966d11ed3daa4a5822ace876dab" 15 | name = "github.com/nsf/termbox-go" 16 | packages = ["."] 17 | pruneopts = "UT" 18 | revision = "60ab7e3d12ed91bc1b2486559c4b3a6b62297577" 19 | 20 | [solve-meta] 21 | analyzer-name = "dep" 22 | analyzer-version = 1 23 | input-imports = ["github.com/nsf/termbox-go"] 24 | solver-name = "gps-cdcl" 25 | solver-version = 1 26 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | [[constraint]] 27 | name = "github.com/nsf/termbox-go" 28 | branch = "master" 29 | 30 | [prune] 31 | go-tests = true 32 | unused-packages = true 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | csv 1-2-3 2 | ========= 3 | 4 | This package is a command-line CSV reader in the style of MS-DOS Lotus 1-2-3. 5 | 6 | It's far from complete, but there's a screenshot: 7 | 8 | ![screenshot](screenshots/screenshot-0.1.png) 9 | 10 | Right now it's a very simple viewer. You can browse around with the arrow 11 | keys, and the interface re-renders if the terminal is resized. 12 | 13 | To exit, hit `ESC`. 14 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | import "fmt" 5 | import "github.com/evert/csv123/view" 6 | import "github.com/evert/csv123/model" 7 | import "github.com/nsf/termbox-go" 8 | 9 | func main() { 10 | 11 | if len(os.Args) < 2 { 12 | usage() 13 | } 14 | 15 | var sheet = model.ReadFromFile(os.Args[1]) 16 | 17 | view.Init(sheet) 18 | defer view.Close() 19 | 20 | view.Render() 21 | 22 | mainloop: 23 | for { 24 | 25 | switch ev := termbox.PollEvent(); ev.Type { 26 | case termbox.EventKey: 27 | switch ev.Key { 28 | case termbox.KeyEsc: 29 | break mainloop 30 | case termbox.KeyArrowLeft: 31 | view.Move(-1, 0) 32 | case termbox.KeyArrowRight: 33 | view.Move(1, 0) 34 | case termbox.KeyArrowUp: 35 | view.Move(0, -1) 36 | case termbox.KeyArrowDown: 37 | view.Move(0, +1) 38 | case termbox.KeyHome, termbox.KeyCtrlA: 39 | view.MoveHome() 40 | case termbox.KeyEnd, termbox.KeyCtrlE: 41 | view.MoveEnd() 42 | case termbox.KeyPgup: 43 | view.PageUp() 44 | case termbox.KeyPgdn: 45 | view.PageDown() 46 | } 47 | case termbox.EventResize: 48 | view.Render() 49 | } 50 | 51 | } 52 | 53 | } 54 | 55 | func usage() { 56 | 57 | fmt.Fprintf(os.Stderr, "usage: %s [inputfile]\n", os.Args[0]) 58 | os.Exit(2) 59 | 60 | } 61 | -------------------------------------------------------------------------------- /model/sheet.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "encoding/csv" 4 | import "os" 5 | import "io" 6 | import "bufio" 7 | 8 | type Sheet struct { 9 | data [][]string 10 | } 11 | 12 | func NewSheet() Sheet { 13 | 14 | s := Sheet{data: make([][]string, 0)} 15 | return s 16 | 17 | } 18 | 19 | func (s Sheet) GetValue(x, y int) string { 20 | 21 | if y < len(s.data) && x < len(s.data[y]) { 22 | return s.data[y][x] 23 | } else { 24 | return "" 25 | } 26 | 27 | } 28 | 29 | func (s Sheet) GetMaxX(y int) int { 30 | 31 | return len(s.data[y]) 32 | 33 | } 34 | 35 | func ReadFromFile(fileName string) Sheet { 36 | 37 | s := NewSheet() 38 | 39 | var reader io.Reader 40 | if os.Args[1] == "-" { 41 | reader = bufio.NewReader(os.Stdin) 42 | } else { 43 | r, err := os.Open(fileName) 44 | if err != nil { 45 | panic(err) 46 | } 47 | reader = r 48 | 49 | } 50 | 51 | csv_reader := csv.NewReader(reader) 52 | 53 | for { 54 | record, err := csv_reader.Read() 55 | if err == io.EOF { 56 | break 57 | } 58 | if err != nil { 59 | panic(err) 60 | } 61 | s.data = append(s.data, record) 62 | } 63 | return s 64 | 65 | } 66 | -------------------------------------------------------------------------------- /screenshots/screenshot-0.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evert/csv123/075ece6fe42d0abfc0d2daf66a6f3d31a3d065b5/screenshots/screenshot-0.1.png -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: csv123 2 | version: git 3 | summary: A command-line CSV viewer 4 | description: A simple command-line viewer for CSV files. 5 | confinement: devmode 6 | 7 | parts: 8 | csv123: 9 | plugin: go 10 | go-importpath: github.com/evert/csv123 11 | source: . 12 | source-type: git 13 | 14 | apps: 15 | csv123: 16 | command: bin/csv123 17 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "fmt" 4 | import "github.com/nsf/termbox-go" 5 | import "strconv" 6 | import "strings" 7 | import "github.com/evert/csv123/model" 8 | 9 | const columnWidth = 20 10 | const rowBarWidth = 6 11 | 12 | var activeCellX = 0 13 | var activeCellY = 0 14 | 15 | var sheetXOffset = 0 16 | var sheetYOffset = 0 17 | 18 | var sheet model.Sheet 19 | 20 | func Init(sheetData model.Sheet) { 21 | 22 | termbox.Init() 23 | sheet = sheetData 24 | 25 | } 26 | 27 | func Close() { 28 | 29 | termbox.Close() 30 | 31 | } 32 | 33 | func Render() { 34 | 35 | renderLogo() 36 | renderInput() 37 | renderRows() 38 | renderColumns() 39 | renderSheet() 40 | renderInput() 41 | termbox.Flush() 42 | 43 | } 44 | 45 | func renderLogo() { 46 | 47 | mx, _ := termbox.Size() 48 | offset := mx - 9 49 | setCells(offset, 0, "csv x-x-x", termbox.ColorDefault, termbox.ColorDefault) 50 | termbox.SetCell(offset+4, 0, '1', termbox.ColorRed|termbox.AttrBold, termbox.ColorDefault) 51 | termbox.SetCell(offset+6, 0, '2', termbox.ColorGreen|termbox.AttrBold, termbox.ColorDefault) 52 | termbox.SetCell(offset+8, 0, '3', termbox.ColorBlue|termbox.AttrBold, termbox.ColorDefault) 53 | 54 | } 55 | 56 | func renderInput() { 57 | 58 | mx, _ := termbox.Size() 59 | str := fmt.Sprintf("%v:%-"+strconv.Itoa(mx-14)+"v", 60 | cellCoords(), 61 | sheet.GetValue(activeCellX, activeCellY), 62 | ) 63 | setCells(0, 0, str, termbox.ColorDefault, termbox.ColorDefault) 64 | 65 | } 66 | 67 | func renderRows() { 68 | 69 | rows := maxRows() 70 | for y := 0; y < rows; y++ { 71 | 72 | str := fmt.Sprintf("%4v ", y+1+sheetYOffset) + " " 73 | setCells(1, y+2, str, termbox.ColorBlack, termbox.ColorCyan) 74 | 75 | } 76 | 77 | } 78 | func renderColumns() { 79 | 80 | columnWidth := 20 81 | 82 | columns := maxColumns() 83 | 84 | for x := 0; x < columns; x++ { 85 | 86 | str := center(charForColumn(x+sheetXOffset), columnWidth) 87 | setCells((x*columnWidth)+rowBarWidth, 1, str, termbox.ColorBlack, termbox.ColorCyan) 88 | 89 | } 90 | 91 | } 92 | 93 | func renderSheet() { 94 | 95 | columns := maxColumns() 96 | rows := maxRows() 97 | 98 | for y := 0; y < rows; y++ { 99 | 100 | for x := 0; x < columns; x++ { 101 | 102 | renderCell(x, y) 103 | 104 | } 105 | 106 | } 107 | 108 | } 109 | 110 | func renderCell(x, y int) { 111 | 112 | real_x := x + sheetXOffset 113 | real_y := y + sheetYOffset 114 | 115 | active := false 116 | if real_x == activeCellX && real_y == activeCellY { 117 | active = true 118 | } 119 | val := sheet.GetValue(real_x, real_y) 120 | 121 | renderSheetCell(x, y, val, active) 122 | 123 | } 124 | 125 | func renderSheetCell(x, y int, value string, active bool) { 126 | 127 | if len(value) > columnWidth-2 { 128 | value = value[0 : columnWidth-2] 129 | } 130 | formatted_string := "" 131 | if _, err := strconv.Atoi(value); err == nil { 132 | // Right-justify 133 | formatted_string = " " + strings.Repeat(" ", columnWidth-2-len(value)) + value + " " 134 | } else { 135 | // Left-justify 136 | formatted_string = " " + value + strings.Repeat(" ", columnWidth-2-len(value)) + " " 137 | } 138 | 139 | fg := termbox.ColorDefault 140 | bg := termbox.ColorDefault 141 | if active { 142 | fg = termbox.ColorBlack 143 | bg = termbox.ColorBlue 144 | } 145 | setCells( 146 | (x*columnWidth)+rowBarWidth, 147 | y+2, 148 | formatted_string, 149 | fg, 150 | bg, 151 | ) 152 | 153 | } 154 | 155 | func setCells(x, y int, str string, fg, bg termbox.Attribute) { 156 | 157 | for off := 0; off < len(str); off++ { 158 | 159 | termbox.SetCell(x+off, y, rune(str[off]), fg, bg) 160 | 161 | } 162 | 163 | } 164 | 165 | // Maximum number of columns based on terminal width 166 | func maxColumns() int { 167 | 168 | mx, _ := termbox.Size() 169 | 170 | // How many columns can we fit? 171 | return (mx-rowBarWidth)/columnWidth + 1 172 | 173 | } 174 | 175 | // Maximum number of rows based on terminal height 176 | func maxRows() int { 177 | 178 | _, my := termbox.Size() 179 | return my - 1 180 | 181 | } 182 | 183 | // Returns the active cell's coordinates as a string like A1 184 | func cellCoords() string { 185 | 186 | return fmt.Sprintf("%v%v", charForColumn(activeCellX), activeCellY+1) 187 | 188 | } 189 | 190 | func charForColumn(i int) string { 191 | 192 | return string('A' + i) 193 | 194 | } 195 | 196 | func center(s string, width int) string { 197 | 198 | pad := (width - len(s)) / 2 199 | extra := (width - len(s)) % 2 200 | return strings.Repeat(" ", pad+extra) + s + strings.Repeat(" ", pad) 201 | 202 | } 203 | 204 | // Move relatively in the sheet 205 | func Move(x, y int) { 206 | 207 | SetActiveCell(activeCellX+x, activeCellY+y) 208 | 209 | } 210 | 211 | // Move to start of line 212 | func MoveHome() { 213 | SetActiveCell(0, activeCellY) 214 | } 215 | 216 | // Move to end of line 217 | func MoveEnd() { 218 | SetActiveCell(sheet.GetMaxX(activeCellY), activeCellY) 219 | } 220 | 221 | func PageDown() { 222 | SetActiveCell(activeCellX, activeCellY+maxRows()-2) 223 | } 224 | 225 | func PageUp() { 226 | SetActiveCell(activeCellX, activeCellY-maxRows()+2) 227 | } 228 | 229 | func SetActiveCell(x, y int) { 230 | 231 | columns := maxColumns() 232 | rows := maxRows() 233 | if x < 0 { 234 | x = 0 235 | } 236 | if y < 0 { 237 | y = 0 238 | } 239 | 240 | prev_x := activeCellX 241 | prev_y := activeCellY 242 | activeCellX = x 243 | activeCellY = y 244 | 245 | full_render := false 246 | 247 | if x < sheetXOffset { 248 | sheetXOffset = x 249 | full_render = true 250 | } else if x > sheetXOffset+(columns-2) { 251 | sheetXOffset = x - columns + 2 252 | full_render = true 253 | } else if y < sheetYOffset { 254 | sheetYOffset = y 255 | full_render = true 256 | } else if y > sheetYOffset+(rows-2) { 257 | sheetYOffset = y - rows + 2 258 | full_render = true 259 | } 260 | 261 | renderInput() 262 | if full_render { 263 | Render() 264 | } else { 265 | renderCell(prev_x-sheetXOffset, prev_y-sheetYOffset) 266 | renderCell(x-sheetXOffset, y-sheetYOffset) 267 | termbox.Flush() 268 | } 269 | 270 | } 271 | --------------------------------------------------------------------------------