Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
35d0305
feat: modularize TUI dashboard components into individual view files …
SuperCoolPencil Apr 12, 2026
156de44
style: update PaneTitleStyle foreground color to light gray
SuperCoolPencil Apr 12, 2026
70d0b4e
refactor: simplify dashboard view content dimensions by using fixed p…
SuperCoolPencil Apr 12, 2026
1705203
refactor: replace if-else block with switch statement for settings va…
SuperCoolPencil Apr 12, 2026
832fc52
chore: go fmt
SuperCoolPencil Apr 16, 2026
81b60c0
refactor: improve TUI responsiveness by implementing dynamic layout c…
SuperCoolPencil Apr 16, 2026
cee6e8b
feat: implement dynamic modal sizing and responsive layout adjustment…
SuperCoolPencil Apr 16, 2026
fad33df
refactor: add filepicker path persistence and update keybindings for …
SuperCoolPencil Apr 16, 2026
1ed334f
fix: dont truncate path in edit mode in settings tui
SuperCoolPencil Apr 16, 2026
8f73133
refactor: centralize dashboard layout calculations and unify componen…
SuperCoolPencil Apr 16, 2026
29edd9d
feat: implement vertical layout mode for narrow terminal displays to …
SuperCoolPencil Apr 16, 2026
c4d1cb5
refactor: enable AltScreen in view and adjust log viewport width calc…
SuperCoolPencil Apr 16, 2026
7d5bc81
refactor: replace UI symbols with Unicode escape sequences for consis…
SuperCoolPencil Apr 16, 2026
32dab45
fix: swap box corner characters to correctly align bottom border
SuperCoolPencil Apr 16, 2026
f87d6ce
feat: implement responsive progress bar layout and update progress re…
SuperCoolPencil Apr 16, 2026
f245116
refactor: decouple detail content generation from renderDetailsBox to…
SuperCoolPencil Apr 16, 2026
fac6741
refactor: replace unicode escape sequences with literal characters in…
SuperCoolPencil Apr 16, 2026
af5ae36
refactor: update layout calculations to use available height and enfo…
SuperCoolPencil Apr 16, 2026
3e964aa
refactor: adjust vertical frame size calculations to use single paddi…
SuperCoolPencil Apr 16, 2026
76a94cb
feat: implement middle truncation for file info display and dynamic w…
SuperCoolPencil Apr 16, 2026
c4ba388
ai review code
SuperCoolPencil Apr 16, 2026
45174f8
refactor: adjust tab bar height, optimize download event handling, an…
SuperCoolPencil Apr 16, 2026
3c44eb8
refactor: remove redundant found flag in download start event handler
SuperCoolPencil Apr 16, 2026
1487645
refactor: centralize filepicker sizing logic and optimize dashboard l…
SuperCoolPencil Apr 16, 2026
7c5f6af
fix: adjust list row calculation to account for internal padding in s…
SuperCoolPencil Apr 16, 2026
01562c1
refactor: standardize TUI layout dimensions and offsets using central…
SuperCoolPencil Apr 16, 2026
6e81c63
lint
SuperCoolPencil Apr 16, 2026
917e4db
updatedemo.tape
SuperCoolPencil Apr 16, 2026
352b4e4
fix: adjust input width calculation in add download modal and update …
SuperCoolPencil Apr 16, 2026
9545448
chore: update demo assets and record new terminal session with zsh shell
SuperCoolPencil Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified assets/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/demo.mp4
Binary file not shown.
18 changes: 9 additions & 9 deletions assets/demo.tape
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
Output assets/demo.gif
Output assets/demo.mp4

