diff --git a/README.md b/README.md index b48a855..23a0495 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ### Features include: -- **Response formatting**: automatically formats and colors output (json, html, msgpack, protobuf, xml, etc.) +- **Response formatting**: automatically formats and colors output (json, html, csv, msgpack, protobuf, xml, etc.) - **Image rendering**: render images directly in your terminal - **Compression**: automatic gzip and zstd response body decompression - **Authentication**: support for Basic Auth, Bearer Token, and AWS Signature V4 diff --git a/docs/USAGE.md b/docs/USAGE.md index 9dd195a..39b8d32 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -336,11 +336,14 @@ Supported formats for automatic formatting and syntax highlighting: - JSON (`application/json`) - HTML (`text/html`) - XML (`application/xml`, `text/xml`) +- CSV (`text/csv`) - MessagePack (`application/msgpack`) - NDJSON/JSONLines (`application/x-ndjson`) - Protobuf (`application/x-protobuf`, `application/protobuf`) - Server-Sent Events (`text/event-stream`) +CSV output is automatically column-aligned. When output is too wide for the terminal, it switches to a vertical "record view" format where each row is displayed with field names as labels. + ```sh fetch --format off example.com fetch --format on example.com diff --git a/go.mod b/go.mod index 10ec87f..c001a35 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.6 require ( github.com/klauspost/compress v1.18.3 + github.com/mattn/go-runewidth v0.0.19 github.com/quic-go/quic-go v0.59.0 github.com/tinylib/msgp v1.6.3 golang.org/x/image v0.35.0 @@ -13,6 +14,7 @@ require ( ) require ( + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect golang.org/x/crypto v0.47.0 // indirect diff --git a/go.sum b/go.sum index 8589f61..fed436c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/internal/core/term_size_unix.go b/internal/core/term_size_unix.go new file mode 100644 index 0000000..fe55d95 --- /dev/null +++ b/internal/core/term_size_unix.go @@ -0,0 +1,33 @@ +//go:build unix + +package core + +import ( + "os" + + "golang.org/x/sys/unix" +) + +// GetTerminalSize returns the terminal size, or an error if unavailable. +func GetTerminalSize() (TerminalSize, error) { + var ts TerminalSize + ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) + if err != nil { + return ts, err + } + + ts.Cols = int(ws.Col) + ts.Rows = int(ws.Row) + ts.WidthPx = int(ws.Xpixel) + ts.HeightPx = int(ws.Ypixel) + return ts, nil +} + +// GetTerminalCols returns the number of columns in the terminal, or 0 if unavailable. +func GetTerminalCols() int { + ts, err := GetTerminalSize() + if err != nil { + return 0 + } + return ts.Cols +} diff --git a/internal/core/term_size_windows.go b/internal/core/term_size_windows.go new file mode 100644 index 0000000..a8444dd --- /dev/null +++ b/internal/core/term_size_windows.go @@ -0,0 +1,37 @@ +//go:build windows + +package core + +import ( + "os" + + "golang.org/x/sys/windows" +) + +// GetTerminalSize returns the terminal size, or an error if unavailable. +func GetTerminalSize() (TerminalSize, error) { + var ts TerminalSize + + var info windows.ConsoleScreenBufferInfo + handle := windows.Handle(int(os.Stdout.Fd())) + err := windows.GetConsoleScreenBufferInfo(handle, &info) + if err != nil { + return ts, err + } + + ts.Cols = int(info.Window.Right - info.Window.Left + 1) + ts.Rows = int(info.Window.Bottom - info.Window.Top + 1) + // Windows console doesn't provide pixel dimensions + ts.WidthPx = 0 + ts.HeightPx = 0 + return ts, nil +} + +// GetTerminalCols returns the number of columns in the terminal, or 0 if unavailable. +func GetTerminalCols() int { + ts, err := GetTerminalSize() + if err != nil { + return 0 + } + return ts.Cols +} diff --git a/internal/core/vars.go b/internal/core/vars.go index 66e2188..8946a9d 100644 --- a/internal/core/vars.go +++ b/internal/core/vars.go @@ -6,6 +6,14 @@ import ( "runtime/debug" ) +// TerminalSize represents the dimensions of the terminal. +type TerminalSize struct { + Cols int // Number of columns (characters) + Rows int // Number of rows (characters) + WidthPx int // Width in pixels (0 if unavailable) + HeightPx int // Height in pixels (0 if unavailable) +} + var ( IsStderrTerm bool IsStdoutTerm bool diff --git a/internal/fetch/fetch.go b/internal/fetch/fetch.go index 29d3161..61fca96 100644 --- a/internal/fetch/fetch.go +++ b/internal/fetch/fetch.go @@ -28,6 +28,7 @@ type ContentType int const ( TypeUnknown ContentType = iota + TypeCSV TypeHTML TypeImage TypeJSON @@ -285,6 +286,10 @@ func formatResponse(ctx context.Context, r *Request, resp *http.Response) (io.Re } switch contentType { + case TypeCSV: + if format.FormatCSV(buf, p) == nil { + buf = p.Bytes() + } case TypeHTML: if format.FormatHTML(buf, p) == nil { buf = p.Bytes() @@ -328,6 +333,8 @@ func getContentType(headers http.Header) ContentType { return TypeImage case "application": switch subtype { + case "csv": + return TypeCSV case "json": return TypeJSON case "msgpack", "x-msgpack", "vnd.msgpack": @@ -350,6 +357,8 @@ func getContentType(headers http.Header) ContentType { } case "text": switch subtype { + case "csv": + return TypeCSV case "html": return TypeHTML case "event-stream": diff --git a/internal/format/csv.go b/internal/format/csv.go new file mode 100644 index 0000000..56e619c --- /dev/null +++ b/internal/format/csv.go @@ -0,0 +1,209 @@ +package format + +import ( + "bytes" + "encoding/csv" + "fmt" + "strings" + + "github.com/ryanfowler/fetch/internal/core" + + "github.com/mattn/go-runewidth" +) + +// FormatCSV formats the provided CSV data to the Printer. +func FormatCSV(buf []byte, p *core.Printer) error { + err := formatCSV(buf, p) + if err != nil { + p.Reset() + } + return err +} + +func formatCSV(buf []byte, p *core.Printer) error { + if len(buf) == 0 { + return nil + } + + delimiter := detectDelimiter(buf) + + reader := csv.NewReader(bytes.NewReader(buf)) + reader.Comma = delimiter + reader.FieldsPerRecord = -1 // Allow ragged rows + reader.LazyQuotes = true // Lenient parsing + + records, err := reader.ReadAll() + if err != nil { + return err + } + if len(records) == 0 { + return nil + } + + // Calculate column widths + colWidths := calculateColumnWidths(records) + totalWidth := calculateTotalWidth(colWidths) + termCols := core.GetTerminalCols() + + // Use vertical mode if terminal width is known and content exceeds it + if termCols > 0 && totalWidth > termCols && len(records) > 1 { + return writeVertical(p, records) + } + + // Output formatted rows (horizontal mode) + for i, row := range records { + if i > 0 { + p.WriteString("\n") + } + writeRow(p, row, colWidths, i == 0) + } + p.WriteString("\n") + + return nil +} + +// detectDelimiter auto-detects the delimiter from the first line. +// Checks comma, tab, semicolon, and pipe. Defaults to comma. +func detectDelimiter(buf []byte) rune { + // Find the first line + firstLine, _, _ := bytes.Cut(buf, []byte{'\n'}) + + delimiters := []rune{',', '\t', ';', '|'} + counts := make(map[rune]int) + + for _, d := range delimiters { + counts[d] = strings.Count(string(firstLine), string(d)) + } + + // Pick the delimiter with the highest count + maxCount := 0 + bestDelimiter := ',' + for _, d := range delimiters { + if counts[d] > maxCount { + maxCount = counts[d] + bestDelimiter = d + } + } + + return bestDelimiter +} + +// calculateColumnWidths finds the max display width per column across all rows. +func calculateColumnWidths(records [][]string) []int { + if len(records) == 0 { + return nil + } + + // Find max number of columns + maxCols := 0 + for _, row := range records { + if len(row) > maxCols { + maxCols = len(row) + } + } + + widths := make([]int, maxCols) + for _, row := range records { + for j, cell := range row { + w := runewidth.StringWidth(cell) + if w > widths[j] { + widths[j] = w + } + } + } + + return widths +} + +// calculateTotalWidth returns the total display width of horizontal output. +func calculateTotalWidth(colWidths []int) int { + total := 0 + for _, w := range colWidths { + total += w + } + if len(colWidths) > 1 { + total += (len(colWidths) - 1) * 2 // separator width (" ") + } + return total +} + +// writeRow writes a single row with proper alignment and coloring. +func writeRow(p *core.Printer, row []string, colWidths []int, isHeader bool) { + for j, cell := range row { + if j > 0 { + p.WriteString(" ") // Column separator + } + + // Apply color + if isHeader { + p.Set(core.Blue) + p.Set(core.Bold) + } else { + p.Set(core.Green) + } + + p.WriteString(cell) + p.Reset() + + // Add padding for alignment (except for the last column) + if j < len(colWidths)-1 { + cellWidth := runewidth.StringWidth(cell) + padding := colWidths[j] - cellWidth + for range padding { + p.WriteString(" ") + } + } + } +} + +// writeVertical renders each data row as a vertical record with field labels. +func writeVertical(p *core.Printer, records [][]string) error { + headers := records[0] + + // Calculate max header display width for right-alignment + maxHeaderWidth := 0 + for _, h := range headers { + if w := runewidth.StringWidth(h); w > maxHeaderWidth { + maxHeaderWidth = w + } + } + + for i, row := range records[1:] { // Skip header row + if i > 0 { + p.WriteString("\n") + } + + // Row separator + p.Set(core.Dim) + fmt.Fprintf(p, "--- Row %d ---\n", i+1) + p.Reset() + + for j, cell := range row { + header := "" + if j < len(headers) { + header = headers[j] + } + + // Right-align header using display width + padding := maxHeaderWidth - runewidth.StringWidth(header) + for range padding { + p.WriteString(" ") + } + + // Header in blue+bold + p.Set(core.Blue) + p.Set(core.Bold) + p.WriteString(header) + p.Reset() + p.WriteString(": ") + + // Value in green + p.Set(core.Green) + p.WriteString(cell) + p.Reset() + p.WriteString("\n") + } + } + + return nil +} diff --git a/internal/format/csv_test.go b/internal/format/csv_test.go new file mode 100644 index 0000000..45e7c35 --- /dev/null +++ b/internal/format/csv_test.go @@ -0,0 +1,392 @@ +package format + +import ( + "strings" + "testing" + + "github.com/ryanfowler/fetch/internal/core" +) + +func TestFormatCSV(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "basic csv", + input: "name,age,city\nAlice,30,NYC\nBob,25,LA", + wantErr: false, + }, + { + name: "tab separated", + input: "name\tage\tcity\nAlice\t30\tNYC\nBob\t25\tLA", + wantErr: false, + }, + { + name: "semicolon separated", + input: "name;age;city\nAlice;30;NYC\nBob;25;LA", + wantErr: false, + }, + { + name: "pipe separated", + input: "name|age|city\nAlice|30|NYC\nBob|25|LA", + wantErr: false, + }, + { + name: "quoted fields with commas", + input: `name,location,notes` + "\n" + `Alice,"New York, NY","Has a cat, dog"` + "\n" + `Bob,LA,None`, + wantErr: false, + }, + { + name: "embedded newlines in quotes", + input: "name,bio\nAlice,\"Line1\nLine2\"\nBob,Simple", + wantErr: false, + }, + { + name: "empty input", + input: "", + wantErr: false, + }, + { + name: "ragged rows", + input: "a,b,c\n1,2\n3,4,5,6", + wantErr: false, + }, + { + name: "single column", + input: "name\nAlice\nBob", + wantErr: false, + }, + { + name: "single row", + input: "name,age,city", + wantErr: false, + }, + { + name: "unicode content", + input: "名前,年齢\n太郎,25\n花子,30", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := core.NewHandle(core.ColorOff).Stderr() + err := FormatCSV([]byte(tt.input), p) + if (err != nil) != tt.wantErr { + t.Errorf("FormatCSV() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFormatCSVOutput(t *testing.T) { + input := "name,age,city\nAlice,30,NYC\nBob,25,LA" + p := core.NewHandle(core.ColorOff).Stderr() + err := FormatCSV([]byte(input), p) + if err != nil { + t.Fatalf("FormatCSV() error = %v", err) + } + + output := string(p.Bytes()) + // Check that all values are present + for _, want := range []string{"name", "age", "city", "Alice", "30", "NYC", "Bob", "25", "LA"} { + if !strings.Contains(output, want) { + t.Errorf("output should contain %q, got: %s", want, output) + } + } + // Check for newlines (rows separated) + if strings.Count(output, "\n") < 3 { + t.Errorf("output should have at least 3 newlines, got: %s", output) + } +} + +func TestFormatCSVAlignment(t *testing.T) { + // Test that columns are aligned properly + input := "a,bb,ccc\n111,22,3" + p := core.NewHandle(core.ColorOff).Stderr() + err := FormatCSV([]byte(input), p) + if err != nil { + t.Fatalf("FormatCSV() error = %v", err) + } + + output := string(p.Bytes()) + lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") + if len(lines) != 2 { + t.Fatalf("expected 2 lines, got %d", len(lines)) + } + + // The header "a" should be padded to width 3 (same as "111") + // The header "bb" should be padded to width 2 (same as "22") + // Check that "a" is followed by spaces for padding + if !strings.HasPrefix(lines[0], "a ") { + t.Errorf("expected 'a' to be padded, got: %q", lines[0]) + } +} + +func TestDetectDelimiter(t *testing.T) { + tests := []struct { + name string + input string + want rune + }{ + { + name: "comma", + input: "a,b,c", + want: ',', + }, + { + name: "tab", + input: "a\tb\tc", + want: '\t', + }, + { + name: "semicolon", + input: "a;b;c", + want: ';', + }, + { + name: "pipe", + input: "a|b|c", + want: '|', + }, + { + name: "empty defaults to comma", + input: "", + want: ',', + }, + { + name: "no delimiters defaults to comma", + input: "abc", + want: ',', + }, + { + name: "mixed prefers most common", + input: "a,b,c;d", + want: ',', + }, + { + name: "multiline uses first line", + input: "a;b;c\na,b,c,d,e", + want: ';', + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := detectDelimiter([]byte(tt.input)) + if got != tt.want { + t.Errorf("detectDelimiter() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestFormatCSVEmpty(t *testing.T) { + p := core.NewHandle(core.ColorOff).Stderr() + err := FormatCSV([]byte(""), p) + if err != nil { + t.Fatalf("FormatCSV() error = %v", err) + } + if len(p.Bytes()) != 0 { + t.Errorf("expected empty output for empty input, got: %q", string(p.Bytes())) + } +} + +func TestCalculateTotalWidth(t *testing.T) { + tests := []struct { + name string + colWidths []int + want int + }{ + { + name: "single column", + colWidths: []int{10}, + want: 10, + }, + { + name: "two columns", + colWidths: []int{10, 20}, + want: 32, // 10 + 20 + 2 (separator) + }, + { + name: "three columns", + colWidths: []int{5, 10, 15}, + want: 34, // 5 + 10 + 15 + 4 (2 separators) + }, + { + name: "empty", + colWidths: []int{}, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := calculateTotalWidth(tt.colWidths) + if got != tt.want { + t.Errorf("calculateTotalWidth() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestVerticalOutput(t *testing.T) { + // Test vertical output format directly + records := [][]string{ + {"name", "age", "city"}, + {"Alice", "30", "NYC"}, + {"Bob", "25", "LA"}, + } + + p := core.NewHandle(core.ColorOff).Stderr() + err := writeVertical(p, records) + if err != nil { + t.Fatalf("writeVertical() error = %v", err) + } + + output := string(p.Bytes()) + + // Check for row separators + if !strings.Contains(output, "--- Row 1 ---") { + t.Errorf("output should contain '--- Row 1 ---', got: %s", output) + } + if !strings.Contains(output, "--- Row 2 ---") { + t.Errorf("output should contain '--- Row 2 ---', got: %s", output) + } + + // Check that all field labels and values are present + for _, want := range []string{"name:", "age:", "city:", "Alice", "30", "NYC", "Bob", "25", "LA"} { + if !strings.Contains(output, want) { + t.Errorf("output should contain %q, got: %s", want, output) + } + } +} + +func TestVerticalOutputHeaderAlignment(t *testing.T) { + // Test that headers are right-aligned in vertical mode + records := [][]string{ + {"a", "longer_header"}, + {"val1", "val2"}, + } + + p := core.NewHandle(core.ColorOff).Stderr() + err := writeVertical(p, records) + if err != nil { + t.Fatalf("writeVertical() error = %v", err) + } + + output := string(p.Bytes()) + lines := strings.Split(output, "\n") + + // Find the line with "a:" - it should have padding before it + for _, line := range lines { + if strings.Contains(line, "a:") { + // "a" should be right-aligned to match "longer_header" (13 chars) + // So there should be 12 spaces before "a:" + if !strings.HasPrefix(line, " a:") { + t.Errorf("expected 'a' to be right-aligned with padding, got: %q", line) + } + break + } + } +} + +func TestUnicodeDisplayWidth(t *testing.T) { + // Test that CJK characters and emoji are measured correctly + tests := []struct { + name string + records [][]string + wantMax int // expected max width for first column + }{ + { + name: "ascii", + records: [][]string{ + {"hello"}, + {"world"}, + }, + wantMax: 5, + }, + { + name: "cjk", + records: [][]string{ + {"名前"}, // 2 CJK chars = 4 display columns + {"ab"}, // 2 ASCII chars = 2 display columns + }, + wantMax: 4, + }, + { + name: "mixed", + records: [][]string{ + {"hello世界"}, // 5 ASCII + 2 CJK = 5 + 4 = 9 display columns + {"test"}, // 4 ASCII = 4 display columns + }, + wantMax: 9, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + widths := calculateColumnWidths(tt.records) + if len(widths) == 0 { + t.Fatalf("expected at least one column width") + } + if widths[0] != tt.wantMax { + t.Errorf("calculateColumnWidths() first column = %d, want %d", widths[0], tt.wantMax) + } + }) + } +} + +func TestVerticalModeWithRaggedRows(t *testing.T) { + // Test vertical mode with rows that have fewer columns than the header + records := [][]string{ + {"a", "b", "c"}, + {"1", "2"}, // Missing third column + {"x", "y", "z"}, + } + + p := core.NewHandle(core.ColorOff).Stderr() + err := writeVertical(p, records) + if err != nil { + t.Fatalf("writeVertical() error = %v", err) + } + + output := string(p.Bytes()) + + // Should still contain all rows + if !strings.Contains(output, "--- Row 1 ---") { + t.Errorf("output should contain '--- Row 1 ---'") + } + if !strings.Contains(output, "--- Row 2 ---") { + t.Errorf("output should contain '--- Row 2 ---'") + } + + // Row 1 should have only 2 fields displayed + // Row 2 should have all 3 fields + if !strings.Contains(output, "x") && !strings.Contains(output, "y") && !strings.Contains(output, "z") { + t.Errorf("output should contain all values from row 2") + } +} + +func TestVerticalModeWithExtraColumns(t *testing.T) { + // Test vertical mode with rows that have more columns than the header + records := [][]string{ + {"a", "b"}, + {"1", "2", "3"}, // Extra column without header + } + + p := core.NewHandle(core.ColorOff).Stderr() + err := writeVertical(p, records) + if err != nil { + t.Fatalf("writeVertical() error = %v", err) + } + + output := string(p.Bytes()) + + // Should contain the extra value even without a header + if !strings.Contains(output, "3") { + t.Errorf("output should contain '3' even without header, got: %s", output) + } +} diff --git a/internal/image/image.go b/internal/image/image.go index b200766..b36cc18 100644 --- a/internal/image/image.go +++ b/internal/image/image.go @@ -10,6 +10,8 @@ import ( "image/png" "strings" + "github.com/ryanfowler/fetch/internal/core" + "golang.org/x/image/draw" _ "golang.org/x/image/tiff" _ "golang.org/x/image/webp" @@ -30,23 +32,23 @@ func Render(ctx context.Context, b []byte, nativeOnly bool) error { return nil } - size, err := getTerminalSize() + size, err := core.GetTerminalSize() if err != nil { return err } - if size.widthPx == 0 || size.heightPx == 0 { + if size.WidthPx == 0 || size.HeightPx == 0 { // If we're unable to get the terminal dimensions in pixels, // render the image using blocks. - return writeBlocks(img, size.cols, size.rows) + return writeBlocks(img, size.Cols, size.Rows) } switch detectEmulator().Protocol() { case protoInline: - return writeInline(img, size.widthPx, size.heightPx) + return writeInline(img, size.WidthPx, size.HeightPx) case protoKitty: - return writeKitty(img, size.widthPx, size.heightPx) + return writeKitty(img, size.WidthPx, size.HeightPx) default: - return writeBlocks(img, size.cols, size.rows) + return writeBlocks(img, size.Cols, size.Rows) } } @@ -144,10 +146,3 @@ func convertToRGBA(img image.Image) *image.RGBA { return out } } - -type terminalSize struct { - cols int - rows int - widthPx int - heightPx int -} diff --git a/internal/image/term_unix.go b/internal/image/term_unix.go deleted file mode 100644 index b5f5992..0000000 --- a/internal/image/term_unix.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build unix - -package image - -import ( - "os" - - "golang.org/x/sys/unix" -) - -func getTerminalSize() (terminalSize, error) { - var ts terminalSize - ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) - if err != nil { - return ts, err - } - - ts.cols = int(ws.Col) - ts.rows = int(ws.Row) - ts.widthPx = int(ws.Xpixel) - ts.heightPx = int(ws.Ypixel) - return ts, nil -} diff --git a/internal/image/term_windows.go b/internal/image/term_windows.go deleted file mode 100644 index 8129363..0000000 --- a/internal/image/term_windows.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build windows - -package image - -import ( - "os" - - "golang.org/x/sys/windows" -) - -func getTerminalSize() (terminalSize, error) { - var ts terminalSize - - var info windows.ConsoleScreenBufferInfo - handle := windows.Handle(int(os.Stdout.Fd())) - err := windows.GetConsoleScreenBufferInfo(handle, &info) - if err != nil { - return ts, err - } - - ts.cols = int(info.Window.Right - info.Window.Left + 1) - ts.rows = int(info.Window.Bottom - info.Window.Top + 1) - return ts, nil -}