Three-tab Bubble Tea TUI (Setup / Manage / Monitor) for setting up and running a headless Jellyfin server over SSH: - Setup: install/uninstall Jellyfin, add media drives (single or btrfs RAID pool), import an existing detached btrfs pool non-destructively, firewall, move Jellyfin storage onto a media drive - Boot-drive-safe drive classifier (walks LUKS/LVM/RAID to the physical disk; never offers a disk hosting /, /boot or swap) - Keep-existing-filesystem path previews on-disk content, skips starter folders when content exists, and grants Jellyfin recursive read access - Manage: copy from external drives, mount/eject, rename/delete - Monitor: per-core CPU, load/mem/uptime, btrfs error counters, SMART Single static binary; installs to /usr/local/bin so sudo can find it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
package tui
|
|
|
|
import (
|
|
"github.com/charmbracelet/lipgloss"
|
|
|
|
"tuistream/internal/theme"
|
|
)
|
|
|
|
// centered horizontally centres every line of a block within width w. Each
|
|
// line is centred independently.
|
|
func centered(s string, w int) string {
|
|
return lipgloss.NewStyle().Width(w).Align(lipgloss.Center).Render(s)
|
|
}
|
|
|
|
// centeredFrame renders content inside a bordered card with every line centred
|
|
// within the card interior.
|
|
func centeredFrame(content string) string {
|
|
return frameStyle.Align(lipgloss.Center).Render(content)
|
|
}
|
|
|
|
// centeredCard is centeredFrame's equivalent for the padded modal card style
|
|
// (confirms, pickers, progress splashes).
|
|
func centeredCard(content string) string {
|
|
return cardStyle.Align(lipgloss.Center).Render(content)
|
|
}
|
|
|
|
// Colours and styles, (re)built from the active Omarchy theme by applyTheme.
|
|
// Mirrors omarchy-send's style block so the two sibling apps look related.
|
|
var (
|
|
accent lipgloss.Color
|
|
text lipgloss.Color
|
|
dim lipgloss.Color
|
|
muted lipgloss.Color
|
|
good lipgloss.Color
|
|
bad lipgloss.Color
|
|
|
|
titleBarStyle lipgloss.Style
|
|
tabActiveStyle lipgloss.Style
|
|
tabInactiveStyle lipgloss.Style
|
|
frameStyle lipgloss.Style
|
|
cardStyle lipgloss.Style
|
|
footerStyle lipgloss.Style
|
|
titleStyle lipgloss.Style
|
|
headerStyle lipgloss.Style
|
|
labelStyle lipgloss.Style
|
|
valueStyle lipgloss.Style
|
|
|
|
roleSystemStyle lipgloss.Style // boot/OS disk — off-limits
|
|
roleAvailableStyle lipgloss.Style // ready to be picked
|
|
roleInUseStyle lipgloss.Style // already mounted somewhere
|
|
devNameStyle lipgloss.Style // /dev/... in inventory rows
|
|
)
|
|
|
|
func init() { applyTheme(theme.Default()) }
|
|
|
|
// cardWidth picks a consistent width for every card/frame the TUI draws.
|
|
// The cap (88) keeps content as a comfortably-centred column with balanced
|
|
// margins instead of filling a wide terminal edge-to-edge (which reads as
|
|
// left-bunched once the lines inside are left-aligned). The floor (40) keeps
|
|
// narrow terminals readable rather than broken. The -8 budget leaves at least
|
|
// a small margin on each side so the block always looks centred, never flush.
|
|
func cardWidth(termWidth int) int {
|
|
w := termWidth - 8
|
|
if w > 88 {
|
|
w = 88
|
|
}
|
|
if w < 40 {
|
|
w = 40
|
|
}
|
|
return w
|
|
}
|
|
|
|
// applyWidth rebuilds the width-sensitive style values for the current
|
|
// terminal width. Called once per render so all cards / frames have the
|
|
// same width, regardless of which renderer drew them.
|
|
func applyWidth(termWidth int) {
|
|
w := cardWidth(termWidth)
|
|
cardStyle = cardStyle.Width(w)
|
|
frameStyle = frameStyle.Width(w)
|
|
}
|
|
|
|
func applyTheme(t theme.Theme) {
|
|
accent = lipgloss.Color(t.Accent)
|
|
text = lipgloss.Color(t.Fg)
|
|
bg := lipgloss.Color(t.Bg)
|
|
dim = lipgloss.Color(t.Dim)
|
|
muted = lipgloss.Color(t.Muted)
|
|
good = lipgloss.Color(t.Good)
|
|
bad = lipgloss.Color(t.Bad)
|
|
|
|
titleBarStyle = lipgloss.NewStyle().Bold(true).Foreground(bg).Background(accent)
|
|
tabActiveStyle = lipgloss.NewStyle().Bold(true).Foreground(bg).Background(accent).Padding(0, 2)
|
|
tabInactiveStyle = lipgloss.NewStyle().Foreground(dim).Padding(0, 2)
|
|
|
|
frameStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(accent).
|
|
Padding(0, 1)
|
|
cardStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(accent).
|
|
Padding(1, 2)
|
|
|
|
footerStyle = lipgloss.NewStyle().Foreground(muted).Padding(0, 1)
|
|
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(accent)
|
|
headerStyle = lipgloss.NewStyle().Foreground(dim)
|
|
labelStyle = lipgloss.NewStyle().Foreground(dim).Width(14)
|
|
valueStyle = lipgloss.NewStyle().Foreground(text)
|
|
|
|
roleSystemStyle = lipgloss.NewStyle().Foreground(bad).Bold(true)
|
|
roleAvailableStyle = lipgloss.NewStyle().Foreground(good).Bold(true)
|
|
roleInUseStyle = lipgloss.NewStyle().Foreground(muted)
|
|
devNameStyle = lipgloss.NewStyle().Foreground(accent)
|
|
}
|