Interactive App Usage Table With Bubbles

by Admin 41 views
Interactive App Usage Table with Bubbles

Hey guys! Let's dive into making the app usage data in our rekap tool way cooler and more informative. The goal? To swap out that basic app list with a slick, sortable table that’ll give us a much better view of what we're actually doing on our computers. Think of it as a serious upgrade to how we explore our app usage, making it easier to see what’s eating up your time. Ready to get started?

The Problem: Simple App List

Currently, the app usage is presented as a pretty simple list. It shows the top three apps with their usage time. While it's a start, it's not super helpful if you want to see a full breakdown or sort things by time, app name, or category. Take a look at the current output, it's pretty basic, right?

PRODUCTIVITY

  ⏱️  Best focus: 1h 27m in VS Code
  πŸ“±  VS Code β€’ 2h 22m
  πŸ“±  Safari β€’ 1h 29m
  πŸ“±  Slack β€’ 52m

The Solution: A Sortable Table

We're going to replace that simple list with a sortable table using the bubbles/table component from Charmbracelet. This lets users sort by time, app name, or category. Here's what we're aiming for:

PRODUCTIVITY

  ⏱️  Best focus: 1h 27m in VS Code

  πŸ“±  App Usage Today                           [Sort: Time ↓]

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Application        β”‚ Time     β”‚ Category   β”‚ Activity β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚ VS Code            β”‚ 2h 22m   β”‚ Work       β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚
  β”‚ Safari             β”‚ 1h 29m   β”‚ Browser    β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β”‚
  β”‚ Slack              β”‚ 52m      β”‚ Comm       β”‚ β–ˆβ–ˆβ–ˆ      β”‚
  β”‚ Terminal           β”‚ 38m      β”‚ Work       β”‚ β–ˆβ–ˆ       β”‚
  β”‚ Chrome             β”‚ 27m      β”‚ Browser    β”‚ β–ˆ        β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Press 't' to sort by time β€’ 'n' for name β€’ 'c' for category

See how much more information-rich this is? Plus, the sorting is super handy. Let's make it happen!

Step-by-Step Implementation Guide

Alright, let's get into the nitty-gritty. This is where we'll walk through the code changes. I'll try to break it down as simple as I can.

Step 1: Add the Dependency

First things first, we need to bring in the bubbles/table package. Open your terminal and run this command:

go get github.com/charmbracelet/bubbles/table

This fetches the necessary code so we can use the table component.

Step 2: Create the Table Helper (internal/ui/table.go)

Now, let's create a helper file to handle the table rendering. Create a new file named internal/ui/table.go and add the following code. This code sets up the table with columns for application, time, category, and a visual activity bar. It also includes styling for a polished look. We'll also build in a fallback for non-TTY environments, so it gracefully degrades to a simple list when needed. This is where the magic happens, setting up the table with the data and styling it nicely.

package ui

import (
    "fmt"
    "strings"

    "github.com/charmbracelet/bubbles/table"
    "github.com/charmbracelet/lipgloss"
)

var (
    tableHeaderStyle = lipgloss.NewStyle().
        Bold(true).
        Foreground(primaryColor).
        BorderStyle(lipgloss.NormalBorder()).
        BorderBottom(true).
        BorderForeground(mutedColor)

    tableRowStyle = lipgloss.NewStyle().
        Foreground(textColor)

    tableSelectedStyle = lipgloss.NewStyle().
        Bold(true).
        Foreground(accentColor).
        Background(lipgloss.Color("235"))
)

// RenderAppTable creates a styled table for app usage
func RenderAppTable(apps []collectors.AppUsage, maxRows int) string {
    if !IsTTY() {
        // Fallback to simple list
        return renderAppList(apps, maxRows)
    }

    columns := []table.Column{
        {Title: "Application", Width: 20},
        {Title: "Time", Width: 10},
        {Title: "Category", Width: 12},
        {Title: "Activity", Width: 10},
    }

    rows := make([]table.Row, 0, len(apps))
    maxMinutes := 0
    if len(apps) > 0 {
        maxMinutes = apps[0].Minutes
    }

    for i, app := range apps {
        if i >= maxRows {
            break
        }

        // Categorize app
        category := categorizeApp(app.BundleID)

        // Create activity bar
        barWidth := 8
        filledBars := (app.Minutes * barWidth) / maxMinutes
        if filledBars < 1 && app.Minutes > 0 {
            filledBars = 1
        }
        activityBar := strings.Repeat("β–ˆ", filledBars) +
            strings.Repeat("β–‘", barWidth-filledBars)

        rows = append(rows, table.Row{
            app.Name,
            FormatDuration(app.Minutes),
            category,
            activityBar,
        })
    }

    t := table.New(
        table.WithColumns(columns),
        table.WithRows(rows),
        table.WithFocused(false),
        table.WithHeight(len(rows)),
    )

    // Apply styles
    s := table.DefaultStyles()
    s.Header = tableHeaderStyle
    s.Cell = tableRowStyle
    s.Selected = tableSelectedStyle
    t.SetStyles(s)

    return t.View()
}

