Skip to content
This repository was archived by the owner on Dec 24, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 84 additions & 11 deletions cmd/impulse/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package main
import (
"errors"
"fmt"
"strconv"

"github.com/charmbracelet/lipgloss"
"github.com/jesseward/impulse/internal/ui"
"github.com/jesseward/impulse/pkg/module"
"github.com/urfave/cli/v2"
)
Expand All @@ -13,6 +16,14 @@ func infoAction(c *cli.Context) error {
return cli.Exit(errors.New("no file specified"), 1)
}
filePath := c.Args().Get(0)
// Apply the theme if specified, otherwise default (OceanSky) will be used
themeName := c.String("theme")
if themeName != "" {
if err := ui.SetTheme(themeName); err != nil {
return cli.Exit(fmt.Sprintf("Invalid theme: %v", err), 1)
}
}

module, err := loadModule(filePath)
if err != nil {
return cli.Exit(err.Error(), 1)
Expand All @@ -23,16 +34,78 @@ func infoAction(c *cli.Context) error {
}

func printModuleInfo(module module.Module) {
fmt.Printf("Successfully parsed file: %s\n", module.Name())
fmt.Printf("Module Type: %s\n", module.Type())
fmt.Printf("Song Length: %d\n", module.SongLength())
fmt.Printf("Song BPM: %d\n", module.DefaultBPM())
fmt.Printf("Song Speed: %d\n", module.DefaultSpeed())

fmt.Printf("Number of channels: %d\n", module.NumChannels())
fmt.Printf("Number of patterns: %d\n", module.NumPatterns())
fmt.Println("Samples:")
for i, sample := range module.Samples() {
fmt.Printf("Sample %d: %s\n", i+1, sample.Name())
const keyWidth = 15

// Helper to render a key-value pair
renderKV := func(key, value string) string {
k := ui.LabelStyle.Copy().Width(keyWidth).Render(key)
sep := ui.SeparatorStyle.Render(" │ ")
v := ui.ValueStyle.Render(value)
return lipgloss.JoinHorizontal(lipgloss.Left, k, sep, v)
}

title := ui.TitleStyle.Render(" Module Information ")

metadata := []string{
renderKV("Filename", module.Name()),
renderKV("Type", module.Type()),
renderKV("Song Length", strconv.Itoa(module.SongLength())),
renderKV("BPM", strconv.Itoa(module.DefaultBPM())),
renderKV("Speed", strconv.Itoa(module.DefaultSpeed())),
renderKV("Channels", strconv.Itoa(module.NumChannels())),
renderKV("Patterns", strconv.Itoa(module.NumPatterns())),
}
metaBlock := lipgloss.JoinVertical(lipgloss.Left, metadata...)
metaBox := lipgloss.NewStyle().
Border(ui.CurrentTheme.AppBorder, true).
Inherit(ui.BorderColorStyle).
Padding(0, 1).
Render(metaBlock)

samples := module.Samples()
numSamples := len(samples)
midpoint := (numSamples + 1) / 2

var leftColRows, rightColRows []string

for i, sample := range samples {
// Format: "01: SampleName"
// Distinct coloring: Index (LabelStyle) : (SeparatorStyle) Name (ValueStyle)
indexStr := ui.LabelStyle.Render(fmt.Sprintf("%02d", i+1))
sepStr := ui.SeparatorStyle.Render(":")
nameStr := ui.ValueStyle.Render(" " + sample.Name())

renderedRow := lipgloss.JoinHorizontal(lipgloss.Left, indexStr, sepStr, nameStr)

if i < midpoint {
leftColRows = append(leftColRows, renderedRow)
} else {
rightColRows = append(rightColRows, renderedRow)
}
}

leftBlock := lipgloss.JoinVertical(lipgloss.Left, leftColRows...)
rightBlock := lipgloss.JoinVertical(lipgloss.Left, rightColRows...)

// Join columns with a gap (e.g., 4 spaces)
sampleBlock := lipgloss.JoinHorizontal(lipgloss.Top,
leftBlock,
lipgloss.NewStyle().Width(4).Render(""),
rightBlock,
)

sampleHeader := ui.LabelStyle.Render("Samples:")
sampleBox := lipgloss.NewStyle().
Border(ui.CurrentTheme.AppBorder, true).
Inherit(ui.BorderColorStyle).
Padding(0, 1).
Render(lipgloss.JoinVertical(lipgloss.Left, sampleHeader, "", sampleBlock))

output := lipgloss.JoinVertical(lipgloss.Left,
title,
metaBox,
sampleBox,
)

fmt.Println(output)
}
6 changes: 6 additions & 0 deletions cmd/impulse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ func main() {
Name: "v2",
Usage: "start the new v2 terminal UI",
},
&cli.StringFlag{
Name: "theme",
Aliases: []string{"t"},
Value: "ocean",
Usage: "UI theme (ocean, sunset, nature, gray)",
},
},
},
{
Expand Down
5 changes: 5 additions & 0 deletions cmd/impulse/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
func playAction(c *cli.Context) error {
filePath := c.String("file")
startUI := c.Bool("ui")
themeName := c.String("theme")

logFile, err := os.OpenFile("impulse.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
Expand All @@ -25,6 +26,10 @@ func playAction(c *cli.Context) error {
defer logFile.Close()
log.SetOutput(logFile)

if err := ui.SetTheme(themeName); err != nil {
return cli.Exit(fmt.Sprintf("Invalid theme: %v", err), 1)
}

var m module.Module
m, err = loadModule(filePath)
if err != nil {
Expand Down
4 changes: 0 additions & 4 deletions internal/player/protracker_ticker.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ var periodTable = [16 * 36]uint16{
862, 814, 768, 725, 684, 646, 610, 575, 543, 513, 484, 457, 431, 407, 384, 363, 342, 323, 305, 288, 272, 256, 242, 228, 216, 203, 192, 181, 171, 161, 152, 144, 136, 128, 121, 114,
}



type ProtrackerTicker struct{}

func (t *ProtrackerTicker) ProcessTick(p *Player, playerState *playerState, channelState *channelState, cell *module.Cell, speed, bpm, nextRow, nextOrder, currentOrder *int, tick int) {
Expand Down Expand Up @@ -214,8 +212,6 @@ func (t *ProtrackerTicker) handleExtendedEffect(state *channelState, command, va
}
}



func (t *ProtrackerTicker) GetPeriod(period uint16, offset int) uint16 {
finetune := 0
note := 0
Expand Down
6 changes: 0 additions & 6 deletions internal/player/s3m_ticker.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,6 @@ func (t *S3MTicker) handleEffect(p *Player, state *channelState, cell *module.Ce
}
}





func (t *S3MTicker) tonePortamento(state *channelState, param byte) {
if param > 0 {
state.portaSpeed = uint16(param) * 4
Expand Down Expand Up @@ -233,8 +229,6 @@ func (t *S3MTicker) specialEffect(state *channelState, param byte, nextRow *int,
}
}



func (t *S3MTicker) GetPeriod(period uint16, offset int) uint16 {
// this is not correct.
// find the note from the period
Expand Down
9 changes: 4 additions & 5 deletions internal/ui/footer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ func (m *footerModel) SetWidth(width int) {

func (m footerModel) View() string {
style := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFC0")).
Border(lipgloss.NormalBorder(), true).
Inherit(borderColorStyle).
Faint(true).
Foreground(CurrentTheme.Primary).
Border(CurrentTheme.AppBorder, true).
Inherit(BorderColorStyle).
Width(m.width).
Align(lipgloss.Center)

text := "'tab' toggle pattern or sample view | 'spacebar' Start/Stop Song | 'q' Quit"
text := "'tab' toggle pattern or sample view 'spacebar' Start/Stop Song 'q' Quit"
return style.Render(text)
}
29 changes: 9 additions & 20 deletions internal/ui/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@ const (
keyWidth = 12
)

var (
headerStyle = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), true).
Inherit(borderColorStyle).
Padding(0, 1)
labelStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("27"))
valueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("15"))
separatorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("238"))
)

type headerModel struct {
module module.Module
width int
Expand Down Expand Up @@ -69,35 +59,34 @@ func (m headerModel) View() string {
{"Position", strconv.Itoa(m.state.Order), "Time", fmt.Sprintf("%02d:%02d", m.duration/60, m.duration%60), "Speed", strconv.Itoa(m.speed)},
}

contentWidth := m.width - headerStyle.GetHorizontalFrameSize() - 3
contentWidth := m.width - HeaderStyle.GetHorizontalFrameSize() - 3

columnWidth := contentWidth / 3
lastColumnWidth := contentWidth - (columnWidth * 2)

var rows []string
for _, r := range data {
// --- ✨ FIX: Build each part of the column separately ---
key1 := labelStyle.Copy().Width(keyWidth).Render(r.left)
sep := separatorStyle.Render(" | ")
val1 := valueStyle.Render(r.lvalue)
key1 := LabelStyle.Copy().Width(keyWidth).Render(r.left)
sep := SeparatorStyle.Render(" │ ")
val1 := ValueStyle.Render(r.lvalue)
// Join the parts to create the aligned content for the column.
col1Content := lipgloss.JoinHorizontal(lipgloss.Left, key1, sep, val1)
// Render the full column with its calculated width.
col1 := lipgloss.NewStyle().Width(columnWidth).Render(col1Content)

key2 := labelStyle.Copy().Width(keyWidth).Render(r.center)
val2 := valueStyle.Render(r.cvalue)
key2 := LabelStyle.Copy().Width(keyWidth).Render(r.center)
val2 := ValueStyle.Render(r.cvalue)
col2Content := lipgloss.JoinHorizontal(lipgloss.Left, key2, sep, val2)
col2 := lipgloss.NewStyle().Width(columnWidth).Render(col2Content)

key3 := labelStyle.Copy().Width(keyWidth).Render(r.right)
val3 := valueStyle.Render(r.rvalue)
key3 := LabelStyle.Copy().Width(keyWidth).Render(r.right)
val3 := ValueStyle.Render(r.rvalue)
col3Content := lipgloss.JoinHorizontal(lipgloss.Left, key3, sep, val3)
col3 := lipgloss.NewStyle().Width(lastColumnWidth).Render(col3Content)

rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, col1, col2, col3))
}

content := lipgloss.JoinHorizontal(lipgloss.Top, rows...)
return headerStyle.Width(m.width - 2).Render(content)
return HeaderStyle.Width(m.width - 2).Render(content)
}
8 changes: 4 additions & 4 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ func (m model) View() string {
var footerView string
if m.flashMessage != "" {
style := lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFFFF")).
Background(lipgloss.Color("200")).
Foreground(CurrentTheme.ActiveRowFg).
Background(CurrentTheme.ActiveRowBg).
Padding(0, 1)
footerView = style.Render(m.flashMessage)
} else {
Expand All @@ -219,8 +219,8 @@ func (m model) View() string {

if m.activeView == showQuitConfirmation {
dialogBox := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
Inherit(borderColorStyle).
Border(CurrentTheme.AppBorder).
Inherit(BorderColorStyle).
Padding(1, 0).
BorderTop(true).
BorderLeft(true).
Expand Down
14 changes: 7 additions & 7 deletions internal/ui/sampler.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ func newSamplerModel(m module.Module) samplerModel {

s := table.DefaultStyles()
s.Header = s.Header.
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("240")).
BorderStyle(CurrentTheme.HeaderBorder).
BorderForeground(CurrentTheme.Border).
BorderBottom(true).
Bold(false)
Bold(true)
s.Selected = s.Selected.
Foreground(lipgloss.Color("15")).
Background(lipgloss.Color("27")).
Foreground(CurrentTheme.ActiveRowFg).
Background(CurrentTheme.ActiveRowBg).
Bold(false)
t.SetStyles(s)

Expand All @@ -80,8 +80,8 @@ func (m samplerModel) Update(msg tea.Msg) (samplerModel, tea.Cmd) {
func (m samplerModel) View() string {

style := lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), true).
Inherit(borderColorStyle).
Border(CurrentTheme.AppBorder, true).
Inherit(BorderColorStyle).
Width(m.width - 2).
Height(m.height)
return style.Render(m.table.View())
Expand Down
52 changes: 52 additions & 0 deletions internal/ui/styles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ui

import "github.com/charmbracelet/lipgloss"

var (
// Global styles
TitleStyle lipgloss.Style
BorderColorStyle lipgloss.Style

// Header styles
HeaderStyle lipgloss.Style
LabelStyle lipgloss.Style
ValueStyle lipgloss.Style
SeparatorStyle lipgloss.Style

// Tracker styles
NoteStyle lipgloss.Style
InstrumentStyle lipgloss.Style
EffectStyle lipgloss.Style
)

func init() {
updateStyles()
}

func updateStyles() {
// Global
TitleStyle = lipgloss.NewStyle().
Background(CurrentTheme.ActiveRowBg).
Foreground(CurrentTheme.ActiveRowFg).
Bold(true).
Padding(0, 1)

BorderColorStyle = lipgloss.NewStyle().
BorderForeground(CurrentTheme.Border).
BorderStyle(CurrentTheme.AppBorder)

// Header
HeaderStyle = lipgloss.NewStyle().
Border(CurrentTheme.HeaderBorder, true).
Inherit(BorderColorStyle).
Padding(0, 1)

LabelStyle = lipgloss.NewStyle().Bold(true).Foreground(CurrentTheme.Label)
ValueStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Value)
SeparatorStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Separator)

// Tracker
NoteStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Note)
InstrumentStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Instrument)
EffectStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Effect)
}
Loading