Skip to content

Commit 7bc5508

Browse files
committed
feat(lk): better theme support
1 parent 1a500d8 commit 7bc5508

14 files changed

Lines changed: 346 additions & 130 deletions

File tree

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ endif
2222
# dependency installation on Linux.
2323
lk$(EXE):
2424
git submodule update --init --recursive
25-
CGO_ENABLED=1 go build -o lk$(EXE) ./cmd/lk
25+
CGO_ENABLED=1 go build -o ./bin/lk$(EXE) ./cmd/lk
2626

2727
install: lk$(EXE)
28-
cp lk$(EXE) "$(GOBIN)/lk$(EXE)"
28+
cp ./bin/lk$(EXE) "$(GOBIN)/lk$(EXE)"
2929
ln -sf "$(GOBIN)/lk$(EXE)" "$(GOBIN)/livekit-cli$(EXE)"

autocomplete/fish_autocomplete

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
function __fish_lk_no_subcommand --description 'Test if there has been any subcommand yet'
44
for i in (commandline -opc)
5-
if contains -- $i generate-fish-completion app agent a cloud docs project room create-room list-rooms list-room update-room-metadata list-participants get-participant remove-participant update-participant mute-track update-subscriptions send-data token create-token join-room dispatch egress start-room-composite-egress start-web-egress start-participant-egress start-track-composite-egress start-track-egress list-egress update-layout update-stream stop-egress test-egress-template ingress create-ingress update-ingress list-ingress delete-ingress sip list-sip-trunk delete-sip-trunk create-sip-dispatch-rule list-sip-dispatch-rule delete-sip-dispatch-rule create-sip-participant number replay perf load-test completion
5+
if contains -- $i generate-fish-completion app agent a cloud docs project set-theme room create-room list-rooms list-room update-room-metadata list-participants get-participant remove-participant update-participant mute-track update-subscriptions send-data token create-token join-room dispatch egress start-room-composite-egress start-web-egress start-participant-egress start-track-composite-egress start-track-egress list-egress update-layout update-stream stop-egress test-egress-template ingress create-ingress update-ingress list-ingress delete-ingress sip list-sip-trunk delete-sip-trunk create-sip-dispatch-rule list-sip-dispatch-rule delete-sip-dispatch-rule create-sip-participant number replay perf load-test completion
66
return 1
77
end
88
end
@@ -267,6 +267,8 @@ complete -x -c lk -n '__fish_seen_subcommand_from project; and not __fish_seen_s
267267
complete -c lk -n '__fish_seen_subcommand_from project; and __fish_seen_subcommand_from set-default' -f -l help -s h -d 'show help'
268268
complete -x -c lk -n '__fish_seen_subcommand_from project; and __fish_seen_subcommand_from set-default; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
269269
complete -x -c lk -n '__fish_seen_subcommand_from project; and not __fish_seen_subcommand_from add list remove set-default help h' -a 'help' -d 'Shows a list of commands or help for one command'
270+
complete -c lk -n '__fish_seen_subcommand_from set-theme' -f -l help -s h -d 'show help'
271+
complete -x -c lk -n '__fish_seen_subcommand_from set-theme; and not __fish_seen_subcommand_from help h' -a 'help' -d 'Shows a list of commands or help for one command'
270272
complete -x -c lk -n '__fish_lk_no_subcommand' -a 'room' -d 'Create or delete rooms and manage existing room properties'
271273
complete -c lk -n '__fish_seen_subcommand_from room' -f -l help -s h -d 'show help'
272274
complete -x -c lk -n '__fish_seen_subcommand_from room; and not __fish_seen_subcommand_from create list update delete join participants mute-track update-subscriptions send-data help h' -a 'create' -d 'Create a room'