func categorizeApp(bundleID string) string {
    // Simple categorization based on bundle ID
    switch {
    case strings.Contains(bundleID, "VSCode"),
        strings.Contains(bundleID, "Xcode"),
        strings.Contains(bundleID, "Terminal"):
        return "Development"
    case strings.Contains(bundleID, "Safari"),
        strings.Contains(bundleID, "Chrome"),
        strings.Contains(bundleID, "Firefox"):
        return "Browser"
    case strings.Contains(bundleID, "Slack"),
        strings.Contains(bundleID, "Discord"),
        strings.Contains(bundleID, "Messages"):
        return "Communication"
    case strings.Contains(bundleID, "Spotify"),
        strings.Contains(bundleID, "Music"):
        return "Media"
    default:
        return "Other"
    }
}

func renderAppList(apps []collectors.AppUsage, maxRows int) string {
    // Fallback for non-TTY
    var b strings.Builder
    for i, app := range apps {
        if i >= maxRows {
            break
        }
        b.WriteString(fmt.Sprintf("  %s β€’ %s\n",
            app.Name, FormatDuration(app.Minutes)))
    }
    return b.String()
}

Step 3: Integrate into Main Output (cmd/rekap/main.go)

Now we need to modify cmd/rekap/main.go to use this shiny new table. Find the section where the app list is currently printed and replace it with a call to our new RenderAppTable function. This step is about integrating the table into the existing output of your application. Replace the simple list output with our new table.

// Productivity Section
if apps.Available && len(apps.TopApps) > 0 {
    fmt.Println()
    fmt.Println(ui.RenderHeader("PRODUCTIVITY"))

    if focus.Available {
        text := fmt.Sprintf("Best focus: %s in %s",
            ui.FormatDuration(focus.StreakMinutes), focus.AppName)
        fmt.Println(ui.RenderHighlight("⏱️ ", text))
        fmt.Println()
    }

    // Use table instead of simple list
    fmt.Println(ui.RenderDataPoint("πŸ“±", "App Usage Today"))
    fmt.Println()
    table := ui.RenderAppTable(apps.TopApps, 10) // Show top 10
    fmt.Println(table)
}

Step 4: Add Interactive Table (TUI Mode)

For an even better experience, let's make the table interactive in TUI mode. This will let users sort the table directly from the terminal. This involves adding keybindings for sorting by time, name, and category. The Update method handles key presses and triggers sorting. The resort method actually sorts the data based on the chosen criteria. The View method renders the table. This adds interactivity to your application, allowing users to sort data in the terminal.

type AppTableModel {
    table     table.Model
    apps      []collectors.AppUsage
    sortBy    string // "time", "name", "category"
    sortDesc  bool
}

func (m AppTableModel) Update(msg tea.Msg) (AppTableModel, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "t":
            m.sortBy = "time"
            m.sortDesc = !m.sortDesc
            m.resort()
        case "n":
            m.sortBy = "name"
            m.sortDesc = !m.sortDesc
            m.resort()
        case "c":
            m.sortBy = "category"
            m.sortDesc = !m.sortDesc
            m.resort()
        }
    }

    var cmd tea.Cmd
    m.table, cmd = m.table.Update(msg)
    return m, cmd
}

func (m *AppTableModel) resort() {
    sort.Slice(m.apps, func(i, j int) bool {
        switch m.sortBy {
        case "name":
            if m.sortDesc {
                return m.apps[i].Name > m.apps[j].Name
            }
            return m.apps[i].Name < m.apps[j].Name
        case "category":
            catI := categorizeApp(m.apps[i].BundleID)
            catJ := categorizeApp(m.apps[j].BundleID)
            if m.sortDesc {
                return catI > catJ
            }
            return catI < catJ
        default: // time
            if m.sortDesc {
                return m.apps[i].Minutes > m.apps[j].Minutes
            }
            return m.apps[i].Minutes < m.apps[j].Minutes
        }
    })

    // Rebuild table rows
    m.rebuildTable()
}

