initial commit
This commit is contained in:
@@ -0,0 +1,709 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bargain-scraper/internal/models"
|
||||
"bargain-scraper/internal/scraper"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var (
|
||||
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FFFFFF")).Background(lipgloss.Color("#1E40AF")).Padding(0, 1)
|
||||
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#64748B"))
|
||||
selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Background(lipgloss.Color("#3B82F6")).Padding(0, 1)
|
||||
normalStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#E2E8F0")).Padding(0, 1)
|
||||
errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#EF4444")).Bold(true)
|
||||
)
|
||||
|
||||
type SortOption struct {
|
||||
Name string
|
||||
Key string
|
||||
Order string
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
products []models.StorageProduct
|
||||
filteredProducts []models.StorageProduct
|
||||
categories []models.Category
|
||||
selectedCategory int
|
||||
selectedProduct int
|
||||
searchQuery string
|
||||
isScraping bool
|
||||
progress scraper.ScrapeProgress
|
||||
err error
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
view string
|
||||
sortBy SortOption
|
||||
sortMenuIndex int
|
||||
ScrapeCallback func(models.Category)
|
||||
ExportCallback func(string) error
|
||||
lastExportError string
|
||||
}
|
||||
|
||||
var sortOptions = []SortOption{
|
||||
{Name: "Price (Low to High)", Key: "price", Order: "asc"},
|
||||
{Name: "Price (High to Low)", Key: "price", Order: "desc"},
|
||||
{Name: "Capacity (Small to Large)", Key: "capacity", Order: "asc"},
|
||||
{Name: "Capacity (Large to Small)", Key: "capacity", Order: "desc"},
|
||||
{Name: "Stock Status (In Stock First)", Key: "stock", Order: "desc"},
|
||||
{Name: "Name (A-Z)", Key: "name", Order: "asc"},
|
||||
{Name: "Name (Z-A)", Key: "name", Order: "desc"},
|
||||
}
|
||||
|
||||
func New() *Model {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Model{
|
||||
products: []models.StorageProduct{},
|
||||
filteredProducts: []models.StorageProduct{},
|
||||
categories: models.StorageCategories,
|
||||
selectedCategory: 0,
|
||||
selectedProduct: 0,
|
||||
searchQuery: "",
|
||||
isScraping: false,
|
||||
view: "categories",
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
return m.handleKeyPress(msg)
|
||||
case scraper.ScrapeProgress:
|
||||
m.progress = msg
|
||||
return m, nil
|
||||
case ScrapingDoneMsg:
|
||||
m.isScraping = false
|
||||
if m.view == "products" {
|
||||
return m, nil
|
||||
}
|
||||
m.view = "products"
|
||||
return m, nil
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
type ScrapingDoneMsg struct{}
|
||||
|
||||
func (m *Model) applySort() {
|
||||
if len(m.filteredProducts) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sorted := make([]models.StorageProduct, len(m.filteredProducts))
|
||||
copy(sorted, m.filteredProducts)
|
||||
|
||||
switch m.sortBy.Key {
|
||||
case "price":
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
priceI := extractPriceValue(sorted[i].Price)
|
||||
priceJ := extractPriceValue(sorted[j].Price)
|
||||
if m.sortBy.Order == "asc" {
|
||||
return priceI < priceJ
|
||||
}
|
||||
return priceI > priceJ
|
||||
})
|
||||
case "capacity":
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
capI := extractCapacityValue(sorted[i].Capacity)
|
||||
capJ := extractCapacityValue(sorted[j].Capacity)
|
||||
if m.sortBy.Order == "asc" {
|
||||
return capI < capJ
|
||||
}
|
||||
return capI > capJ
|
||||
})
|
||||
case "stock":
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
stockI := sorted[i].StockStatus
|
||||
stockJ := sorted[j].StockStatus
|
||||
inStockI := strings.Contains(strings.ToLower(stockI), "in stock")
|
||||
inStockJ := strings.Contains(strings.ToLower(stockJ), "in stock")
|
||||
if inStockI && !inStockJ {
|
||||
return true
|
||||
}
|
||||
if !inStockI && inStockJ {
|
||||
return false
|
||||
}
|
||||
return sorted[i].Name < sorted[j].Name
|
||||
})
|
||||
case "name":
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
if m.sortBy.Order == "asc" {
|
||||
return sorted[i].Name < sorted[j].Name
|
||||
}
|
||||
return sorted[i].Name > sorted[j].Name
|
||||
})
|
||||
}
|
||||
|
||||
m.filteredProducts = sorted
|
||||
}
|
||||
|
||||
func extractPriceValue(priceStr string) float64 {
|
||||
priceStr = strings.ReplaceAll(priceStr, "£", "")
|
||||
priceStr = strings.ReplaceAll(priceStr, ",", "")
|
||||
parts := strings.Split(priceStr, " ")
|
||||
if len(parts) > 0 {
|
||||
priceStr = parts[0]
|
||||
}
|
||||
price, _ := strconv.ParseFloat(priceStr, 64)
|
||||
return price
|
||||
}
|
||||
|
||||
func extractCapacityValue(capStr string) float64 {
|
||||
capStr = strings.ToUpper(capStr)
|
||||
multiplier := 1.0
|
||||
if strings.Contains(capStr, "TB") {
|
||||
multiplier = 1000.0
|
||||
}
|
||||
capStr = strings.ReplaceAll(capStr, "TB", "")
|
||||
capStr = strings.ReplaceAll(capStr, "GB", "")
|
||||
capStr = strings.ReplaceAll(capStr, " ", "")
|
||||
cap, _ := strconv.ParseFloat(capStr, 64)
|
||||
return cap * multiplier
|
||||
}
|
||||
|
||||
func (m *Model) SetSort(option SortOption) {
|
||||
m.sortBy = option
|
||||
m.applySort()
|
||||
}
|
||||
|
||||
func (m *Model) View() string {
|
||||
if m.err != nil {
|
||||
return m.renderError()
|
||||
}
|
||||
|
||||
if m.isScraping {
|
||||
return m.renderProgress()
|
||||
}
|
||||
|
||||
switch m.view {
|
||||
case "categories":
|
||||
return m.renderCategories()
|
||||
case "products":
|
||||
return m.renderProducts()
|
||||
case "export":
|
||||
return m.renderExport()
|
||||
default:
|
||||
return m.renderCategories()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.isScraping {
|
||||
if msg.String() == "esc" {
|
||||
m.CancelScraping()
|
||||
m.isScraping = false
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch m.view {
|
||||
case "categories":
|
||||
return m.handleCategoriesKeyPress(msg)
|
||||
case "products":
|
||||
return m.handleProductsKeyPress(msg)
|
||||
case "export":
|
||||
return m.handleExportKeyPress(msg)
|
||||
case "sort":
|
||||
return m.handleSortMenuKeyPress(msg)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) handleCategoriesKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.selectedCategory > 0 {
|
||||
m.selectedCategory--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedCategory < len(m.categories)-1 {
|
||||
m.selectedCategory++
|
||||
}
|
||||
case "enter":
|
||||
m.view = "products"
|
||||
m.isScraping = true
|
||||
if m.ScrapeCallback != nil {
|
||||
go m.ScrapeCallback(m.categories[m.selectedCategory])
|
||||
}
|
||||
case "esc":
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) handleProductsKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.selectedProduct > 0 {
|
||||
m.selectedProduct--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.selectedProduct < len(m.filteredProducts)-1 {
|
||||
m.selectedProduct++
|
||||
}
|
||||
case "enter":
|
||||
if len(m.filteredProducts) > 0 && m.selectedProduct < len(m.filteredProducts) {
|
||||
url := m.filteredProducts[m.selectedProduct].URL
|
||||
if url != "" {
|
||||
go func() {
|
||||
cmd := exec.Command("xdg-open", url)
|
||||
cmd.Start()
|
||||
}()
|
||||
}
|
||||
}
|
||||
case "/":
|
||||
m.searchQuery = ""
|
||||
case "r":
|
||||
m.isScraping = true
|
||||
m.view = "products"
|
||||
case "e":
|
||||
m.view = "export"
|
||||
case "o":
|
||||
m.sortMenuIndex = 0
|
||||
m.view = "sort"
|
||||
case "p":
|
||||
m.SetSort(SortOption{Name: "Price (Low to High)", Key: "price", Order: "asc"})
|
||||
case "P":
|
||||
m.SetSort(SortOption{Name: "Price (High to Low)", Key: "price", Order: "desc"})
|
||||
case "c":
|
||||
m.SetSort(SortOption{Name: "Capacity (Small to Large)", Key: "capacity", Order: "asc"})
|
||||
case "C":
|
||||
m.SetSort(SortOption{Name: "Capacity (Large to Small)", Key: "capacity", Order: "desc"})
|
||||
case "s":
|
||||
m.SetSort(SortOption{Name: "Stock Status (In Stock First)", Key: "stock", Order: "desc"})
|
||||
case "esc":
|
||||
m.view = "categories"
|
||||
m.searchQuery = ""
|
||||
default:
|
||||
if len(msg.String()) == 1 {
|
||||
m.searchQuery += msg.String()
|
||||
m.applyFilter()
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) handleExportKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.ExportCallback == nil {
|
||||
m.view = "products"
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var format string
|
||||
switch msg.String() {
|
||||
case "j", "J":
|
||||
format = "json"
|
||||
case "c", "C":
|
||||
format = "csv"
|
||||
case "p", "P":
|
||||
format = "print"
|
||||
case "esc":
|
||||
m.view = "products"
|
||||
return m, nil
|
||||
default:
|
||||
return m, nil
|
||||
}
|
||||
|
||||
err := m.ExportCallback(format)
|
||||
if err != nil {
|
||||
m.lastExportError = err.Error()
|
||||
} else {
|
||||
m.lastExportError = ""
|
||||
}
|
||||
|
||||
m.view = "products"
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) handleSortMenuKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.sortMenuIndex > 0 {
|
||||
m.sortMenuIndex--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.sortMenuIndex < len(sortOptions) {
|
||||
m.sortMenuIndex++
|
||||
}
|
||||
case "enter":
|
||||
if m.sortMenuIndex >= 0 && m.sortMenuIndex < len(sortOptions) {
|
||||
m.SetSort(sortOptions[m.sortMenuIndex])
|
||||
}
|
||||
m.view = "products"
|
||||
case "esc":
|
||||
m.view = "products"
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) GetProducts() []models.StorageProduct {
|
||||
return m.products
|
||||
}
|
||||
|
||||
func (m *Model) GetFilteredProducts() []models.StorageProduct {
|
||||
return m.filteredProducts
|
||||
}
|
||||
|
||||
func (m *Model) GetCategories() []models.Category {
|
||||
return m.categories
|
||||
}
|
||||
|
||||
func (m *Model) GetSelectedCategory() int {
|
||||
return m.selectedCategory
|
||||
}
|
||||
|
||||
func (m *Model) GetSelectedProduct() int {
|
||||
return m.selectedProduct
|
||||
}
|
||||
|
||||
func (m *Model) GetSearchQuery() string {
|
||||
return m.searchQuery
|
||||
}
|
||||
|
||||
func (m *Model) IsScraping() bool {
|
||||
return m.isScraping
|
||||
}
|
||||
|
||||
func (m *Model) GetProgress() scraper.ScrapeProgress {
|
||||
return m.progress
|
||||
}
|
||||
|
||||
func (m *Model) GetError() error {
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *Model) GetView() string {
|
||||
return m.view
|
||||
}
|
||||
|
||||
func (m *Model) SetProducts(products []models.StorageProduct) {
|
||||
m.products = products
|
||||
m.filteredProducts = products
|
||||
m.selectedProduct = 0
|
||||
}
|
||||
|
||||
func (m *Model) SetScraping(isScraping bool) {
|
||||
m.isScraping = isScraping
|
||||
}
|
||||
|
||||
func (m *Model) SetProgress(progress scraper.ScrapeProgress) {
|
||||
m.progress = progress
|
||||
}
|
||||
|
||||
func (m *Model) SetError(err error) {
|
||||
m.err = err
|
||||
}
|
||||
|
||||
func (m *Model) SetView(view string) {
|
||||
m.view = view
|
||||
}
|
||||
|
||||
func (m *Model) SetSelectedCategory(index int) {
|
||||
if index >= 0 && index < len(m.categories) {
|
||||
m.selectedCategory = index
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) SetSelectedProduct(index int) {
|
||||
if index >= 0 && index < len(m.filteredProducts) {
|
||||
m.selectedProduct = index
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) SetSearchQuery(query string) {
|
||||
m.searchQuery = query
|
||||
m.applyFilter()
|
||||
}
|
||||
|
||||
func (m *Model) applyFilter() {
|
||||
if m.searchQuery == "" {
|
||||
m.filteredProducts = m.products
|
||||
} else {
|
||||
query := toLower(m.searchQuery)
|
||||
var filtered []models.StorageProduct
|
||||
for _, p := range m.products {
|
||||
if contains(toLower(p.Name), query) ||
|
||||
contains(toLower(p.Capacity), query) ||
|
||||
contains(toLower(p.Brand), query) ||
|
||||
contains(toLower(p.Interface), query) ||
|
||||
contains(toLower(p.FormFactor), query) ||
|
||||
contains(toLower(p.PartNumber), query) {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
m.filteredProducts = filtered
|
||||
}
|
||||
m.selectedProduct = 0
|
||||
}
|
||||
|
||||
func toLower(s string) string {
|
||||
result := make([]rune, len(s))
|
||||
for i, r := range s {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
result[i] = r + 32
|
||||
} else {
|
||||
result[i] = r
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
if len(substr) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(s) < len(substr) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Model) FilterByCategory(categoryIndex int) {
|
||||
if categoryIndex < 0 || categoryIndex >= len(m.categories) {
|
||||
return
|
||||
}
|
||||
|
||||
category := m.categories[categoryIndex]
|
||||
m.selectedCategory = categoryIndex
|
||||
|
||||
if category.Name == "All Categories" {
|
||||
m.filteredProducts = m.products
|
||||
} else {
|
||||
var filtered []models.StorageProduct
|
||||
for _, p := range m.products {
|
||||
if matchesCategory(p, category) {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
m.filteredProducts = filtered
|
||||
}
|
||||
m.selectedProduct = 0
|
||||
}
|
||||
|
||||
func matchesCategory(p models.StorageProduct, category models.Category) bool {
|
||||
catLower := toLower(category.Name)
|
||||
|
||||
if contains(catLower, "3.5") && p.FormFactor == "3.5\"" {
|
||||
return true
|
||||
}
|
||||
if contains(catLower, "2.5") && p.FormFactor == "2.5\"" {
|
||||
return true
|
||||
}
|
||||
if contains(catLower, "nvme") && contains(toLower(p.Interface), "nvme") {
|
||||
return true
|
||||
}
|
||||
if contains(catLower, "sata") && contains(toLower(p.Interface), "sata") {
|
||||
return true
|
||||
}
|
||||
if contains(catLower, "sas") && contains(toLower(p.Interface), "sas") {
|
||||
return true
|
||||
}
|
||||
if contains(catLower, "ssd") && (p.Interface == "SATA" || p.Interface == "SAS" || p.Interface == "NVMe") {
|
||||
return true
|
||||
}
|
||||
if contains(catLower, "hdd") && (p.Interface == "SATA" || p.Interface == "SAS") && p.Capacity != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Model) GetContext() context.Context {
|
||||
return m.ctx
|
||||
}
|
||||
|
||||
func (m *Model) CancelScraping() {
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) renderCategories() string {
|
||||
var result string
|
||||
result += headerStyle.Width(60).Render(" Select Storage Category ")
|
||||
result += "\n\n"
|
||||
|
||||
result += subtleStyle.Render("Use arrow keys to navigate, Enter to select, Esc to go back\n")
|
||||
result += "\n"
|
||||
|
||||
for i, cat := range m.categories {
|
||||
if i == m.selectedCategory {
|
||||
result += selectedStyle.Width(58).Render(" " + cat.Name)
|
||||
} else {
|
||||
result += normalStyle.Width(58).Render(" " + cat.Name)
|
||||
}
|
||||
result += "\n"
|
||||
}
|
||||
|
||||
result += "\n"
|
||||
result += subtleStyle.Render(fmt.Sprintf("Categories: %d | Press Enter to scrape selected category", len(m.categories)))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Model) renderProducts() string {
|
||||
var result string
|
||||
|
||||
title := fmt.Sprintf(" Storage Products - %s ", m.categories[m.selectedCategory].Name)
|
||||
result += headerStyle.Width(80).Render(title)
|
||||
result += "\n\n"
|
||||
|
||||
if len(m.products) == 0 {
|
||||
result += subtleStyle.Render("No products scraped yet. Select a category and press Enter to scrape.\n")
|
||||
result += "\n"
|
||||
result += subtleStyle.Render("Press Esc to go back to category selection.\n")
|
||||
return result
|
||||
}
|
||||
|
||||
result += subtleStyle.Render(fmt.Sprintf("Search: %s | ", m.searchQuery))
|
||||
result += fmt.Sprintf("Showing: %d/%d products", len(m.filteredProducts), len(m.products))
|
||||
result += "\n\n"
|
||||
|
||||
result += m.renderProductTable()
|
||||
|
||||
result += "\n\n"
|
||||
result += subtleStyle.Render("Controls: ↑↓ Navigate | / Search | E Export | Esc Back | R Rescrape")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Model) renderProductTable() string {
|
||||
if len(m.filteredProducts) == 0 {
|
||||
return " No products found matching your criteria."
|
||||
}
|
||||
|
||||
var result string
|
||||
|
||||
header := " # Name Capacity Interface FormFactor Price \n"
|
||||
result += headerStyle.Render(header)
|
||||
|
||||
start := m.selectedProduct - 5
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := start + 15
|
||||
if end > len(m.filteredProducts) {
|
||||
end = len(m.filteredProducts)
|
||||
}
|
||||
|
||||
for i := start; i < end; i++ {
|
||||
p := m.filteredProducts[i]
|
||||
row := fmt.Sprintf(" %-4d %-35s %-10s %-8s %-10s %-12s\n",
|
||||
i+1,
|
||||
truncate(p.Name, 35),
|
||||
p.Capacity,
|
||||
p.Interface,
|
||||
p.FormFactor,
|
||||
truncate(p.Price, 12))
|
||||
|
||||
if i == m.selectedProduct {
|
||||
result += selectedStyle.Width(79).Render(row)
|
||||
} else {
|
||||
result += normalStyle.Width(79).Render(row)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Model) renderProgress() string {
|
||||
var result string
|
||||
|
||||
result += headerStyle.Width(60).Render(" Scraping Progress ")
|
||||
result += "\n\n"
|
||||
|
||||
result += fmt.Sprintf(" Category: %s\n\n", m.progress.Category)
|
||||
result += fmt.Sprintf(" Status: %s\n\n", m.progress.Status)
|
||||
|
||||
if m.progress.Total > 0 {
|
||||
percent := float64(m.progress.Current) / float64(m.progress.Total) * 100
|
||||
barWidth := 40
|
||||
filled := int(float64(barWidth) * percent / 100)
|
||||
|
||||
bar := "["
|
||||
for i := 0; i < barWidth; i++ {
|
||||
if i < filled {
|
||||
bar += "="
|
||||
} else if i == filled {
|
||||
bar += ">"
|
||||
} else {
|
||||
bar += " "
|
||||
}
|
||||
}
|
||||
bar += "]"
|
||||
|
||||
result += fmt.Sprintf(" Progress: %s %d/%d (%.1f%%)\n\n", bar, m.progress.Current, m.progress.Total, percent)
|
||||
}
|
||||
|
||||
if m.progress.ProductName != "" {
|
||||
result += fmt.Sprintf(" Current: %s\n", truncate(m.progress.ProductName, 50))
|
||||
}
|
||||
|
||||
result += "\n\n"
|
||||
result += subtleStyle.Render("Press Esc to cancel...")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Model) renderError() string {
|
||||
var result string
|
||||
|
||||
result += headerStyle.Width(60).Render(" Error ")
|
||||
result += "\n\n"
|
||||
|
||||
result += errorStyle.Render(fmt.Sprintf(" %v\n", m.err))
|
||||
result += "\n\n"
|
||||
result += subtleStyle.Render("Press any key to continue...")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Model) renderExport() string {
|
||||
var result string
|
||||
|
||||
result += headerStyle.Width(60).Render(" Export Data ")
|
||||
result += "\n\n"
|
||||
|
||||
result += fmt.Sprintf(" Products: %d\n", len(m.products))
|
||||
result += fmt.Sprintf(" Filtered: %d\n\n", len(m.filteredProducts))
|
||||
|
||||
result += " Export Options:\n"
|
||||
result += " [J] Export to JSON\n"
|
||||
result += " [C] Export to CSV\n"
|
||||
result += " [P] Print to console\n"
|
||||
result += " [Esc] Go back\n"
|
||||
|
||||
result += "\n\n"
|
||||
result += subtleStyle.Render("Select an option...")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen-3] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user