cmd/lk/app.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ func requireProjectWithOpts(ctx context.Context, cmd *cli.Command, opts ...loadO
163163
cliConfig != nil && len(cliConfig.Projects) > 1 {
164164
useDefault := true
165165
if err = huh.NewForm(huh.NewGroup(util.Confirm().
166-
Title(fmt.Sprintf("Use project [%s] (%s)?", rp.project.Name, rp.project.URL)).
166+
Title(fmt.Sprintf("Use project [%s]?", rp.project.Name)).
167+
Description(rp.project.URL).
167168
Value(&useDefault).
168169
Options(
169170
huh.NewOption("Yes", true),
@@ -360,6 +361,7 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error {
360361
preinstallPrompts = append(preinstallPrompts, huh.NewInput().
361362
Title("Application Name").
362363
Placeholder("my-app").
364+
Prompt("").
363365
Value(&appName).
364366
Validate(func(s string) error {
365367
if len(s) < 2 {
@@ -551,6 +553,7 @@ func instantiateEnv(ctx context.Context, cmd *cli.Command, rootPath string, addl
551553
EchoMode(huh.EchoModePassword).
552554
Title("Enter " + key + "?").
553555
Placeholder(oldValue).
556+
Prompt("").
554557
Value(&newValue).
555558
WithTheme(util.Theme).
556559
Run(); err != nil || newValue == "" {

cmd/lk/cloud.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
259259
// get devicename
260260
if err := huh.NewForm(huh.NewGroup(huh.NewInput().
261261
Title("What is the name of this device?").
262+
Prompt("").
262263
Value(&cliConfig.DeviceName).
263264
WithTheme(util.Theme))).
264265
Run(); err != nil {
@@ -328,6 +329,7 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
328329
if err := huh.NewInput().
329330
Title("Choose a different alias").
330331
Description(fmt.Sprintf("You've already authenticated a project with the alias %q.", name)).
332+
Prompt("").
331333
Value(&name).
332334
Validate(func(s string) error {
333335
if cliConfig.ProjectExists(s) {

cmd/lk/console.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
"github.com/livekit/livekit-cli/v2/pkg/console"
3434
"github.com/livekit/livekit-cli/v2/pkg/portaudio"
35+
"github.com/livekit/livekit-cli/v2/pkg/util"
3536
)
3637

3738
func init() {
@@ -240,8 +241,8 @@ func listDevices() error {
240241
return err
241242
}
242243

243-
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))
244-
defaultStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
244+
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(util.Brand())
245+
defaultStyle := lipgloss.NewStyle().Foreground(util.Success())
245246

246247
out.Result(headerStyle.Render(fmt.Sprintf(" %-4s %-8s %-45s %s", "#", "Type", "Name", "Default")))
247248
out.Result(strings.Repeat("─", 70))

cmd/lk/console_tui.go

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,18 @@ import (
2929
agent "github.com/livekit/protocol/livekit/agent"
3030

3131
"github.com/livekit/livekit-cli/v2/pkg/console"
32+
"github.com/livekit/livekit-cli/v2/pkg/util"
3233
)
3334

3435
// Console-specific styles (tagStyle, greenStyle, redStyle, dimStyle, boldStyle, cyanStyle
35-
// are inherited from simulate_tui.go which is always compiled)
36-
var (
37-
lkCyan = lipgloss.Color("#1fd5f9")
38-
lkPurple = lipgloss.Color("#8f83ff")
39-
lkGreen = lipgloss.Color("#6BCB77")
40-
lkRed = lipgloss.Color("#EF4444")
41-
42-
labelStyle = lipgloss.NewStyle().Foreground(lkPurple)
43-
cyanBoldStyle = lipgloss.NewStyle().Foreground(lkCyan).Bold(true)
44-
greenBoldStyle = lipgloss.NewStyle().Foreground(lkGreen).Bold(true)
45-
redBoldStyle = lipgloss.NewStyle().Foreground(lkRed).Bold(true)
46-
)
36+
// are inherited from simulate_tui.go which is always compiled). Colors are pulled from the
37+
// active theme palette at render time, so they follow `lk set-theme`.
38+
func labelStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(util.Accent()) }
39+
func cyanBoldStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(util.Brand()).Bold(true) }
40+
func greenBoldStyle() lipgloss.Style {
41+
return lipgloss.NewStyle().Foreground(util.Success()).Bold(true)
42+
}
43+
func redBoldStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(util.Error()).Bold(true) }
4744

4845
// Unicode block characters for frequency visualizer (matching Python console)
4946
var blocks = []string{"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"}
@@ -384,8 +381,8 @@ func (m *consoleModel) updateTextMode(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
384381
// ● You
385382
// text here
386383
printCmd := tea.Println(
387-
"\n " + lipgloss.NewStyle().Foreground(lkCyan).Render("● ") +
388-
cyanBoldStyle.Render("You") +
384+
"\n " + lipgloss.NewStyle().Foreground(util.Brand()).Render("● ") +
385+
cyanBoldStyle().Render("You") +
389386
"\n " + text + "\n",
390387
)
391388

@@ -427,8 +424,8 @@ func (m *consoleModel) handleSessionEvent(ev *agent.AgentSessionEvent) []tea.Cmd
427424
m.partialTranscript = ""
428425
if text := e.UserInputTranscribed.Transcript; text != "" {
429426
cmds = append(cmds, tea.Println(
430-
"\n "+lipgloss.NewStyle().Foreground(lkCyan).Render("● ")+
431-
cyanBoldStyle.Render("You")+
427+
"\n "+lipgloss.NewStyle().Foreground(util.Brand()).Render("● ")+
428+
cyanBoldStyle().Render("You")+
432429
"\n "+text+"\n",
433430
))
434431
}
@@ -465,11 +462,11 @@ func (m *consoleModel) handleSessionEvent(ev *agent.AgentSessionEvent) []tea.Cmd
465462
if fco, ok := outputsByCallID[fc.CallId]; ok {
466463
if fco.IsError {
467464
b.WriteString("\n ")
468-
b.WriteString(redBoldStyle.Render("✗ "))
469-
b.WriteString(redStyle.Render(truncateOutput(fco.Output)))
465+
b.WriteString(redBoldStyle().Render("✗ "))
466+
b.WriteString(redStyle().Render(truncateOutput(fco.Output)))
470467
} else {
471468
b.WriteString("\n ")
472-
b.WriteString(greenStyle.Render("✓ "))
469+
b.WriteString(greenStyle().Render("✓ "))
473470
b.WriteString(dimStyle.Render(summarizeOutput(fco.Output)))
474471
}
475472
}
@@ -479,7 +476,7 @@ func (m *consoleModel) handleSessionEvent(ev *agent.AgentSessionEvent) []tea.Cmd
479476

480477
case *agent.AgentSessionEvent_Error_:
481478
cmds = append(cmds, tea.Println(
482-
" "+redBoldStyle.Render("✗ ")+redStyle.Render(e.Error.Message),
479+
" "+redBoldStyle().Render("✗ ")+redStyle().Render(e.Error.Message),
483480
))
484481
}
485482

@@ -506,8 +503,8 @@ func formatChatItem(item *agent.ChatContext_ChatItem) string {
506503

507504
var b strings.Builder
508505
b.WriteString("\n ")
509-
b.WriteString(lipgloss.NewStyle().Foreground(lkGreen).Render("● "))
510-
b.WriteString(greenBoldStyle.Render("Agent"))
506+
b.WriteString(lipgloss.NewStyle().Foreground(util.Success()).Render("● "))
507+
b.WriteString(greenBoldStyle().Render("Agent"))
511508
for tl := range strings.SplitSeq(text, "\n") {
512509
b.WriteString("\n ")
513510
b.WriteString(tl)
@@ -521,8 +518,8 @@ func formatChatItem(item *agent.ChatContext_ChatItem) string {
521518
if h.OldAgentId != nil && *h.OldAgentId != "" {
522519
old = dimStyle.Render(*h.OldAgentId) + " → "
523520
}
524-
return " " + lipgloss.NewStyle().Foreground(lkPurple).Render("● ") +
525-
dimStyle.Render("handoff: ") + old + labelStyle.Render(h.NewAgentId)
521+
return " " + lipgloss.NewStyle().Foreground(util.Accent()).Render("● ") +
522+
dimStyle.Render("handoff: ") + old + labelStyle().Render(h.NewAgentId)
526523
}
527524
return ""
528525
}
@@ -538,7 +535,7 @@ func (m consoleModel) View() string {
538535

539536
if m.shuttingDown {
540537
b.WriteString("\n ")
541-
b.WriteString(labelStyle.Render("Shutting down agent..."))
538+
b.WriteString(labelStyle().Render("Shutting down agent..."))
542539
b.WriteString(" ")
543540
b.WriteString(dimStyle.Render("ctrl+C to force"))
544541
b.WriteString("\n")
@@ -568,7 +565,7 @@ func (m consoleModel) View() string {
568565
if m.audioError != "" {
569566
b.WriteString("\n")
570567
b.WriteString(" ")
571-
b.WriteString(redStyle.Render("audio: " + m.audioError))
568+
b.WriteString(redStyle().Render("audio: " + m.audioError))
572569
}
573570

574571
if m.showShortcuts {
@@ -584,7 +581,7 @@ func (m consoleModel) View() string {
584581
} else {
585582
// ── Audio visualizer (matching old Python FrequencyVisualizer) ──
586583
b.WriteString(" ")
587-
b.WriteString(labelStyle.Render(m.inputDev))
584+
b.WriteString(labelStyle().Render(m.inputDev))
588585
b.WriteString(" ")
589586
bands := m.pipeline.FFTBands()
590587
for _, band := range bands {
@@ -601,7 +598,7 @@ func (m consoleModel) View() string {
601598

602599
if m.pipeline.Muted() {
603600
b.WriteString(" ")
604-
b.WriteString(redBoldStyle.Render("MUTED"))
601+
b.WriteString(redBoldStyle().Render("MUTED"))
605602
}
606603

607604
// Partial transcription on same line (dim)
@@ -677,7 +674,7 @@ func formatMetrics(m *agent.MetricsReport) string {
677674
if m.E2ELatency != nil {
678675
label := "e2e " + formatMs(*m.E2ELatency)
679676
if *m.E2ELatency >= 1.0 {
680-
parts = append(parts, redStyle.Render(label))
677+
parts = append(parts, redStyle().Render(label))
681678
} else {
682679
parts = append(parts, dimStyle.Render(label))
683680
}

cmd/lk/main.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
lksdk "github.com/livekit/server-sdk-go/v2"
3030

3131
livekitcli "github.com/livekit/livekit-cli/v2"
32+
"github.com/livekit/livekit-cli/v2/pkg/config"
3233
"github.com/livekit/livekit-cli/v2/pkg/util"
3334
)
3435

@@ -64,6 +65,7 @@ func main() {
6465
app.Commands = append(app.Commands, CloudCommands...)
6566
app.Commands = append(app.Commands, DocsCommands...)
6667
app.Commands = append(app.Commands, ProjectCommands...)
68+
app.Commands = append(app.Commands, ThemeCommands...)
6769
app.Commands = append(app.Commands, RoomCommands...)
6870
app.Commands = append(app.Commands, TokenCommands...)
6971
app.Commands = append(app.Commands, JoinCommands...)
@@ -92,7 +94,7 @@ func main() {
9294
checkForLegacyName()
9395

9496
if err := app.Run(ctx, os.Args); err != nil {
95-
errStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
97+
errStyle := lipgloss.NewStyle().Foreground(util.Error())
9698
fmt.Fprintln(os.Stderr, errStyle.Render(err.Error()))
9799
os.Exit(1)
98100
}
@@ -131,6 +133,14 @@ func initLogger(ctx context.Context, cmd *cli.Command) (context.Context, error)
131133
// defaults them to os.Stdout / os.Stderr, but they're overridable in tests).
132134
out = util.NewPrinter(cmd.Root().Writer, cmd.Root().ErrWriter, cmd.Bool("quiet"))
133135

136+
// Apply the persisted color theme before any output/forms render. An empty value
137+
// resolves to the default; an invalid stored value is reported and falls back.
138+
if conf, err := config.LoadOrCreate(); err == nil {
139+
if err := util.SetTheme(conf.Theme); err != nil {
140+
out.Warnf("%v; using default theme", err)
141+
}
142+
}
143+
134144
return nil, nil
135145
}
136146

cmd/lk/project.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
166166
prompts = append(prompts, huh.NewInput().
167167
Title("Project Name").
168168
Placeholder("my-project").
169+
Prompt("").
169170
Validate(validateName).
170171
Value(&p.Name))
171172
}
@@ -187,6 +188,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
187188
prompts = append(prompts, huh.NewInput().
188189
Title("Project URL").
189190
Placeholder("wss://my-project.livekit.cloud").
191+
Prompt("").
190192
Validate(validateURL).
191193
Value(&p.URL))
192194
}
@@ -207,6 +209,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
207209
prompts = append(prompts, huh.NewInput().
208210
Title("API Key").
209211
Placeholder("APIxxxxxxxxxxxx").
212+
Prompt("").
210213
Validate(validateKey).
211214
Value(&p.APIKey))
212215
}
@@ -221,6 +224,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
221224
prompts = append(prompts, huh.NewInput().
222225
Title("API Secret").
223226
Placeholder("****************************").
227+
Prompt("").
224228
Validate(validateKey).
225229
Value(&p.APISecret))
226230
}

cmd/lk/simulate_matrix.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import (
2121

2222
tea "github.com/charmbracelet/bubbletea"
2323
"github.com/charmbracelet/lipgloss"
24+
25+
"github.com/livekit/livekit-cli/v2/pkg/util"
2426
)
2527

2628
const (
@@ -32,14 +34,22 @@ const (
3234

3335
var matrixCharset = []rune("ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホ0123456789")
3436

37+
// The "digital rain" head + green gradient is a deliberate standalone effect (a bright
38+
// leading glyph fading through three greens), not part of the semantic theme palette, so
39+
// it keeps its fixed shades regardless of theme.
3540
var (
36-
matrixHeadStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("231")).Bold(true)
37-
matrixTier1Style = lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Bold(true)
38-
matrixTier2Style = lipgloss.NewStyle().Foreground(lipgloss.Color("34"))
39-
matrixTier3Style = lipgloss.NewStyle().Foreground(lipgloss.Color("22"))
40-
matrixCursorMarkerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true)
41+
matrixHeadStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("231")).Bold(true)
42+
matrixTier1Style = lipgloss.NewStyle().Foreground(lipgloss.Color("46")).Bold(true)
43+
matrixTier2Style = lipgloss.NewStyle().Foreground(lipgloss.Color("34"))
44+
matrixTier3Style = lipgloss.NewStyle().Foreground(lipgloss.Color("22"))
4145
)
4246

47+
// matrixCursorMarkerStyle uses the active theme's brand color so the cursor ties into the
48+
// selected theme.
49+
func matrixCursorMarkerStyle() lipgloss.Style {
50+
return lipgloss.NewStyle().Foreground(util.Brand()).Bold(true)
51+
}
52+
4353
// matrixRow describes the underlying text layer for one row of the rain area.
4454
// The renderer composites rain on top of this neutral description without
4555
// needing to know anything about the upstream domain (jobs, IDs, etc.).
@@ -312,7 +322,7 @@ func writeMatrixRun(b *strings.Builder, cat int, rs []rune, iconStyle *lipgloss.
312322
b.WriteString(s)
313323
}
314324
case mcCursor:
315-
b.WriteString(matrixCursorMarkerStyle.Render(s))
325+
b.WriteString(matrixCursorMarkerStyle().Render(s))
316326
default:
317327
b.WriteString(s)
318328
}

0 commit comments

Comments
 (0)