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