// 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("" + html.EscapeString(title) + "") 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(`` + html.EscapeString(i18n.T("welcome.title")) + "") 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 }