Step 5: Add Scrolling for Long Lists

To handle longer lists of apps, let's add scrolling. This makes sure that even if you have dozens of apps, they all remain visible. We'll use a viewport from the charmbracelet/bubbles package to enable scrolling.

import "github.com/charmbracelet/bubbles/viewport"

type AppTableModel {
    table    table.Model
    viewport viewport.Model
    apps     []collectors.AppUsage
}

func (m AppTableModel) View() string {
    // If more apps than fit on screen, use viewport
    if len(m.apps) > 15 {
        m.viewport.SetContent(m.table.View())
        return m.viewport.View()
    }
    return m.table.View()
}

Visual Examples

Let's see the before and after, shall we?

Static Mode (Standard Output)

This is what the table looks like in a standard terminal output.

PRODUCTIVITY

  ⏱️  Best focus: 1h 27m in VS Code

  πŸ“±  App Usage Today

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Application        β”‚ Time     β”‚ Category     β”‚ Activity β”‚
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚ VS Code            β”‚ 2h 22m   β”‚ Development  β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚
  β”‚ Safari             β”‚ 1h 29m   β”‚ Browser      β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β”‚
  β”‚ Slack              β”‚ 52m      β”‚ Comm         β”‚ β–ˆβ–ˆβ–ˆ      β”‚
  β”‚ Terminal           β”‚ 38m      β”‚ Development  β”‚ β–ˆβ–ˆ       β”‚
  β”‚ Chrome             β”‚ 27m      β”‚ Browser      β”‚ β–ˆ        β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Interactive Mode (TUI)

And here's the interactive TUI version. You can sort by time, name, and category using the t, n, and c keys, respectively.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Application β–Ό      β”‚ Time     β”‚ Category     β”‚ Activity β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ VS Code            β”‚ 2h 22m   β”‚ Development  β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β”‚
β”‚ Safari             β”‚ 1h 29m   β”‚ Browser      β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ    β”‚
β”‚ Slack              β”‚ 52m      β”‚ Comm         β”‚ β–ˆβ–ˆβ–ˆ      β”‚
β”‚ Terminal           β”‚ 38m      β”‚ Development  β”‚ β–ˆβ–ˆ       β”‚
β”‚ Chrome             β”‚ 27m      β”‚ Browser      β”‚ β–ˆ        β”‚
β”‚ Notion             β”‚ 18m      β”‚ Other        β”‚ β–ˆ        β”‚
β”‚ Discord            β”‚ 12m      β”‚ Comm         β”‚ β–ˆ        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

↑/↓ navigate β€’ t sort time β€’ n sort name β€’ c sort category

Configuration Options

We can add some configuration options to customize the table's appearance.

display:
  table:
    enabled: true
    max_rows: 10
    show_category: true
    show_activity_bar: true
    border_style: "rounded"  # rounded, normal, thick

This would allow users to enable/disable the table, control the number of rows displayed, and customize visual elements.

Testing Checklist

Make sure to run through this checklist to ensure everything works correctly:

  • [ ] Test with 3 apps (fits without scrolling)
  • [ ] Test with 20+ apps (requires scrolling)
  • [ ] Test table formatting in narrow terminal (<80 cols)
  • [ ] Test with long app names (truncation)
  • [ ] Test activity bars scale correctly
  • [ ] Test category detection for various apps
  • [ ] Run ./rekap --quiet - should not show table
  • [ ] Run ./rekap | cat - should fall back to list
  • [ ] Test sorting in interactive mode (if implemented)

Files to Create

  • internal/ui/table.go

Files to Modify

  • cmd/rekap/main.go (use table in printHuman)
  • go.mod (add bubbles/table dependency)

Estimated Time

  • 3-4 hours for static table
  • +4-6 hours for interactive features

Future Enhancements

Here are some ideas to make this even better in the future:

  • Add filtering (press 'f' to filter by category)
  • Show bundle ID on demand (press 'i' for info)
  • Export table to CSV
  • Add percentage column (% of total screen time)
  • Color-code categories
  • Add icons per category

Wrapping Up

That's it, guys! You should now have a much more useful and visually appealing way to see your app usage data. We've gone from a simple list to a sortable, interactive table. If you have any questions or run into any snags, don't hesitate to ask! Happy coding!

References

Labels

enhancement, visualization