Skip to content
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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ endif
# dependency installation on Linux.
lk$(EXE):
git submodule update --init --recursive
CGO_ENABLED=1 go build -o lk$(EXE) ./cmd/lk
CGO_ENABLED=1 go build -o ./bin/lk$(EXE) ./cmd/lk

install: lk$(EXE)
cp lk$(EXE) "$(GOBIN)/lk$(EXE)"
cp ./bin/lk$(EXE) "$(GOBIN)/lk$(EXE)"
ln -sf "$(GOBIN)/lk$(EXE)" "$(GOBIN)/livekit-cli$(EXE)"
4 changes: 3 additions & 1 deletion autocomplete/fish_autocomplete
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

function __fish_lk_no_subcommand --description 'Test if there has been any subcommand yet'
for i in (commandline -opc)
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
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
return 1
end
end
Expand Down Expand Up @@ -267,6 +267,8 @@ complete -x -c lk -n '__fish_seen_subcommand_from project; and not __fish_seen_s
complete -c lk -n '__fish_seen_subcommand_from project; and __fish_seen_subcommand_from set-default' -f -l help -s h -d 'show help'
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'
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'
complete -c lk -n '__fish_seen_subcommand_from set-theme' -f -l help -s h -d 'show help'
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'
complete -x -c lk -n '__fish_lk_no_subcommand' -a 'room' -d 'Create or delete rooms and manage existing room properties'
complete -c lk -n '__fish_seen_subcommand_from room' -f -l help -s h -d 'show help'
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'
Expand Down
5 changes: 4 additions & 1 deletion cmd/lk/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ func requireProjectWithOpts(ctx context.Context, cmd *cli.Command, opts ...loadO
cliConfig != nil && len(cliConfig.Projects) > 1 {
useDefault := true
if err = huh.NewForm(huh.NewGroup(util.Confirm().
Title(fmt.Sprintf("Use project [%s] (%s)?", rp.project.Name, rp.project.URL)).
Title(fmt.Sprintf("Use project [%s]?", rp.project.Name)).
Description(rp.project.URL).
Value(&useDefault).
Options(
huh.NewOption("Yes", true),
Expand Down Expand Up @@ -360,6 +361,7 @@ func setupTemplate(ctx context.Context, cmd *cli.Command) error {
preinstallPrompts = append(preinstallPrompts, huh.NewInput().
Title("Application Name").
Placeholder("my-app").
Prompt("").
Value(&appName).
Validate(func(s string) error {
if len(s) < 2 {
Expand Down Expand Up @@ -551,6 +553,7 @@ func instantiateEnv(ctx context.Context, cmd *cli.Command, rootPath string, addl
EchoMode(huh.EchoModePassword).
Title("Enter " + key + "?").
Placeholder(oldValue).
Prompt("").
Value(&newValue).
WithTheme(util.Theme).
Run(); err != nil || newValue == "" {
Expand Down
2 changes: 2 additions & 0 deletions cmd/lk/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
// get devicename
if err := huh.NewForm(huh.NewGroup(huh.NewInput().
Title("What is the name of this device?").
Prompt("").
Value(&cliConfig.DeviceName).
WithTheme(util.Theme))).
Run(); err != nil {
Expand Down Expand Up @@ -328,6 +329,7 @@ func tryAuthIfNeeded(ctx context.Context, cmd *cli.Command) error {
if err := huh.NewInput().
Title("Choose a different alias").
Description(fmt.Sprintf("You've already authenticated a project with the alias %q.", name)).
Prompt("").
Value(&name).
Validate(func(s string) error {
if cliConfig.ProjectExists(s) {
Expand Down
5 changes: 3 additions & 2 deletions cmd/lk/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

"github.com/livekit/livekit-cli/v2/pkg/console"
"github.com/livekit/livekit-cli/v2/pkg/portaudio"
"github.com/livekit/livekit-cli/v2/pkg/util"
)

func init() {
Expand Down Expand Up @@ -240,8 +241,8 @@ func listDevices() error {
return err
}

headerStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))
defaultStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(util.Brand())
defaultStyle := lipgloss.NewStyle().Foreground(util.Success())

out.Result(headerStyle.Render(fmt.Sprintf(" %-4s %-8s %-45s %s", "#", "Type", "Name", "Default")))
out.Result(strings.Repeat("─", 70))
Expand Down
55 changes: 26 additions & 29 deletions cmd/lk/console_tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,18 @@ import (
agent "github.com/livekit/protocol/livekit/agent"

"github.com/livekit/livekit-cli/v2/pkg/console"
"github.com/livekit/livekit-cli/v2/pkg/util"
)

// Console-specific styles (tagStyle, greenStyle, redStyle, dimStyle, boldStyle, cyanStyle
// are inherited from simulate_tui.go which is always compiled)
var (
lkCyan = lipgloss.Color("#1fd5f9")
lkPurple = lipgloss.Color("#8f83ff")
lkGreen = lipgloss.Color("#6BCB77")
lkRed = lipgloss.Color("#EF4444")

labelStyle = lipgloss.NewStyle().Foreground(lkPurple)
cyanBoldStyle = lipgloss.NewStyle().Foreground(lkCyan).Bold(true)
greenBoldStyle = lipgloss.NewStyle().Foreground(lkGreen).Bold(true)
redBoldStyle = lipgloss.NewStyle().Foreground(lkRed).Bold(true)
)
// are inherited from simulate_tui.go which is always compiled). Colors are pulled from the
// active theme palette at render time, so they follow `lk set-theme`.
func labelStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(util.Accent()) }
func cyanBoldStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(util.Brand()).Bold(true) }
func greenBoldStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(util.Success()).Bold(true)
}
func redBoldStyle() lipgloss.Style { return lipgloss.NewStyle().Foreground(util.Error()).Bold(true) }

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

Expand Down Expand Up @@ -427,8 +424,8 @@ func (m *consoleModel) handleSessionEvent(ev *agent.AgentSessionEvent) []tea.Cmd
m.partialTranscript = ""
if text := e.UserInputTranscribed.Transcript; text != "" {
cmds = append(cmds, tea.Println(
"\n "+lipgloss.NewStyle().Foreground(lkCyan).Render("● ")+
cyanBoldStyle.Render("You")+
"\n "+lipgloss.NewStyle().Foreground(util.Brand()).Render("● ")+
cyanBoldStyle().Render("You")+
"\n "+text+"\n",
))
}
Expand Down Expand Up @@ -465,11 +462,11 @@ func (m *consoleModel) handleSessionEvent(ev *agent.AgentSessionEvent) []tea.Cmd
if fco, ok := outputsByCallID[fc.CallId]; ok {
if fco.IsError {
b.WriteString("\n ")
b.WriteString(redBoldStyle.Render("✗ "))
b.WriteString(redStyle.Render(truncateOutput(fco.Output)))
b.WriteString(redBoldStyle().Render("✗ "))
b.WriteString(redStyle().Render(truncateOutput(fco.Output)))
} else {
b.WriteString("\n ")
b.WriteString(greenStyle.Render("✓ "))
b.WriteString(greenStyle().Render("✓ "))
b.WriteString(dimStyle.Render(summarizeOutput(fco.Output)))
}
}
Expand All @@ -479,7 +476,7 @@ func (m *consoleModel) handleSessionEvent(ev *agent.AgentSessionEvent) []tea.Cmd

case *agent.AgentSessionEvent_Error_:
cmds = append(cmds, tea.Println(
" "+redBoldStyle.Render("✗ ")+redStyle.Render(e.Error.Message),
" "+redBoldStyle().Render("✗ ")+redStyle().Render(e.Error.Message),
))
}

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

var b strings.Builder
b.WriteString("\n ")
b.WriteString(lipgloss.NewStyle().Foreground(lkGreen).Render("● "))
b.WriteString(greenBoldStyle.Render("Agent"))
b.WriteString(lipgloss.NewStyle().Foreground(util.Success()).Render("● "))
b.WriteString(greenBoldStyle().Render("Agent"))
for tl := range strings.SplitSeq(text, "\n") {
b.WriteString("\n ")
b.WriteString(tl)
Expand All @@ -521,8 +518,8 @@ func formatChatItem(item *agent.ChatContext_ChatItem) string {
if h.OldAgentId != nil && *h.OldAgentId != "" {
old = dimStyle.Render(*h.OldAgentId) + " → "
}
return " " + lipgloss.NewStyle().Foreground(lkPurple).Render("● ") +
dimStyle.Render("handoff: ") + old + labelStyle.Render(h.NewAgentId)
return " " + lipgloss.NewStyle().Foreground(util.Accent()).Render("● ") +
dimStyle.Render("handoff: ") + old + labelStyle().Render(h.NewAgentId)
}
return ""
}
Expand All @@ -538,7 +535,7 @@ func (m consoleModel) View() string {

if m.shuttingDown {
b.WriteString("\n ")
b.WriteString(labelStyle.Render("Shutting down agent..."))
b.WriteString(labelStyle().Render("Shutting down agent..."))
b.WriteString(" ")
b.WriteString(dimStyle.Render("ctrl+C to force"))
b.WriteString("\n")
Expand Down Expand Up @@ -568,7 +565,7 @@ func (m consoleModel) View() string {
if m.audioError != "" {
b.WriteString("\n")
b.WriteString(" ")
b.WriteString(redStyle.Render("audio: " + m.audioError))
b.WriteString(redStyle().Render("audio: " + m.audioError))
}

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

if m.pipeline.Muted() {
b.WriteString(" ")
b.WriteString(redBoldStyle.Render("MUTED"))
b.WriteString(redBoldStyle().Render("MUTED"))
}

// Partial transcription on same line (dim)
Expand Down Expand Up @@ -677,7 +674,7 @@ func formatMetrics(m *agent.MetricsReport) string {
if m.E2ELatency != nil {
label := "e2e " + formatMs(*m.E2ELatency)
if *m.E2ELatency >= 1.0 {
parts = append(parts, redStyle.Render(label))
parts = append(parts, redStyle().Render(label))
} else {
parts = append(parts, dimStyle.Render(label))
}
Expand Down
12 changes: 11 additions & 1 deletion cmd/lk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
lksdk "github.com/livekit/server-sdk-go/v2"

livekitcli "github.com/livekit/livekit-cli/v2"
"github.com/livekit/livekit-cli/v2/pkg/config"
"github.com/livekit/livekit-cli/v2/pkg/util"
)

Expand Down Expand Up @@ -64,6 +65,7 @@ func main() {
app.Commands = append(app.Commands, CloudCommands...)
app.Commands = append(app.Commands, DocsCommands...)
app.Commands = append(app.Commands, ProjectCommands...)
app.Commands = append(app.Commands, ThemeCommands...)
app.Commands = append(app.Commands, RoomCommands...)
app.Commands = append(app.Commands, TokenCommands...)
app.Commands = append(app.Commands, JoinCommands...)
Expand Down Expand Up @@ -92,7 +94,7 @@ func main() {
checkForLegacyName()

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

// Apply the persisted color theme before any output/forms render. An empty value
// resolves to the default; an invalid stored value is reported and falls back.
if conf, err := config.LoadOrCreate(); err == nil {
if err := util.SetTheme(conf.Theme); err != nil {
out.Warnf("%v; using default theme", err)
}
}

return nil, nil
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/lk/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
prompts = append(prompts, huh.NewInput().
Title("Project Name").
Placeholder("my-project").
Prompt("").
Validate(validateName).
Value(&p.Name))
}
Expand All @@ -187,6 +188,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
prompts = append(prompts, huh.NewInput().
Title("Project URL").
Placeholder("wss://my-project.livekit.cloud").
Prompt("").
Validate(validateURL).
Value(&p.URL))
}
Expand All @@ -207,6 +209,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
prompts = append(prompts, huh.NewInput().
Title("API Key").
Placeholder("APIxxxxxxxxxxxx").
Prompt("").
Validate(validateKey).
Value(&p.APIKey))
}
Expand All @@ -221,6 +224,7 @@ func addProject(ctx context.Context, cmd *cli.Command) error {
prompts = append(prompts, huh.NewInput().
Title("API Secret").
Placeholder("****************************").
Prompt("").
Validate(validateKey).
Value(&p.APISecret))
}
Expand Down
22 changes: 16 additions & 6 deletions cmd/lk/simulate_matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"

"github.com/livekit/livekit-cli/v2/pkg/util"
)

const (
Expand All @@ -32,14 +34,22 @@ const (

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

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

// matrixCursorMarkerStyle uses the active theme's brand color so the cursor ties into the
// selected theme.
func matrixCursorMarkerStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(util.Brand()).Bold(true)
}

// matrixRow describes the underlying text layer for one row of the rain area.
// The renderer composites rain on top of this neutral description without
// needing to know anything about the upstream domain (jobs, IDs, etc.).
Expand Down Expand Up @@ -312,7 +322,7 @@ func writeMatrixRun(b *strings.Builder, cat int, rs []rune, iconStyle *lipgloss.
b.WriteString(s)
}
case mcCursor:
b.WriteString(matrixCursorMarkerStyle.Render(s))
b.WriteString(matrixCursorMarkerStyle().Render(s))
default:
b.WriteString(s)
}
Expand Down
Loading
Loading