Set Shell "bash"
Set Shell "zsh"
Set FontSize 16
Set Width 1920
Set Height 1080
Set FontFamily "MesloLGS NF"
Set Theme { "name": "IBM 3270 High Contrast", "black": "#000000", "red": "#ff5454", "green": "#00ff00", "yellow": "#ffff00", "blue": "#00aaff", "magenta": "#ff00ff", "cyan": "#00ffff", "white": "#ffffff", "brightBlack": "#666666", "brightRed": "#ff6666", "brightGreen": "#33ff33", "brightYellow": "#ffff66", "brightBlue": "#33ccff", "brightMagenta": "#ff66ff", "brightCyan": "#66ffff", "brightWhite": "#ffffff", "background": "#000000", "foreground": "#00ff00", "selection": "#222222", "cursor": "#00ff00" }
Set FontFamily "JetBrainsMono NF"
# Set Theme { "name": "IBM 3270 High Contrast", "black": "#000000", "red": "#ff5454", "green": "#00ff00", "yellow": "#ffff00", "blue": "#00aaff", "magenta": "#ff00ff", "cyan": "#00ffff", "white": "#ffffff", "brightBlack": "#666666", "brightRed": "#ff6666", "brightGreen": "#33ff33", "brightYellow": "#ffff66", "brightBlue": "#33ccff", "brightMagenta": "#ff66ff", "brightCyan": "#66ffff", "brightWhite": "#ffffff", "background": "#000000", "foreground": "#00ff00", "selection": "#222222", "cursor": "#00ff00" }
Set Padding 0
Set WindowBar Colorful
Set Framerate 60

# --- Launch Surge ---
Type "surge"
Type "./Surge"
Enter
Sleep 3s

Expand All @@ -24,20 +24,16 @@ Sleep 3s
Type "a"
Sleep 500ms

# Type a sample URL (use a small, fast file for demo)
Type "https://ash-speed.hetzner.com/100MB.bin"
Type "https://sin-speed.hetzner.com/1GB.bin"
Enter
Sleep 300ms

Type "https://sin-speed.hetzner.com/100MB.bin"
Enter
Sleep 300ms

# Press Enter to skip path (use default)
Enter
Sleep 300ms

# Press Enter to skip filename (use default)
Enter
Sleep 5s

Expand All @@ -56,6 +52,10 @@ Sleep 800ms
Screenshot assets/settings.png
Right
Sleep 800ms
Right
Sleep 800ms
Right
Sleep 800ms

