// 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
}