feat: add CLI with TUI, self-update, install script, and terminal section on landing page

- Full-screen Bubble Tea TUI with cream background fill using PadLine/FillBlankLines
- Self-update command (--update) pulling from GitHub releases
- install.sh for curl one-liner installation
- Terminal Lovers section on web landing page with install command and CLI features
- All 7 format categories, glob/directory batch support, auto-download ffmpeg
This commit is contained in:
noah
2026-03-09 22:53:10 +01:00
parent b9aac61ab8
commit 04a1f33cb1
23 changed files with 4064 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
package converter
import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/xuri/excelize/v2"
"gopkg.in/yaml.v3"
)
func convertSpreadsheet(inputPath, outputPath, sourceExt, targetFormat string) error {
// Read spreadsheet using excelize (supports xlsx, xls via xlsx conversion)
f, err := excelize.OpenFile(inputPath)
if err != nil {
return fmt.Errorf("opening spreadsheet: %w", err)
}
defer f.Close()
// Get first sheet
sheetName := f.GetSheetName(0)
if sheetName == "" {
return fmt.Errorf("no sheets found in spreadsheet")
}
rows, err := f.GetRows(sheetName)
if err != nil {
return fmt.Errorf("reading rows: %w", err)
}
if len(rows) == 0 {
return fmt.Errorf("spreadsheet is empty")
}
headers := rows[0]
dataRows := rows[1:]
switch targetFormat {
case "csv":
return writeSpreadsheetDelimited(headers, dataRows, outputPath, ',')
case "tsv":
return writeSpreadsheetDelimited(headers, dataRows, outputPath, '\t')
case "json":
return writeSpreadsheetJSON(headers, dataRows, outputPath)
case "yaml", "yml":
return writeSpreadsheetYAML(headers, dataRows, outputPath)
case "xml":
return writeSpreadsheetXML(headers, dataRows, outputPath)
case "html":
return writeSpreadsheetHTML(headers, dataRows, outputPath)
default:
return fmt.Errorf("unsupported target for spreadsheet: %s", targetFormat)
}
}
func writeSpreadsheetDelimited(headers []string, rows [][]string, outputPath string, sep rune) error {
f, err := os.Create(outputPath)
if err != nil {
return err
}
defer f.Close()
w := csv.NewWriter(f)
w.Comma = sep
if err := w.Write(headers); err != nil {
return err
}
for _, row := range rows {
// Pad row to match header length
for len(row) < len(headers) {
row = append(row, "")
}
if err := w.Write(row[:len(headers)]); err != nil {
return err
}
}
w.Flush()
return w.Error()
}
func writeSpreadsheetJSON(headers []string, rows [][]string, outputPath string) error {
var records []map[string]string
for _, row := range rows {
record := make(map[string]string)
for i, h := range headers {
if i < len(row) {
record[h] = row[i]
} else {
record[h] = ""
}
}
records = append(records, record)
}
b, err := json.MarshalIndent(records, "", " ")
if err != nil {
return err
}
return os.WriteFile(outputPath, b, 0o644)
}
func writeSpreadsheetYAML(headers []string, rows [][]string, outputPath string) error {
var records []map[string]string
for _, row := range rows {
record := make(map[string]string)
for i, h := range headers {
if i < len(row) {
record[h] = row[i]
} else {
record[h] = ""
}
}
records = append(records, record)
}
b, err := yaml.Marshal(records)
if err != nil {
return err
}
return os.WriteFile(outputPath, b, 0o644)
}
func writeSpreadsheetXML(headers []string, rows [][]string, outputPath string) error {
var sb strings.Builder
sb.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<spreadsheet>\n")
for _, row := range rows {
sb.WriteString(" <row>\n")
for i, h := range headers {
val := ""
if i < len(row) {
val = row[i]
}
sb.WriteString(fmt.Sprintf(" <%s>%s</%s>\n", h, val, h))
}
sb.WriteString(" </row>\n")
}
sb.WriteString("</spreadsheet>")
return os.WriteFile(outputPath, []byte(sb.String()), 0o644)
}
func writeSpreadsheetHTML(headers []string, rows [][]string, outputPath string) error {
var sb strings.Builder
sb.WriteString("<html><body>\n<table border=\"1\">\n<thead>\n<tr>")
for _, h := range headers {
sb.WriteString("<th>" + h + "</th>")
}
sb.WriteString("</tr>\n</thead>\n<tbody>\n")
for _, row := range rows {
sb.WriteString("<tr>")
for i := range headers {
val := ""
if i < len(row) {
val = row[i]
}
sb.WriteString("<td>" + val + "</td>")
}
sb.WriteString("</tr>\n")
}
sb.WriteString("</tbody>\n</table>\n</body></html>")
return os.WriteFile(outputPath, []byte(sb.String()), 0o644)
}