more UI and change default local user permissions

This commit is contained in:
2026-03-19 22:22:24 +00:00
parent a997b184f5
commit a79666f5a6
4 changed files with 198 additions and 37 deletions

48
app.go
View File

@@ -744,6 +744,52 @@ func (a *App) collectCIFSMountConfig(defaults CIFSMountConfig) (CIFSMountConfig,
}, nil }, nil
} }
func defaultCIFSMountConfig() CIFSMountConfig {
uid, gid := defaultLocalMountOwnerGroup(os.Getenv, user.Current, user.Lookup, user.LookupGroupId)
return CIFSMountConfig{
UID: uid,
GID: gid,
}
}
func defaultLocalMountOwnerGroup(
getenv func(string) string,
currentUser func() (*user.User, error),
lookupUser func(string) (*user.User, error),
lookupGroupID func(string) (*user.Group, error),
) (uid string, gid string) {
resolveGroup := func(groupID string) string {
if strings.TrimSpace(groupID) == "" {
return ""
}
group, err := lookupGroupID(groupID)
if err != nil {
return strings.TrimSpace(groupID)
}
return strings.TrimSpace(group.Name)
}
if sudoUser := strings.TrimSpace(getenv("SUDO_USER")); sudoUser != "" {
uid = sudoUser
if sudoGID := strings.TrimSpace(getenv("SUDO_GID")); sudoGID != "" {
return uid, resolveGroup(sudoGID)
}
if u, err := lookupUser(sudoUser); err == nil {
return uid, resolveGroup(u.Gid)
}
return uid, ""
}
u, err := currentUser()
if err != nil {
return "", ""
}
uid = strings.TrimSpace(u.Username)
gid = resolveGroup(u.Gid)
return uid, gid
}
func (a *App) ensureMountPoint(path string) error { func (a *App) ensureMountPoint(path string) error {
info, err := os.Stat(path) info, err := os.Stat(path)
if err == nil { if err == nil {
@@ -1545,7 +1591,7 @@ func (a *App) addClientMount() error {
return err return err
} }
cfg, err := a.collectCIFSMountConfig(CIFSMountConfig{}) cfg, err := a.collectCIFSMountConfig(defaultCIFSMountConfig())
if err != nil { if err != nil {
return err return err
} }

View File

@@ -1,6 +1,9 @@
package main package main
import "testing" import (
"os/user"
"testing"
)
func TestParseCIFSMountEntries(t *testing.T) { func TestParseCIFSMountEntries(t *testing.T) {
contents := `# comment contents := `# comment
@@ -86,3 +89,56 @@ func TestUpdateFstabContentsAddEditDelete(t *testing.T) {
t.Fatalf("unexpected delete result:\nwant: %q\ngot: %q", wantDeleted, deleted) t.Fatalf("unexpected delete result:\nwant: %q\ngot: %q", wantDeleted, deleted)
} }
} }
func TestDefaultLocalMountOwnerGroupUsesCurrentUser(t *testing.T) {
getenv := func(string) string { return "" }
currentUser := func() (*user.User, error) {
return &user.User{Username: "alice", Gid: "1000"}, nil
}
lookupUser := func(string) (*user.User, error) {
t.Fatal("lookupUser should not be called without SUDO_USER")
return nil, nil
}
lookupGroupID := func(id string) (*user.Group, error) {
if id != "1000" {
t.Fatalf("unexpected group id lookup: %q", id)
}
return &user.Group{Name: "alice"}, nil
}
uid, gid := defaultLocalMountOwnerGroup(getenv, currentUser, lookupUser, lookupGroupID)
if uid != "alice" || gid != "alice" {
t.Fatalf("unexpected defaults: uid=%q gid=%q", uid, gid)
}
}
func TestDefaultLocalMountOwnerGroupUsesSudoUser(t *testing.T) {
getenv := func(key string) string {
switch key {
case "SUDO_USER":
return "carol"
case "SUDO_GID":
return "2000"
default:
return ""
}
}
currentUser := func() (*user.User, error) {
return &user.User{Username: "root", Gid: "0"}, nil
}
lookupUser := func(string) (*user.User, error) {
t.Fatal("lookupUser should not be called when SUDO_GID is set")
return nil, nil
}
lookupGroupID := func(id string) (*user.Group, error) {
if id != "2000" {
t.Fatalf("unexpected group id lookup: %q", id)
}
return &user.Group{Name: "developers"}, nil
}
uid, gid := defaultLocalMountOwnerGroup(getenv, currentUser, lookupUser, lookupGroupID)
if uid != "carol" || gid != "developers" {
t.Fatalf("unexpected defaults: uid=%q gid=%q", uid, gid)
}
}

Binary file not shown.

129
tui.go
View File

@@ -12,7 +12,9 @@ import (
type tuiTheme struct { type tuiTheme struct {
outer lipgloss.Style outer lipgloss.Style
headerBar lipgloss.Style
badge lipgloss.Style badge lipgloss.Style
chrome lipgloss.Style
title lipgloss.Style title lipgloss.Style
subtitle lipgloss.Style subtitle lipgloss.Style
sectionTitle lipgloss.Style sectionTitle lipgloss.Style
@@ -20,10 +22,15 @@ type tuiTheme struct {
muted lipgloss.Style muted lipgloss.Style
footer lipgloss.Style footer lipgloss.Style
panel lipgloss.Style panel lipgloss.Style
panelAccent lipgloss.Style
row lipgloss.Style row lipgloss.Style
rowSelected lipgloss.Style rowSelected lipgloss.Style
rowNumber lipgloss.Style
rowNumberOn lipgloss.Style
rowKey lipgloss.Style rowKey lipgloss.Style
rowKeySelected lipgloss.Style rowKeySelected lipgloss.Style
rowTitle lipgloss.Style
rowTitleOn lipgloss.Style
rowHint lipgloss.Style rowHint lipgloss.Style
rowHintActive lipgloss.Style rowHintActive lipgloss.Style
statusInfo lipgloss.Style statusInfo lipgloss.Style
@@ -34,35 +41,44 @@ type tuiTheme struct {
} }
func newTUITheme() tuiTheme { func newTUITheme() tuiTheme {
border := lipgloss.Color("#4B5D6B") border := lipgloss.Color("#5A6772")
text := lipgloss.Color("#E9E1D4") borderSoft := lipgloss.Color("#31414A")
muted := lipgloss.Color("#9E9385") text := lipgloss.Color("#ECE4D8")
accent := lipgloss.Color("#D97745") muted := lipgloss.Color("#A49788")
accentSoft := lipgloss.Color("#F4D8BD") accent := lipgloss.Color("#D9733F")
panel := lipgloss.Color("#192126") accentSoft := lipgloss.Color("#F2D2B5")
panelAlt := lipgloss.Color("#232E36") panel := lipgloss.Color("#161C20")
info := lipgloss.Color("#7FB0C2") panelAlt := lipgloss.Color("#212A30")
warn := lipgloss.Color("#D7A55A") panelLift := lipgloss.Color("#2B363D")
success := lipgloss.Color("#8AAC83") info := lipgloss.Color("#86B7C6")
warn := lipgloss.Color("#D8AB63")
success := lipgloss.Color("#91B57F")
return tuiTheme{ return tuiTheme{
outer: lipgloss.NewStyle(). outer: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(border). BorderForeground(border).
Padding(1, 2), Padding(1, 2),
headerBar: lipgloss.NewStyle().
Foreground(borderSoft),
chrome: lipgloss.NewStyle().
Foreground(muted),
badge: lipgloss.NewStyle(). badge: lipgloss.NewStyle().
Foreground(panel). Foreground(panel).
Background(accentSoft). Background(accentSoft).
Bold(true). Bold(true).
Padding(0, 1), Padding(0, 1),
subtitle: lipgloss.NewStyle().
Foreground(muted),
title: lipgloss.NewStyle(). title: lipgloss.NewStyle().
Foreground(text). Foreground(text).
Bold(true), Bold(true),
subtitle: lipgloss.NewStyle().
Foreground(muted),
sectionTitle: lipgloss.NewStyle(). sectionTitle: lipgloss.NewStyle().
Foreground(accentSoft). Foreground(accentSoft).
Bold(true). Bold(true).
Border(lipgloss.NormalBorder(), false, false, true, false).
BorderForeground(borderSoft).
PaddingBottom(0).
MarginBottom(1), MarginBottom(1),
body: lipgloss.NewStyle(). body: lipgloss.NewStyle().
Foreground(text), Foreground(text),
@@ -71,26 +87,50 @@ func newTUITheme() tuiTheme {
footer: lipgloss.NewStyle(). footer: lipgloss.NewStyle().
Foreground(muted). Foreground(muted).
Border(lipgloss.NormalBorder(), true, false, false, false). Border(lipgloss.NormalBorder(), true, false, false, false).
BorderForeground(border). BorderForeground(borderSoft).
PaddingTop(1), PaddingTop(1).
MarginTop(1),
panel: lipgloss.NewStyle(). panel: lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, false, false, true). Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(border). BorderForeground(borderSoft).
PaddingLeft(1), PaddingLeft(1).
row: lipgloss.NewStyle(). MarginBottom(1),
panelAccent: lipgloss.NewStyle().
Border(lipgloss.ThickBorder(), false, false, false, true). Border(lipgloss.ThickBorder(), false, false, false, true).
BorderForeground(panelAlt). BorderForeground(accent).
PaddingLeft(1), PaddingLeft(1).
MarginBottom(1),
row: lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, false, false, true).
BorderForeground(panelLift).
Padding(0, 1).
MarginBottom(1),
rowSelected: lipgloss.NewStyle(). rowSelected: lipgloss.NewStyle().
Border(lipgloss.ThickBorder(), false, false, false, true). Border(lipgloss.ThickBorder(), false, false, false, true).
BorderForeground(accent). BorderForeground(accent).
Background(panel). Background(panel).
PaddingLeft(1), Padding(0, 1).
MarginBottom(1),
rowNumber: lipgloss.NewStyle().
Foreground(muted),
rowNumberOn: lipgloss.NewStyle().
Foreground(accentSoft).
Bold(true),
rowKey: lipgloss.NewStyle(). rowKey: lipgloss.NewStyle().
Foreground(accent). Foreground(accent).
Bold(true), Bold(true).
Background(panelAlt).
Padding(0, 1),
rowKeySelected: lipgloss.NewStyle(). rowKeySelected: lipgloss.NewStyle().
Foreground(accentSoft). Foreground(accentSoft).
Background(panelLift).
Bold(true).
Padding(0, 1),
rowTitle: lipgloss.NewStyle().
Foreground(text).
Bold(true),
rowTitleOn: lipgloss.NewStyle().
Foreground(text).
Bold(true), Bold(true),
rowHint: lipgloss.NewStyle(). rowHint: lipgloss.NewStyle().
Foreground(muted), Foreground(muted),
@@ -111,7 +151,8 @@ func newTUITheme() tuiTheme {
inputBox: lipgloss.NewStyle(). inputBox: lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()). Border(lipgloss.RoundedBorder()).
BorderForeground(border). BorderForeground(border).
Padding(0, 1), Padding(0, 1).
Background(panelAlt),
} }
} }
@@ -144,10 +185,11 @@ func (t tuiTheme) renderFrame(width int, title, subtitle, body, footer string) s
placeWidth = frameWidth placeWidth = frameWidth
} }
sections := []string{ headerLeft := lipgloss.JoinHorizontal(lipgloss.Top, t.badge.Render("SAMBA CONFIGER"), " ", t.chrome.Render("share editor"))
t.badge.Render("SAMBA CONFIGER"), headerRule := t.headerBar.Render(strings.Repeat("─", max(0, contentWidth-lipgloss.Width(headerLeft)-1)))
t.title.Width(contentWidth).Render(title), header := lipgloss.JoinHorizontal(lipgloss.Center, headerLeft, " ", headerRule)
}
sections := []string{header, t.title.Width(contentWidth).Render(title)}
if subtitle != "" { if subtitle != "" {
sections = append(sections, t.subtitle.Width(contentWidth).Render(subtitle)) sections = append(sections, t.subtitle.Width(contentWidth).Render(subtitle))
} }
@@ -172,7 +214,7 @@ func (t tuiTheme) renderSection(width int, title string, lines []string) string
rendered = append(rendered, t.sectionTitle.Width(contentWidth).Render(title)) rendered = append(rendered, t.sectionTitle.Width(contentWidth).Render(title))
} }
for _, line := range lines { for _, line := range lines {
rendered = append(rendered, t.panel.Width(contentWidth).Render(t.body.Width(contentWidth-2).Render(line))) rendered = append(rendered, t.panel.Width(contentWidth).Render(t.body.Width(max(1, contentWidth-2)).Render(line)))
} }
return strings.Join(rendered, "\n") return strings.Join(rendered, "\n")
} }
@@ -190,6 +232,13 @@ func (t tuiTheme) renderMessage(width int, kind, text string) string {
) )
} }
func max(a, b int) int {
if a > b {
return a
}
return b
}
type menuOption struct { type menuOption struct {
Key string Key string
Value string Value string
@@ -260,17 +309,29 @@ func (m menuModel) View() string {
for i, option := range m.options { for i, option := range m.options {
rowStyle := m.theme.row rowStyle := m.theme.row
keyStyle := m.theme.rowKey keyStyle := m.theme.rowKey
titleStyle := m.theme.rowTitle
hintStyle := m.theme.rowHint hintStyle := m.theme.rowHint
prefix := m.theme.muted.Render(fmt.Sprintf("%02d", i+1)) numberStyle := m.theme.rowNumber
prefix := numberStyle.Render(fmt.Sprintf("%02d", i+1))
if i == m.cursor { if i == m.cursor {
rowStyle = m.theme.rowSelected rowStyle = m.theme.rowSelected
keyStyle = m.theme.rowKeySelected keyStyle = m.theme.rowKeySelected
titleStyle = m.theme.rowTitleOn
hintStyle = m.theme.rowHintActive hintStyle = m.theme.rowHintActive
prefix = keyStyle.Render(">>") numberStyle = m.theme.rowNumberOn
prefix = numberStyle.Render(">>")
} }
label := fmt.Sprintf("%s %s %s", prefix, keyStyle.Render("["+option.Key+"]"), option.Label) top := lipgloss.JoinHorizontal(
rowLines := []string{label} lipgloss.Center,
prefix,
" ",
keyStyle.Render(strings.ToUpper(option.Key)),
" ",
titleStyle.Render(option.Label),
)
rowLines := []string{top}
if option.Description != "" { if option.Description != "" {
rowLines = append(rowLines, hintStyle.Render(option.Description)) rowLines = append(rowLines, hintStyle.Render(option.Description))
} }
@@ -360,10 +421,8 @@ func (m textPromptModel) View() string {
width := clampFrameWidth(m.width) width := clampFrameWidth(m.width)
contentWidth := innerWidth(width) contentWidth := innerWidth(width)
lines := []string{ lines := []string{m.theme.inputLabel.Width(contentWidth).Render(m.label)}
m.theme.inputLabel.Width(contentWidth).Render(m.label), lines = append(lines, m.theme.panelAccent.Width(contentWidth).Render(m.theme.inputBox.Width(max(8, contentWidth-2)).Render(m.input.View())))
m.theme.inputBox.Width(contentWidth).Render(m.input.View()),
}
if m.defaultValue != "" { if m.defaultValue != "" {
lines = append(lines, m.theme.muted.Width(contentWidth).Render("Enter keeps: "+m.defaultValue)) lines = append(lines, m.theme.muted.Width(contentWidth).Render("Enter keeps: "+m.defaultValue))
} }