Files
ragflow/internal/cli/table.go
Yingfeng 6e309f9d0a Feat: Initialize context engine CLI (#13776)
### What problem does this PR solve?

- Add multiple output format to ragflow_cli
- Initialize contextengine to Go module
  - ls datasets/ls files
  - cat file
  - search -d dir -q query

issue: #13714

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-03-26 21:07:06 +08:00

298 lines
7.7 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package cli
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
const maxColWidth = 256
// PrintTableSimple prints data in a simple table format (default: table format with borders)
// Similar to Python's _print_table_simple
func PrintTableSimple(data []map[string]interface{}) {
PrintTableSimpleByFormat(data, OutputFormatTable)
}
// PrintTableSimpleByFormat prints data in the specified format
// Supports: table (with borders), plain (no borders, space-separated), json
// - Column names in lowercase
// - Two spaces between columns
// - Numeric columns right-aligned
// - URI/path columns not truncated
func PrintTableSimpleByFormat(data []map[string]interface{}, format OutputFormat) {
if len(data) == 0 {
if format == OutputFormatJSON {
fmt.Println("[]")
} else if format == OutputFormatPlain {
fmt.Println("(empty)")
} else {
fmt.Println("No data to print")
}
return
}
// JSON format: output as JSON array
if format == OutputFormatJSON {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Printf("Error marshaling JSON: %v\n", err)
return
}
fmt.Println(string(jsonData))
return
}
// Collect all column names
columnSet := make(map[string]bool)
for _, item := range data {
for key := range item {
columnSet[key] = true
}
}
// Sort columns
columns := make([]string, 0, len(columnSet))
for col := range columnSet {
columns = append(columns, col)
}
// Simple sort - in production you might want specific column ordering
for i := 0; i < len(columns); i++ {
for j := i + 1; j < len(columns); j++ {
if columns[i] > columns[j] {
columns[i], columns[j] = columns[j], columns[i]
}
}
}
// Analyze columns: check if numeric and if URI column
colIsNumeric := make(map[string]bool)
colIsURI := make(map[string]bool)
for _, col := range columns {
colLower := strings.ToLower(col)
if colLower == "uri" || colLower == "path" || colLower == "id" {
colIsURI[col] = true
}
// Check if all values are numeric
isNumeric := true
for _, item := range data {
if val, ok := item[col]; ok {
if !isNumericValue(val) {
isNumeric = false
break
}
}
}
colIsNumeric[col] = isNumeric
}
// Calculate column widths (capped at maxColWidth)
colWidths := make(map[string]int)
for _, col := range columns {
maxWidth := getStringWidth(strings.ToLower(col))
for _, item := range data {
value := formatValue(item[col])
valueWidth := getStringWidth(value)
if valueWidth > maxWidth {
maxWidth = valueWidth
}
}
if maxWidth > maxColWidth {
maxWidth = maxColWidth
}
if maxWidth < 2 {
maxWidth = 2
}
colWidths[col] = maxWidth
}
if format == OutputFormatPlain {
// Plain mode: no borders, space-separated (ov CLI compatible)
// Print header (lowercase column names, right-aligned for numeric columns)
headerParts := make([]string, 0, len(columns))
for _, col := range columns {
// Header follows the same alignment as data (right-aligned for numeric columns)
headerParts = append(headerParts, padCell(strings.ToLower(col), colWidths[col], colIsNumeric[col]))
}
fmt.Println(strings.Join(headerParts, " "))
// Print data rows
for _, item := range data {
rowParts := make([]string, 0, len(columns))
for _, col := range columns {
value := formatValue(item[col])
isURI := colIsURI[col]
isNumeric := colIsNumeric[col]
// URI columns: never truncate, no padding if too long
if isURI && getStringWidth(value) > colWidths[col] {
rowParts = append(rowParts, value)
} else {
// Normal cell: truncate if too long, then pad
valueWidth := getStringWidth(value)
if valueWidth > colWidths[col] {
runes := []rune(value)
value = truncateStringByWidth(runes, colWidths[col])
valueWidth = getStringWidth(value)
}
rowParts = append(rowParts, padCell(value, colWidths[col], isNumeric))
}
}
fmt.Println(strings.Join(rowParts, " "))
}
} else {
// Normal mode: with borders
// Generate separator
separatorParts := make([]string, 0, len(columns))
for _, col := range columns {
separatorParts = append(separatorParts, strings.Repeat("-", colWidths[col]+2))
}
separator := "+" + strings.Join(separatorParts, "+") + "+"
// Print header
fmt.Println(separator)
headerParts := make([]string, 0, len(columns))
for _, col := range columns {
headerParts = append(headerParts, fmt.Sprintf(" %-*s ", colWidths[col], col))
}
fmt.Println("|" + strings.Join(headerParts, "|") + "|")
fmt.Println(separator)
// Print data rows
for _, item := range data {
rowParts := make([]string, 0, len(columns))
for _, col := range columns {
value := formatValue(item[col])
valueWidth := getStringWidth(value)
// Truncate if too long
if valueWidth > colWidths[col] {
runes := []rune(value)
value = truncateStringByWidth(runes, colWidths[col])
valueWidth = getStringWidth(value)
}
// Pad to column width
padding := colWidths[col] - valueWidth + len(value)
rowParts = append(rowParts, fmt.Sprintf(" %-*s ", padding, value))
}
fmt.Println("|" + strings.Join(rowParts, "|") + "|")
}
fmt.Println(separator)
}
}
// formatValue formats a value for display
func formatValue(v interface{}) string {
if v == nil {
return ""
}
switch val := v.(type) {
case string:
return val
case int:
return strconv.Itoa(val)
case int64:
return strconv.FormatInt(val, 10)
case float64:
return strconv.FormatFloat(val, 'f', -1, 64)
case bool:
return strconv.FormatBool(val)
default:
return fmt.Sprintf("%v", v)
}
}
// isNumericValue checks if a value is numeric
func isNumericValue(v interface{}) bool {
if v == nil {
return false
}
switch val := v.(type) {
case int, int8, int16, int32, int64:
return true
case uint, uint8, uint16, uint32, uint64:
return true
case float32, float64:
return true
case string:
_, err := strconv.ParseFloat(val, 64)
return err == nil
default:
return false
}
}
// truncateStringByWidth truncates a string to fit within maxWidth display width
func truncateStringByWidth(runes []rune, maxWidth int) string {
width := 0
for i, r := range runes {
if isHalfWidth(r) {
width++
} else {
width += 2
}
if width > maxWidth-3 {
return string(runes[:i]) + "..."
}
}
return string(runes)
}
// padCell pads a string to the specified width for alignment
func padCell(content string, width int, alignRight bool) string {
contentWidth := getStringWidth(content)
if contentWidth >= width {
return content
}
padding := width - contentWidth
if alignRight {
return strings.Repeat(" ", padding) + content
}
return content + strings.Repeat(" ", padding)
}
// getStringWidth calculates the display width of a string
// Treats CJK characters as width 2
func getStringWidth(text string) int {
width := 0
for _, r := range text {
if isHalfWidth(r) {
width++
} else {
width += 2
}
}
return width
}
// isHalfWidth checks if a rune is half-width
func isHalfWidth(r rune) bool {
// ASCII printable characters and common whitespace
if r >= 0x20 && r <= 0x7E {
return true
}
if r == '\t' || r == '\n' || r == '\r' {
return true
}
return false
}