# Close settings (Esc)
Escape
Expand Down
Binary file modified assets/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ func TestRootCmd_Version(t *testing.T) {

func TestRootCmd_VersionMatchesPackageVar(t *testing.T) {
if rootCmd.Version != Version {
t.Errorf("rootCmd.Version %q does not match Version %q init() must sync them", rootCmd.Version, Version)
t.Errorf("rootCmd.Version %q does not match Version %q \u2014 init() must sync them", rootCmd.Version, Version)
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ func LoadSettings() (*Settings, error) {

settings := DefaultSettings() // Start with defaults to fill any missing fields
if err := json.Unmarshal(data, settings); err != nil {
utils.Debug("Warning: corrupt settings file %s: %v using defaults", path, err)
utils.Debug("Warning: corrupt settings file %s: %v \u2014 using defaults", path, err)
return DefaultSettings(), nil
}

Expand Down
2 changes: 1 addition & 1 deletion internal/download/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ func TestWorkerPool_GracefulShutdown_WorkerSkipsQueuedAfterShutdown(t *testing.T
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("GracefulShutdown timed out worker may have started a download it should have skipped")
t.Fatal("GracefulShutdown timed out \u2014 worker may have started a download it should have skipped")
}

// The queued map must be empty.
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/colors/colors.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ var (

// === Progress Bar Colors ===
var (
ProgressStart color.Color = themeColor{light: "#d10074", dark: "#ff79c6"} // Pink
ProgressEnd color.Color = themeColor{light: "#7b1fa2", dark: "#bd93f9"} // Purple
ProgressStart color.Color = themeColor{light: "#950053", dark: "#fa70bc"} // Muted Pink
ProgressEnd color.Color = themeColor{light: "#5a1376", dark: "#b472ff"} // Muted Purple
)

var (
Expand Down
17 changes: 16 additions & 1 deletion internal/tui/components/add_download_modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,22 @@ func (m AddDownloadModal) View() string {
}

for i := 0; i < len(m.Inputs) && i < len(m.Labels); i++ {
row := lipgloss.JoinHorizontal(lipgloss.Left, labelStyle.Render(m.Labels[i]), m.Inputs[i].View())
// Calculate available width for inputs
// Accounts for: horizontal padding (4), label (10), and box borders (2)
const labelWidth = 10
horizontalPadding := lipgloss.NewStyle().Padding(0, 2).GetHorizontalFrameSize()
inputW := m.Width - BorderFrameWidth - horizontalPadding - labelWidth

if m.BrowseHintIndex == i {
inputW -= 13 // Margin (1) + "[Tab] Browse" (12)
}
if inputW < 10 {
inputW = 10
}
ti := m.Inputs[i]
ti.SetWidth(inputW)

row := lipgloss.JoinHorizontal(lipgloss.Left, labelStyle.Render(m.Labels[i]), ti.View())
if m.BrowseHintIndex == i {
hintStyle := hintBase
if m.FocusedInput == i {
Expand Down
27 changes: 19 additions & 8 deletions internal/tui/components/box.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import (
"charm.land/lipgloss/v2"
)

const (
// BorderFrameHeight is the combined height of top and bottom borders (2)
BorderFrameHeight = 2
// BorderFrameWidth is the combined width of left and right borders (2)
BorderFrameWidth = 2
// BtopBoxOverheadHeight is the header + footer overhead (2)
BtopBoxOverheadHeight = 2
// SingleLineHeight is a standard single line height (1)
SingleLineHeight = 1
)

// BoxRenderer is the function signature for rendering btop-style boxes
type BoxRenderer func(leftTitle, rightTitle, content string, width, height int, borderColor color.Color) string

Expand All @@ -19,14 +30,14 @@ type BoxRenderer func(leftTitle, rightTitle, content string, width, height int,
func RenderBtopBox(leftTitle, rightTitle string, content string, width, height int, borderColor color.Color) string {
// Border characters
const (
topLeft = ""
topRight = ""
bottomLeft = ""
bottomRight = ""
horizontal = ""
vertical = ""
topLeft = "\u256d"
topRight = "\u256e"
bottomLeft = "\u2570"
bottomRight = "\u256f"
horizontal = "\u2500"
vertical = "\u2502"
)
innerWidth := width - 2
innerWidth := width - BorderFrameWidth
if innerWidth < 1 {
innerWidth = 1
}
Expand Down Expand Up @@ -90,7 +101,7 @@ func RenderBtopBox(leftTitle, rightTitle string, content string, width, height i

// Wrap content lines with vertical borders
contentLines := strings.Split(content, "\n")
innerHeight := height - 2 // Account for top and bottom borders
innerHeight := height - BorderFrameHeight // Account for top and bottom borders

// Style for truncation
truncStyle := lipgloss.NewStyle().MaxWidth(innerWidth)
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/components/chunkmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ func (m ChunkMapModel) View() string {
pausedStyle := lipgloss.NewStyle().Foreground(colors.StatePaused) // Yellow/Gold for paused Partial
completedStyle := lipgloss.NewStyle().Foreground(colors.StateDownloading) // Neon Green / Cyan

block := ""
block := "\u25a0"

for i, status := range visualChunks {
if i > 0 && i%cols == 0 {
Expand Down
14 changes: 7 additions & 7 deletions internal/tui/components/chunkmap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestChunkMap_Basic(t *testing.T) {
out := model.View()

// Just verify connection mostly.
if !strings.Contains(out, "") {
if !strings.Contains(out, "\u25a0") {
t.Error("Output should contain blocks")
}
}
Expand Down Expand Up @@ -131,7 +131,7 @@ func TestChunkMap_LogicVerify(t *testing.T) {
out := model.View()

pinkStyle := lipgloss.NewStyle().Foreground(colors.NeonPink)
if strings.Contains(out, pinkStyle.Render("")) {
if strings.Contains(out, pinkStyle.Render("\u25a0")) {
t.Error("Mixed state (Completed+Pending) should NOT render as Downloading (Pink)")
}
}
Expand All @@ -153,7 +153,7 @@ func TestChunkMap_DownloadingPriority(t *testing.T) {

// Dynamic check to avoid hardcoded color codes
pinkStyle := lipgloss.NewStyle().Foreground(colors.NeonPink)
expectedPink := pinkStyle.Render("")
expectedPink := pinkStyle.Render("\u25a0")

if !strings.Contains(out, expectedPink) {
t.Errorf("Block containing a Downloading chunk with bytes SHOULD render as Downloading")
Expand Down Expand Up @@ -199,10 +199,10 @@ func TestChunkMap_GranularProgress(t *testing.T) {
}

pinkStyle := lipgloss.NewStyle().Foreground(colors.NeonPink)
pinkBlock := pinkStyle.Render("")
pinkBlock := pinkStyle.Render("\u25a0")

pendingStyle := lipgloss.NewStyle().Foreground(colors.DarkGray)
grayBlock := pendingStyle.Render("")
grayBlock := pendingStyle.Render("\u25a0")

// Row 0 should be Pink
if !strings.Contains(rows[0], pinkBlock) {
Expand Down Expand Up @@ -235,9 +235,9 @@ func TestChunkMap_BlockTurnsCompletedWhenItsByteRangeIsFullyDownloaded(t *testin
}

completedStyle := lipgloss.NewStyle().Foreground(colors.StateDownloading)
completedBlock := completedStyle.Render("")
completedBlock := completedStyle.Render("\u25a0")
downloadingStyle := lipgloss.NewStyle().Foreground(colors.NeonPink)
downloadingBlock := downloadingStyle.Render("")
downloadingBlock := downloadingStyle.Render("\u25a0")

if !strings.Contains(rows[0], completedBlock) {
t.Errorf("Row 0 should be Completed (green/cyan) when fully covered by downloaded bytes")
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/components/filepicker_modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// FilePickerModal represents a styled file picker modal
type FilePickerModal struct {
Title string
Picker filepicker.Model
Picker *filepicker.Model
Help help.Model
HelpKeys help.KeyMap
BorderColor color.Color
Expand All @@ -22,7 +22,7 @@ type FilePickerModal struct {
}

// NewFilePickerModal creates a file picker modal with default styling
func NewFilePickerModal(title string, picker filepicker.Model, helpModel help.Model, helpKeys help.KeyMap, borderColor color.Color) FilePickerModal {
func NewFilePickerModal(title string, picker *filepicker.Model, helpModel help.Model, helpKeys help.KeyMap, borderColor color.Color) FilePickerModal {
return FilePickerModal{
Title: title,
Picker: picker,
Expand Down
10 changes: 5 additions & 5 deletions internal/tui/components/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ type statusInfo struct {
}

var statusMap = map[DownloadStatus]statusInfo{
StatusQueued: {icon: "", label: "Queued"},
StatusDownloading: {icon: "", label: "Downloading"},
StatusPaused: {icon: "", label: "Paused"},
StatusComplete: {icon: "", label: "Completed"},
StatusError: {icon: "", label: "Error"},
StatusQueued: {icon: "\u22ef", label: "Queued"},
StatusDownloading: {icon: "\u2b07", label: "Downloading"},
StatusPaused: {icon: "\u23f8", label: "Paused"},
StatusComplete: {icon: "\u2714", label: "Completed"},
StatusError: {icon: "\u2716", label: "Error"},
}

var (
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/components/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestStatusRender_ReflectsThemeChanges(t *testing.T) {
}

func TestStatusRenderWithSpinner(t *testing.T) {
spinnerFrame := ""
spinnerFrame := "\u280b"

queuedStr := StatusQueued.RenderWithSpinner(spinnerFrame)
if !strings.Contains(queuedStr, spinnerFrame+" Queued") {
Expand Down
19 changes: 12 additions & 7 deletions internal/tui/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const (

MinRightColumnWidth = 50 // Hide right column if narrow
MinGraphStatsWidth = 70 // Hide inline graph stats if narrow
MinLogoWidth = 60 // Hide ASCII logo if narrow
MinLogoWidth = 40 // Hide ASCII logo if narrow

MinGraphHeight = 9
MinGraphHeightShort = 5
Expand All @@ -44,15 +44,20 @@ const (
CardHeight = 2 // Compact rows for downloads list

// === Padding and Offsets ===
DefaultPaddingX = 1
DefaultPaddingY = 0
PopupPaddingX = 2
PopupPaddingY = 1
PopupWidth = 70 // Consistent width for small popup dialogs

DefaultPaddingX = 1
DefaultPaddingY = 0
PopupPaddingX = 2
PopupPaddingY = 1
PopupWidth = 70 // Consistent width for small popup dialogs
HeaderWidthOffset = 2
ProgressBarWidthOffset = 4

// === Layout Offsets (Clean Math) ===
InternalPaddingHeight = 2 // Standard internal vertical padding
InternalPaddingWidth = 2 // Standard internal horizontal padding
FooterHeight = 1 // Application-wide footer height (keybindings)
DividerHeight = 1 // Horizontal/Vertical divider line

// === Graph Configuration ===
GraphAxisWidth = 10
GraphStatsWidth = 18
Expand Down
14 changes: 7 additions & 7 deletions internal/tui/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,17 @@ func renderMultiLineGraph(data []float64, width, height int, maxVal float64, sta
for j := range rows[i] {
if i == height-1 {
// Bottom row: solid baseline
rows[i][j] = gridStyle.Render("")
rows[i][j] = gridStyle.Render("\u2500")
} else if i%2 == 0 {
rows[i][j] = gridStyle.Render("")
rows[i][j] = gridStyle.Render("\u254c")
} else {
rows[i][j] = " "
}
}
}

// Block characters for partial fills
blocks := []string{" ", "", "", "", "", "", "", ""}
blocks := []string{" ", "\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2588"}

// Pre-calculate styles for every row to avoid re-creating them in the loop
// Optimization: Pre-render all possible block characters for each row style
Expand Down Expand Up @@ -156,18 +156,18 @@ func overlayStatsBox(graph string, stats *GraphStats, width, height int) string
statsLines := []string{
headerStyle.Render("download"),
fmt.Sprintf("%s %s %s",
valueStyle.Render(""),
valueStyle.Render("\u25bc"),
valueStyle.Render(fmt.Sprintf("%.2f MB/s", stats.DownloadSpeed)),
dimStyle.Render(fmt.Sprintf("(%.0f Mbps)", speedMbps)),
),
fmt.Sprintf("%s %s %s %s",
labelStyle.Render(""),
labelStyle.Render("\u25bc"),
labelStyle.Render("Top:"),
valueStyle.Render(fmt.Sprintf("%.2f MB/s", stats.DownloadTop)),
dimStyle.Render(fmt.Sprintf("(%.0f Mbps)", topMbps)),
),
fmt.Sprintf("%s %s %s",
labelStyle.Render(""),
labelStyle.Render("\u25bc"),
labelStyle.Render("Total:"),
valueStyle.Render(utils.ConvertBytesToHumanReadable(stats.DownloadTotal)),
),
Expand All @@ -189,7 +189,7 @@ func overlayStatsBox(graph string, stats *GraphStats, width, height int) string
graphLineWidth := lipgloss.Width(graphLines[i])
statsLineWidth := lipgloss.Width(statsBoxLines[i])

keepWidth := graphLineWidth - statsLineWidth - 1
keepWidth := graphLineWidth - statsLineWidth - DividerHeight
if keepWidth < 0 {
keepWidth = 0
}
Expand Down
Loading
Loading