'more bubbletea, less look bad plz'
This commit is contained in:
301
tui.go
301
tui.go
@@ -332,7 +332,7 @@ func (m menuModel) View() string {
|
||||
)
|
||||
|
||||
rowLines := []string{top}
|
||||
if option.Description != "" {
|
||||
if option.Description != "" && width >= 72 {
|
||||
rowLines = append(rowLines, hintStyle.Render(option.Description))
|
||||
}
|
||||
rows = append(rows, rowStyle.Width(contentWidth).Render(strings.Join(rowLines, "\n")))
|
||||
@@ -445,7 +445,7 @@ func runMenuPrompt(in io.Reader, out io.Writer, theme tuiTheme, title, subtitle
|
||||
options: options,
|
||||
}
|
||||
|
||||
result, err := tea.NewProgram(model, tea.WithInput(in), tea.WithOutput(out)).Run()
|
||||
result, err := tea.NewProgram(model, tea.WithInput(in), tea.WithOutput(out), tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -460,7 +460,7 @@ func runMenuPrompt(in io.Reader, out io.Writer, theme tuiTheme, title, subtitle
|
||||
func runTextPrompt(in io.Reader, out io.Writer, theme tuiTheme, title, subtitle, label, defaultValue string) (string, error) {
|
||||
model := newTextPromptModel(theme, title, subtitle, label, defaultValue)
|
||||
|
||||
result, err := tea.NewProgram(model, tea.WithInput(in), tea.WithOutput(out)).Run()
|
||||
result, err := tea.NewProgram(model, tea.WithInput(in), tea.WithOutput(out), tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -471,3 +471,298 @@ func runTextPrompt(in io.Reader, out io.Writer, theme tuiTheme, title, subtitle,
|
||||
}
|
||||
return final.value, nil
|
||||
}
|
||||
|
||||
type cifsFormTextField struct {
|
||||
key string
|
||||
label string
|
||||
required bool
|
||||
defaultValue string
|
||||
input textinput.Model
|
||||
}
|
||||
|
||||
type cifsFormToggle struct {
|
||||
label string
|
||||
value bool
|
||||
}
|
||||
|
||||
type cifsMountFormModel struct {
|
||||
theme tuiTheme
|
||||
title string
|
||||
subtitle string
|
||||
help []string
|
||||
fields []cifsFormTextField
|
||||
toggles []cifsFormToggle
|
||||
cursor int
|
||||
submitted bool
|
||||
cancel bool
|
||||
width int
|
||||
errMessage string
|
||||
result CIFSMountConfig
|
||||
}
|
||||
|
||||
func newCIFSMountFormModel(theme tuiTheme, title, subtitle string, defaults CIFSMountConfig) cifsMountFormModel {
|
||||
makeInput := func(value string, password bool) textinput.Model {
|
||||
input := textinput.New()
|
||||
input.Prompt = ""
|
||||
input.SetValue(value)
|
||||
input.Focus()
|
||||
input.CharLimit = 512
|
||||
input.Width = 48
|
||||
if password {
|
||||
input.EchoMode = textinput.EchoPassword
|
||||
input.EchoCharacter = '•'
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
fields := []cifsFormTextField{
|
||||
{key: "server", label: "Server name or IP", required: true, defaultValue: defaults.Server, input: makeInput(defaults.Server, false)},
|
||||
{key: "share", label: "Share name on that server", required: true, defaultValue: defaults.Share, input: makeInput(defaults.Share, false)},
|
||||
{key: "mount", label: "Local mount folder", required: true, defaultValue: defaults.MountPoint, input: makeInput(defaults.MountPoint, false)},
|
||||
{key: "username", label: "Username for the remote share", required: true, defaultValue: defaults.Username, input: makeInput(defaults.Username, false)},
|
||||
{key: "password", label: "Password for the remote share", required: true, defaultValue: defaults.Password, input: makeInput(defaults.Password, true)},
|
||||
{key: "domain", label: "Domain or workgroup", defaultValue: defaults.Domain, input: makeInput(defaults.Domain, false)},
|
||||
{key: "uid", label: "Local owner username or uid", defaultValue: defaults.UID, input: makeInput(defaults.UID, false)},
|
||||
{key: "gid", label: "Local group name or gid", defaultValue: defaults.GID, input: makeInput(defaults.GID, false)},
|
||||
{key: "filemode", label: "File permissions", defaultValue: defaultString(defaults.FileMode, "0664"), input: makeInput(defaultString(defaults.FileMode, "0664"), false)},
|
||||
{key: "dirmode", label: "Folder permissions", defaultValue: defaultString(defaults.DirMode, "0775"), input: makeInput(defaultString(defaults.DirMode, "0775"), false)},
|
||||
}
|
||||
|
||||
model := cifsMountFormModel{
|
||||
theme: theme,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
help: []string{
|
||||
"Tab or arrows move between fields.",
|
||||
"Enter toggles switches or submits when Save is selected.",
|
||||
},
|
||||
fields: fields,
|
||||
toggles: []cifsFormToggle{
|
||||
{label: "Mount automatically at startup", value: defaults.AutoMount || (defaults.Server == "" && defaults.Share == "" && defaults.MountPoint == "")},
|
||||
{label: "Make this mount read-only", value: defaults.ReadOnly},
|
||||
},
|
||||
}
|
||||
model.focusCursor()
|
||||
return model
|
||||
}
|
||||
|
||||
func (m *cifsMountFormModel) totalItems() int {
|
||||
return len(m.fields) + len(m.toggles) + 2
|
||||
}
|
||||
|
||||
func (m *cifsMountFormModel) focusCursor() {
|
||||
for i := range m.fields {
|
||||
if i == m.cursor {
|
||||
m.fields[i].input.Focus()
|
||||
} else {
|
||||
m.fields[i].input.Blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *cifsMountFormModel) moveCursor(delta int) {
|
||||
total := m.totalItems()
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
m.cursor = (m.cursor + delta + total) % total
|
||||
m.focusCursor()
|
||||
m.errMessage = ""
|
||||
}
|
||||
|
||||
func (m *cifsMountFormModel) currentConfig() CIFSMountConfig {
|
||||
values := map[string]string{}
|
||||
for _, field := range m.fields {
|
||||
value := strings.TrimSpace(field.input.Value())
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(field.defaultValue)
|
||||
}
|
||||
values[field.key] = value
|
||||
}
|
||||
|
||||
return CIFSMountConfig{
|
||||
Server: values["server"],
|
||||
Share: values["share"],
|
||||
MountPoint: values["mount"],
|
||||
Username: values["username"],
|
||||
Password: values["password"],
|
||||
Domain: values["domain"],
|
||||
UID: values["uid"],
|
||||
GID: values["gid"],
|
||||
FileMode: values["filemode"],
|
||||
DirMode: values["dirmode"],
|
||||
AutoMount: m.toggles[0].value,
|
||||
ReadOnly: m.toggles[1].value,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *cifsMountFormModel) validate() string {
|
||||
for _, field := range m.fields {
|
||||
if !field.required {
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(field.input.Value())
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(field.defaultValue)
|
||||
}
|
||||
if value == "" {
|
||||
return field.label + " is required."
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m cifsMountFormModel) Init() tea.Cmd {
|
||||
return textinput.Blink
|
||||
}
|
||||
|
||||
func (m cifsMountFormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
available := innerWidth(clampFrameWidth(m.width)) - 8
|
||||
if available < 16 {
|
||||
available = 16
|
||||
}
|
||||
for i := range m.fields {
|
||||
m.fields[i].input.Width = available
|
||||
}
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
m.cancel = true
|
||||
return m, tea.Quit
|
||||
case "shift+tab", "up", "k":
|
||||
m.moveCursor(-1)
|
||||
return m, nil
|
||||
case "tab", "down", "j":
|
||||
m.moveCursor(1)
|
||||
return m, nil
|
||||
case "enter":
|
||||
textCount := len(m.fields)
|
||||
toggleStart := textCount
|
||||
saveIndex := textCount + len(m.toggles)
|
||||
cancelIndex := saveIndex + 1
|
||||
|
||||
switch {
|
||||
case m.cursor < textCount:
|
||||
m.moveCursor(1)
|
||||
return m, nil
|
||||
case m.cursor >= toggleStart && m.cursor < saveIndex:
|
||||
idx := m.cursor - toggleStart
|
||||
m.toggles[idx].value = !m.toggles[idx].value
|
||||
return m, nil
|
||||
case m.cursor == saveIndex:
|
||||
if errMessage := m.validate(); errMessage != "" {
|
||||
m.errMessage = errMessage
|
||||
return m, nil
|
||||
}
|
||||
m.result = m.currentConfig()
|
||||
m.submitted = true
|
||||
return m, tea.Quit
|
||||
case m.cursor == cancelIndex:
|
||||
m.cancel = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.cursor < len(m.fields) {
|
||||
var cmd tea.Cmd
|
||||
m.fields[m.cursor].input, cmd = m.fields[m.cursor].input.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m cifsMountFormModel) View() string {
|
||||
width := clampFrameWidth(m.width)
|
||||
if width == 0 {
|
||||
width = clampFrameWidth(84)
|
||||
}
|
||||
contentWidth := innerWidth(width)
|
||||
|
||||
formRows := make([]string, 0, len(m.fields)+len(m.toggles)+3)
|
||||
for i, field := range m.fields {
|
||||
active := i == m.cursor
|
||||
boxStyle := m.theme.panel
|
||||
if active {
|
||||
boxStyle = m.theme.panelAccent
|
||||
}
|
||||
|
||||
label := field.label
|
||||
if field.required {
|
||||
label += " *"
|
||||
}
|
||||
fieldLines := []string{m.theme.inputLabel.Render(label)}
|
||||
fieldLines = append(fieldLines, m.theme.inputBox.Width(max(12, contentWidth-6)).Render(field.input.View()))
|
||||
formRows = append(formRows, boxStyle.Width(contentWidth).Render(strings.Join(fieldLines, "\n")))
|
||||
}
|
||||
|
||||
toggleStart := len(m.fields)
|
||||
for i, toggle := range m.toggles {
|
||||
active := toggleStart+i == m.cursor
|
||||
rowStyle := m.theme.row
|
||||
marker := "[ ]"
|
||||
if toggle.value {
|
||||
marker = "[x]"
|
||||
}
|
||||
if active {
|
||||
rowStyle = m.theme.rowSelected
|
||||
}
|
||||
formRows = append(formRows, rowStyle.Width(contentWidth).Render(
|
||||
m.theme.rowTitle.Render(marker+" "+toggle.label),
|
||||
))
|
||||
}
|
||||
|
||||
saveIndex := len(m.fields) + len(m.toggles)
|
||||
saveStyle := m.theme.row
|
||||
cancelStyle := m.theme.row
|
||||
if m.cursor == saveIndex {
|
||||
saveStyle = m.theme.rowSelected
|
||||
}
|
||||
if m.cursor == saveIndex+1 {
|
||||
cancelStyle = m.theme.rowSelected
|
||||
}
|
||||
formRows = append(formRows, saveStyle.Width(contentWidth).Render(m.theme.rowTitle.Render("Save mount")))
|
||||
formRows = append(formRows, cancelStyle.Width(contentWidth).Render(m.theme.rowTitle.Render("Cancel")))
|
||||
|
||||
previewCfg := m.currentConfig()
|
||||
previewLines := []string{
|
||||
buildFstabLine(previewCfg),
|
||||
"Passwords are written directly into /etc/fstab by this workflow.",
|
||||
}
|
||||
bodyParts := []string{
|
||||
m.theme.renderSection(width, "Workflow", m.help),
|
||||
m.theme.sectionTitle.Width(contentWidth).Render("Mount Details"),
|
||||
strings.Join(formRows, "\n"),
|
||||
m.theme.renderSection(width, "Preview", previewLines),
|
||||
}
|
||||
if m.errMessage != "" {
|
||||
bodyParts = append(bodyParts, m.theme.renderMessage(width, "warn", m.errMessage))
|
||||
}
|
||||
|
||||
return m.theme.renderFrame(
|
||||
m.width,
|
||||
m.title,
|
||||
m.subtitle,
|
||||
strings.Join(bodyParts, "\n\n"),
|
||||
"tab move • enter next/toggle/save • esc cancel",
|
||||
)
|
||||
}
|
||||
|
||||
func runCIFSMountForm(in io.Reader, out io.Writer, theme tuiTheme, title, subtitle string, defaults CIFSMountConfig) (CIFSMountConfig, error) {
|
||||
model := newCIFSMountFormModel(theme, title, subtitle, defaults)
|
||||
|
||||
result, err := tea.NewProgram(model, tea.WithInput(in), tea.WithOutput(out), tea.WithAltScreen()).Run()
|
||||
if err != nil {
|
||||
return CIFSMountConfig{}, err
|
||||
}
|
||||
|
||||
final := result.(cifsMountFormModel)
|
||||
if final.cancel {
|
||||
return CIFSMountConfig{}, ErrCancelled
|
||||
}
|
||||
return final.result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user