das-tools (Go, GTK 3 via gotk3): - das-update: update window, tray icon when updates are available, --cli mode; xbps-install -Su between a snapper pre/post snapshot pair, privileged part through pkexec, rollback and reboot hints - das-welcome: first-login window with install (live), language, home folder renaming, updates, settings and help das-settings 0.3.0: create home folders with translated names. xdg-user-dirs 0.20 only looks for translations in the first XDG_DATA_DIRS entry with a locale directory (/usr/local/share/locale). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
335 lines
8.9 KiB
Go
335 lines
8.9 KiB
Go
// Command das-welcome is the Das OS first-login window: interface language,
|
|
// home folder names, updates, settings and help.
|
|
//
|
|
// das-welcome show the window
|
|
// das-welcome --autostart show it only if the user did not turn that off
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"html"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gotk3/gotk3/glib"
|
|
"github.com/gotk3/gotk3/gtk"
|
|
|
|
"dasos/tools/internal/i18n"
|
|
"dasos/tools/internal/system"
|
|
"dasos/tools/internal/userdirs"
|
|
)
|
|
|
|
const (
|
|
appID = "ua.org.das.Welcome"
|
|
helpURL = "https://git.das.org.ua/das/Das-OS"
|
|
)
|
|
|
|
func main() {
|
|
autostart := flag.Bool("autostart", false, "show the window only if it is enabled at login")
|
|
flag.Parse()
|
|
|
|
if *autostart && !showAtLogin() {
|
|
return
|
|
}
|
|
app, err := gtk.ApplicationNew(appID, glib.APPLICATION_FLAGS_NONE)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "das-welcome:", err)
|
|
os.Exit(1)
|
|
}
|
|
var win *gtk.ApplicationWindow
|
|
app.Connect("activate", func() {
|
|
if win != nil {
|
|
win.Present()
|
|
return
|
|
}
|
|
win, err = newWindow(app)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "das-welcome:", err)
|
|
app.Quit()
|
|
return
|
|
}
|
|
win.ShowAll()
|
|
})
|
|
os.Exit(app.Run(nil))
|
|
}
|
|
|
|
// settingsFile records whether the window opens at login. Without it the
|
|
// window opens once: at the first login of a user (or every live session).
|
|
func settingsFile() string {
|
|
dir := os.Getenv("XDG_CONFIG_HOME")
|
|
if dir == "" {
|
|
dir = filepath.Join(os.Getenv("HOME"), ".config")
|
|
}
|
|
return filepath.Join(dir, "das", "welcome")
|
|
}
|
|
|
|
func showAtLogin() bool {
|
|
data, err := os.ReadFile(settingsFile())
|
|
if err != nil {
|
|
return true
|
|
}
|
|
return strings.TrimSpace(string(data)) == "autostart=true"
|
|
}
|
|
|
|
func setShowAtLogin(on bool) error {
|
|
path := settingsFile()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, []byte(fmt.Sprintf("autostart=%v\n", on)), 0o644)
|
|
}
|
|
|
|
// builder collects the first error of a chain of gotk3 constructors.
|
|
type builder struct{ err error }
|
|
|
|
func (b *builder) check(err error) {
|
|
if b.err == nil {
|
|
b.err = err
|
|
}
|
|
}
|
|
|
|
func (b *builder) label(text string, wrap bool) *gtk.Label {
|
|
l, err := gtk.LabelNew(text)
|
|
b.check(err)
|
|
if err == nil {
|
|
l.SetXAlign(0)
|
|
l.SetLineWrap(wrap)
|
|
}
|
|
return l
|
|
}
|
|
|
|
func (b *builder) button(text string, onClick func()) *gtk.Button {
|
|
btn, err := gtk.ButtonNewWithLabel(text)
|
|
b.check(err)
|
|
if err == nil {
|
|
btn.SetVAlign(gtk.ALIGN_CENTER)
|
|
btn.Connect("clicked", onClick)
|
|
}
|
|
return btn
|
|
}
|
|
|
|
// row is one entry of the list: icon, title, description and controls.
|
|
func (b *builder) row(icon, title, desc string, controls ...gtk.IWidget) (*gtk.Box, *gtk.Label) {
|
|
box, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 14)
|
|
b.check(err)
|
|
img, err := gtk.ImageNewFromIconName(icon, gtk.ICON_SIZE_DND)
|
|
b.check(err)
|
|
text, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 2)
|
|
b.check(err)
|
|
if b.err != nil {
|
|
return box, nil
|
|
}
|
|
img.SetPixelSize(32)
|
|
t := b.label("", false)
|
|
t.SetMarkup("<b>" + html.EscapeString(title) + "</b>")
|
|
d := b.label(desc, true)
|
|
if style, err := d.GetStyleContext(); err == nil {
|
|
style.AddClass("dim-label")
|
|
}
|
|
text.PackStart(t, false, false, 0)
|
|
text.PackStart(d, false, false, 0)
|
|
box.PackStart(img, false, false, 0)
|
|
box.PackStart(text, true, true, 0)
|
|
for _, c := range controls {
|
|
box.PackEnd(c, false, false, 0)
|
|
}
|
|
return box, d
|
|
}
|
|
|
|
func newWindow(app *gtk.Application) (*gtk.ApplicationWindow, error) {
|
|
b := &builder{}
|
|
win, err := gtk.ApplicationWindowNew(app)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
win.SetTitle(i18n.T("welcome.title"))
|
|
win.SetIconName("das-logo")
|
|
win.SetDefaultSize(640, -1)
|
|
win.SetPosition(gtk.WIN_POS_CENTER)
|
|
|
|
outer, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 16)
|
|
b.check(err)
|
|
if b.err != nil {
|
|
return nil, b.err
|
|
}
|
|
outer.SetMarginStart(24)
|
|
outer.SetMarginEnd(24)
|
|
outer.SetMarginTop(24)
|
|
outer.SetMarginBottom(18)
|
|
|
|
header, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 16)
|
|
b.check(err)
|
|
logo, err := gtk.ImageNewFromIconName("das-logo", gtk.ICON_SIZE_DIALOG)
|
|
b.check(err)
|
|
titles, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 4)
|
|
b.check(err)
|
|
if b.err != nil {
|
|
return nil, b.err
|
|
}
|
|
logo.SetPixelSize(72)
|
|
title := b.label("", false)
|
|
title.SetMarkup(`<span size="xx-large" weight="bold">` + html.EscapeString(i18n.T("welcome.title")) + "</span>")
|
|
titles.SetVAlign(gtk.ALIGN_CENTER)
|
|
titles.PackStart(title, false, false, 0)
|
|
titles.PackStart(b.label(i18n.T("welcome.subtitle"), true), false, false, 0)
|
|
header.PackStart(logo, false, false, 0)
|
|
header.PackStart(titles, true, true, 0)
|
|
outer.PackStart(header, false, false, 0)
|
|
|
|
list, err := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 14)
|
|
b.check(err)
|
|
add := func(row *gtk.Box) {
|
|
if sep, err := gtk.SeparatorNew(gtk.ORIENTATION_HORIZONTAL); err == nil {
|
|
list.PackStart(sep, false, false, 0)
|
|
}
|
|
list.PackStart(row, false, false, 0)
|
|
}
|
|
|
|
if system.IsLive() {
|
|
install := b.button(i18n.T("welcome.install"), func() { launch("das-install") })
|
|
if style, err := install.GetStyleContext(); err == nil {
|
|
style.AddClass("suggested-action")
|
|
}
|
|
row, _ := b.row("das-logo", i18n.T("welcome.install"), i18n.T("welcome.install.desc"), install)
|
|
add(row)
|
|
}
|
|
if row := languageRow(b); row != nil {
|
|
add(row)
|
|
}
|
|
if row := foldersRow(b); row != nil {
|
|
add(row)
|
|
}
|
|
if !system.IsLive() {
|
|
row, _ := b.row("software-update-available", i18n.T("welcome.update"), i18n.T("welcome.update.desc"),
|
|
b.button(i18n.T("welcome.update.open"), func() { launch("das-update") }))
|
|
add(row)
|
|
}
|
|
row, _ := b.row("preferences-desktop", i18n.T("welcome.settings"), i18n.T("welcome.settings.desc"),
|
|
b.button(i18n.T("welcome.open"), func() { launch("xfce4-settings-manager") }))
|
|
add(row)
|
|
row, _ = b.row("help-browser", i18n.T("welcome.help"), i18n.T("welcome.help.desc"),
|
|
b.button(i18n.T("welcome.open"), func() { launch("xdg-open", helpURL) }))
|
|
add(row)
|
|
outer.PackStart(list, false, false, 0)
|
|
|
|
footer, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 12)
|
|
b.check(err)
|
|
autostart, err := gtk.CheckButtonNewWithLabel(i18n.T("welcome.autostart"))
|
|
b.check(err)
|
|
if b.err != nil {
|
|
return nil, b.err
|
|
}
|
|
// The window opens by itself only once unless the user asks for more.
|
|
autostart.SetActive(false)
|
|
if _, err := os.Stat(settingsFile()); err == nil {
|
|
autostart.SetActive(showAtLogin())
|
|
} else if err := setShowAtLogin(false); err != nil {
|
|
fmt.Fprintln(os.Stderr, "das-welcome:", err)
|
|
}
|
|
autostart.Connect("toggled", func() {
|
|
if err := setShowAtLogin(autostart.GetActive()); err != nil {
|
|
fmt.Fprintln(os.Stderr, "das-welcome:", err)
|
|
}
|
|
})
|
|
footer.PackStart(autostart, false, false, 0)
|
|
footer.PackEnd(b.button(i18n.T("welcome.close"), func() { win.Close() }), false, false, 0)
|
|
outer.PackEnd(footer, false, false, 0)
|
|
|
|
win.Add(outer)
|
|
return win, b.err
|
|
}
|
|
|
|
func launch(name string, args ...string) {
|
|
if err := exec.Command(name, args...).Start(); err != nil {
|
|
fmt.Fprintln(os.Stderr, "das-welcome:", err)
|
|
}
|
|
}
|
|
|
|
// languageRow offers the UTF-8 locales the system has built. It returns nil
|
|
// when there is nothing to choose from.
|
|
func languageRow(b *builder) *gtk.Box {
|
|
locales := availableLocales()
|
|
if len(locales) < 2 {
|
|
return nil
|
|
}
|
|
combo, err := gtk.ComboBoxTextNew()
|
|
b.check(err)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
current := currentLocale()
|
|
for _, l := range locales {
|
|
combo.Append(l, localeName(l))
|
|
}
|
|
if !combo.SetActiveID(current) {
|
|
combo.SetActive(0)
|
|
}
|
|
combo.SetVAlign(gtk.ALIGN_CENTER)
|
|
|
|
var desc *gtk.Label
|
|
apply := b.button(i18n.T("welcome.apply"), func() {
|
|
if err := setLanguage(combo.GetActiveID()); err != nil {
|
|
desc.SetText(i18n.T("welcome.error", err))
|
|
return
|
|
}
|
|
desc.SetText(i18n.T("welcome.lang.done"))
|
|
})
|
|
row, d := b.row("preferences-desktop-locale", i18n.T("welcome.lang"), i18n.T("welcome.lang.desc"), apply, combo)
|
|
desc = d
|
|
return row
|
|
}
|
|
|
|
// foldersRow offers to rename home folders that kept English names.
|
|
func foldersRow(b *builder) *gtk.Box {
|
|
home := os.Getenv("HOME")
|
|
renames, err := pendingRenames(home)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "das-welcome:", err)
|
|
return nil
|
|
}
|
|
if len(renames) == 0 {
|
|
return nil
|
|
}
|
|
names := make([]string, len(renames))
|
|
for i, r := range renames {
|
|
names[i] = filepath.Base(r.From)
|
|
}
|
|
var desc *gtk.Label
|
|
var rename *gtk.Button
|
|
rename = b.button(i18n.T("welcome.dirs.rename"), func() {
|
|
if err := userdirs.Apply(home, renames); err != nil {
|
|
desc.SetText(i18n.T("welcome.error", err))
|
|
return
|
|
}
|
|
desc.SetText(i18n.T("welcome.dirs.done"))
|
|
rename.SetSensitive(false)
|
|
})
|
|
row, d := b.row("folder", i18n.T("welcome.dirs"), i18n.T("welcome.dirs.desc", strings.Join(names, ", ")), rename)
|
|
desc = d
|
|
return row
|
|
}
|
|
|
|
func pendingRenames(home string) ([]userdirs.Rename, error) {
|
|
data, err := os.ReadFile(userdirs.ConfigFile(home))
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx := context.Background()
|
|
english, err := userdirs.Defaults(ctx, "C.UTF-8")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
localized, err := userdirs.Defaults(ctx, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return userdirs.Plan(home, userdirs.Parse(string(data)), english, localized), nil
|
|
}
|