From 95fa3784f7acd930217bfed2e55b7264077475df Mon Sep 17 00:00:00 2001 From: decentral1se Date: Tue, 30 Jul 2024 14:47:40 +0200 Subject: [PATCH] feat: init --- .gitignore | 2 + README.md | 16 + cmd/hui/main.go | 127 ++++++++ go.mod | 33 ++ go.sum | 149 +++++++++ internal/bldr/get.go | 182 +++++++++++ internal/conf/bldr.go | 6 + internal/conf/dir.go | 36 ++ internal/conf/hugo.go | 13 + internal/conf/widget.go | 37 +++ internal/model/model.go | 16 + internal/ui/about.go | 31 ++ internal/ui/error.go | 41 +++ internal/ui/header.go | 201 ++++++++++++ internal/ui/init.go | 155 +++++++++ internal/ui/new.go | 128 ++++++++ internal/ui/preview.go | 34 ++ internal/ui/stack.go | 52 +++ makefile | 14 + ui/hui.glade | 705 ++++++++++++++++++++++++++++++++++++++++ 20 files changed, 1978 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/hui/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/bldr/get.go create mode 100644 internal/conf/bldr.go create mode 100644 internal/conf/dir.go create mode 100644 internal/conf/hugo.go create mode 100644 internal/conf/widget.go create mode 100644 internal/model/model.go create mode 100644 internal/ui/about.go create mode 100644 internal/ui/error.go create mode 100644 internal/ui/header.go create mode 100644 internal/ui/init.go create mode 100644 internal/ui/new.go create mode 100644 internal/ui/preview.go create mode 100644 internal/ui/stack.go create mode 100644 makefile create mode 100644 ui/hui.glade diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82d16ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +ui/*.glade~ +dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..00f57ff --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# hui + +> 🚧 **Extremely WIP** 🚧 + +A [GTK](https://gtk.org) desktop app for publishing [static websites](https://en.wikipedia.org/wiki/Static_web_page) with [Hugo](https://gohugo.io). + +Inspired by [`another.varia.zone`](https://another.varia.zone/) and [Rebuilding a Solar Powered Website](https://solar.lowtechmagazine.com/2023/06/rebuilding-a-solar-powered-website/). + +## Preview + +* [Initial setup (video)](https://d1.hackers.moe/gui/demotime1.mp4) +* [Glade design workflow (image)](https://d1.hackers.moe/gui/glade.png) + +## Planning + +See [`#1`](https://git.coopcloud.tech/decentral1se/hui/issues/1). diff --git a/cmd/hui/main.go b/cmd/hui/main.go new file mode 100644 index 0000000..83701e8 --- /dev/null +++ b/cmd/hui/main.go @@ -0,0 +1,127 @@ +// Package main is the application entrypoint. +package main + +import ( + "fmt" + "log" + "os" + "os/user" + + "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/gtk" + "varia.zone/hui/internal/bldr" + "varia.zone/hui/internal/conf" + "varia.zone/hui/internal/model" + "varia.zone/hui/internal/ui" +) + +const appID = "zone.varia.hui" + +func main() { + application, err := gtk.ApplicationNew(appID, glib.APPLICATION_FLAGS_NONE) + if err != nil { + log.Fatal(fmt.Errorf("main: unable to initialise GTK application: %s", err)) + } + + usr, err := user.Current() + if err != nil { + log.Fatal(fmt.Errorf("main: unable to detect current user: %s", err)) + } + + m := &model.Model{ + User: usr, + } + + application.Connect("activate", func() { + builder, err := gtk.BuilderNewFromFile(conf.MainWindowGladeFile) + if err != nil { + err = fmt.Errorf("main: unable to load '%s' from file: %s", conf.MainWindowGladeFile, err) + log.Fatal(err) + } + m.Builder = builder + + if _, err := os.Stat(conf.HugoBinPath(m.User.HomeDir)); err != nil && os.IsNotExist(err) { + if err := ui.SwitchStackPage(m, conf.InitStackPage); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + + if err := ui.HideSiteButtons(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + + } else { + if err := ui.SwitchStackPage(m, conf.EmptyStackPage); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + + if err := ui.PopulateOpenSiteMenu(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + } + + signals := map[string]interface{}{ + "hideErrorDialog": func() { + if err := ui.HideErrorDialog(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + }, + "initialise": func() { + if err := ui.Initialise(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + }, + "showNewSiteDialog": func() { + if err := ui.ShowNewSiteDialog(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + }, + "hideNewSiteDialog": func() { + if err := ui.HideNewSiteDialog(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + }, + "createNewSite": func() { + if err := ui.CreateNewSite(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + }, + "previewSite": func() { + if err := ui.PreviewSite(m); err != nil { + if err := ui.ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + }, + } + + m.Builder.ConnectSignals(signals) + + window, err := bldr.GetWindow(m.Builder, conf.MainWindow) + if err != nil { + log.Fatal(err) + } + + window.Show() + application.AddWindow(window) + }) + + os.Exit(application.Run(os.Args)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0090f3b --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module varia.zone/hui + +go 1.21.1 + +require ( + github.com/codeclysm/extract v2.2.0+incompatible + github.com/go-git/go-git/v5 v5.12.0 + github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/cloudflare/circl v1.3.9 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/h2non/filetype v1.1.3 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/juju/errors v1.0.0 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..47fbc14 --- /dev/null +++ b/go.sum @@ -0,0 +1,149 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/codeclysm/extract v2.2.0+incompatible h1:q3wyckoA30bhUSiwdQezMqVhwd8+WGE64/GL//LtUhI= +github.com/codeclysm/extract v2.2.0+incompatible/go.mod h1:2nhFMPHiU9At61hz+12bfrlpXSUrOnK+wR+KlGO4Uks= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gotk3/gotk3 v0.6.4 h1:5ur/PRr86PwCG8eSj98D1eXvhrNNK6GILS2zq779dCg= +github.com/gotk3/gotk3 v0.6.4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 h1:eR+xxC8qqKuPMTucZqaklBxLIT7/4L7dzhlwKMrDbj8= +github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= +github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bldr/get.go b/internal/bldr/get.go new file mode 100644 index 0000000..81dab1b --- /dev/null +++ b/internal/bldr/get.go @@ -0,0 +1,182 @@ +// Package bldr deals with Glade GTK* builder functionality. +package bldr + +import ( + "fmt" + + "github.com/gotk3/gotk3/gtk" +) + +// GetBuilder retrieves a named builder. +func GetBuilder(builderName string) (*gtk.Builder, error) { + builder, err := gtk.BuilderNewFromFile(builderName) + if err != nil { + return nil, fmt.Errorf("GetBuilder: unable to BuilderNewFromFile: %s", err) + } + return builder, nil +} + +// GetAboutDialog retrieves a named about dialog. +func GetAboutDialog(builder *gtk.Builder, widgetName string) (*gtk.AboutDialog, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetAboutDialog: unable to GetObject: '%s'", widgetName) + } + + dialog, ok := obj.(*gtk.AboutDialog) + if !ok { + return nil, fmt.Errorf("GetAboutDialog: unable to GetObject: '%s'", widgetName) + } + + return dialog, nil +} + +// GetDialog retrieves a named dialog. +func GetDialog(builder *gtk.Builder, widgetName string) (*gtk.Dialog, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetDialog: unable to GetObject: '%s'", widgetName) + } + + dialog, ok := obj.(*gtk.Dialog) + if !ok { + return nil, fmt.Errorf("GetDialog: unable to GetObject: '%s'", widgetName) + } + + return dialog, nil +} + +// GetEntry retrieves a named entry. +func GetEntry(builder *gtk.Builder, widgetName string) (*gtk.Entry, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetEntry: unable to GetObject: '%s'", widgetName) + } + + entry, ok := obj.(*gtk.Entry) + if !ok { + return nil, fmt.Errorf("GetEntry: unable to GetObject: '%s'", widgetName) + } + + return entry, nil +} + +// GetBox retrieves a named box. +func GetBox(builder *gtk.Builder, widgetName string) (*gtk.Box, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetBox: unable to GetObject: '%s'", widgetName) + } + + box, ok := obj.(*gtk.Box) + if !ok { + return nil, fmt.Errorf("GetBox: unable to GetObject: '%s'", widgetName) + } + + return box, nil +} + +// GetSpinner retrieves a named spinner. +func GetSpinner(builder *gtk.Builder, widgetName string) (*gtk.Spinner, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetSpinner: unable to GetObject: '%s'", widgetName) + } + + spinner, ok := obj.(*gtk.Spinner) + if !ok { + return nil, fmt.Errorf("GetSpinner: unable to GetObject: '%s'", widgetName) + } + + return spinner, nil +} + +// GetLabel retrieves a named label. +func GetLabel(builder *gtk.Builder, widgetName string) (*gtk.Label, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetLabel: unable to GetObject: '%s'", widgetName) + } + + label, ok := obj.(*gtk.Label) + if !ok { + return nil, fmt.Errorf("GetLabel: unable to GetObject: '%s'", widgetName) + } + + return label, nil +} + +// GetWindow retrieves a named window. +func GetWindow(builder *gtk.Builder, widgetName string) (*gtk.Window, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetWindow: unable to GetObject: '%s'", widgetName) + } + + window, ok := obj.(*gtk.Window) + if !ok { + return nil, fmt.Errorf("GetWindow: unable to GetObject: '%s'", widgetName) + } + + return window, nil +} + +// GetButton retrieves a named button. +func GetButton(builder *gtk.Builder, widgetName string) (*gtk.Button, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetButton: unable to GetObject: '%s'", widgetName) + } + + button, ok := obj.(*gtk.Button) + if !ok { + return nil, fmt.Errorf("GetButton: unable to GetObject: '%s'", widgetName) + } + + return button, nil +} + +// GetSeparator retrieves a named seperator. +func GetSeparator(builder *gtk.Builder, widgetName string) (*gtk.Separator, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetSeparator: unable to GetObject: '%s'", widgetName) + } + + sep, ok := obj.(*gtk.Separator) + if !ok { + return nil, fmt.Errorf("GetSeparator: unable to GetObject: '%s'", widgetName) + } + + return sep, nil +} + +// GetHeaderBar retrieves a named header bar. +func GetHeaderBar(builder *gtk.Builder, widgetName string) (*gtk.HeaderBar, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetHeaderBar: unable to GetObject: '%s'", widgetName) + } + + headerBar, ok := obj.(*gtk.HeaderBar) + if !ok { + return nil, fmt.Errorf("GetHeaderBar: unable to GetObject: '%s'", widgetName) + } + + return headerBar, nil +} + +// GetMenu retrieves a named menu. +func GetMenu(builder *gtk.Builder, widgetName string) (*gtk.Menu, error) { + obj, err := builder.GetObject(widgetName) + if err != nil { + return nil, fmt.Errorf("GetMenu: unable to GetObject: '%s'", widgetName) + } + + menu, ok := obj.(*gtk.Menu) + if !ok { + return nil, fmt.Errorf("GetMenu: unable to GetObject: '%s'", widgetName) + } + + return menu, nil +} diff --git a/internal/conf/bldr.go b/internal/conf/bldr.go new file mode 100644 index 0000000..d2bf850 --- /dev/null +++ b/internal/conf/bldr.go @@ -0,0 +1,6 @@ +// Package conf deals with application configuration. +package conf + +var ( + MainWindowGladeFile = "ui/hui.glade" +) diff --git a/internal/conf/dir.go b/internal/conf/dir.go new file mode 100644 index 0000000..e0780f8 --- /dev/null +++ b/internal/conf/dir.go @@ -0,0 +1,36 @@ +package conf + +import ( + "fmt" + "os/user" + "path/filepath" +) + +func GetHomeDir() (string, error) { + usr, err := user.Current() + if err != nil { + return "", fmt.Errorf("GetHomeDir: unable to retrieve current user") + } + + return usr.HomeDir, nil +} + +func DataDir(homeDir string) string { + return filepath.Join(homeDir, ".hui") +} + +func HugoDir(homeDir string) string { + return filepath.Join(DataDir(homeDir), "hugo") +} + +func HugoBinPath(homeDir string) string { + return filepath.Join(HugoDir(homeDir), "hugo") +} + +func SitesDir(homeDir string) string { + return filepath.Join(DataDir(homeDir), "sites") +} + +func NewSiteDir(homeDir, newSiteName string) string { + return filepath.Join(DataDir(homeDir), "sites", newSiteName) +} diff --git a/internal/conf/hugo.go b/internal/conf/hugo.go new file mode 100644 index 0000000..1d01647 --- /dev/null +++ b/internal/conf/hugo.go @@ -0,0 +1,13 @@ +package conf + +import ( + "fmt" +) + +var ( + // HugoVersion is the version of Hugo used. + HugoVersion = "0.124.1" + + // HugoReleaseURL is the URL where Hugo can be downloaded from. + HugoReleaseURL = fmt.Sprintf("https://github.com/gohugoio/hugo/releases/download/v%s/hugo_extended_%s_linux-amd64.tar.gz", HugoVersion, HugoVersion) +) diff --git a/internal/conf/widget.go b/internal/conf/widget.go new file mode 100644 index 0000000..c340a0d --- /dev/null +++ b/internal/conf/widget.go @@ -0,0 +1,37 @@ +package conf + +var ( + AboutDialog = "aboutDialog" + ErrorDialog = "errorDialog" + NewSiteDialog = "newSiteDialog" + ExistingSiteDialog = "existingSiteDialog" + + InitStackPage = "initStackPage" + EmptyStackPage = "emptyStackPage" + BusyStackPage = "busyStackPage" + ContentStackPage = "contentStackPage" + + InitSpinner = "initSpinner" + + InitPromptBox = "initPromptBox" + InitSpinnerBox = "initSpinnerBox" + OpenSiteBox = "openSiteBox" + + InitSpinnerLabel = "initSpinnerLabel" + ErrorMessageLabel = "errorMessageLabel" + BusyStackLabel = "busyStackLabel" + + NewSiteNameEntry = "newSiteNameEntry" + + MainWindow = "mainWindow" + + CreateSiteButton = "createSiteButton" + CreatePostButton = "createPostButton" + PreviewButton = "previewButton" + + HeaderBarSeparator = "headerBarSeparator" + + HeaderBar = "headerBar" + + OpenSiteMenu = "openSiteMenu" +) diff --git a/internal/model/model.go b/internal/model/model.go new file mode 100644 index 0000000..d36a733 --- /dev/null +++ b/internal/model/model.go @@ -0,0 +1,16 @@ +// Package model handles the application state API. +package model + +import ( + "os/user" + + "github.com/gotk3/gotk3/gtk" +) + +// Model is the application internal state. +type Model struct { + Builder *gtk.Builder // Gtk builder + User *user.User // System user + CurrentSite string // The currently chosen site + SiteNames []string // Existing site names +} diff --git a/internal/ui/about.go b/internal/ui/about.go new file mode 100644 index 0000000..cae5a45 --- /dev/null +++ b/internal/ui/about.go @@ -0,0 +1,31 @@ +package ui + +import ( + "varia.zone/hui/internal/bldr" + "varia.zone/hui/internal/conf" + "varia.zone/hui/internal/model" +) + +// ShowAboutDialog handles the show about dialog signal. +func ShowAboutDialog(m *model.Model) error { + dialog, err := bldr.GetAboutDialog(m.Builder, conf.AboutDialog) + if err != nil { + return err + } + + dialog.Show() + + return nil +} + +// HideAboutDialog handles the hide ainbout dialog signal. +func HideAboutDialog(m *model.Model) error { + dialog, err := bldr.GetAboutDialog(m.Builder, conf.AboutDialog) + if err != nil { + return err + } + + dialog.Hide() + + return nil +} diff --git a/internal/ui/error.go b/internal/ui/error.go new file mode 100644 index 0000000..73920f0 --- /dev/null +++ b/internal/ui/error.go @@ -0,0 +1,41 @@ +package ui + +import ( + "fmt" + + "varia.zone/hui/internal/bldr" + "varia.zone/hui/internal/conf" + "varia.zone/hui/internal/model" +) + +// ShowErrDialog raises a dialog dedicated to showing error messages. +func ShowErrDialog(m *model.Model, uiErr error) error { + dialog, err := bldr.GetDialog(m.Builder, conf.ErrorDialog) + if err != nil { + return err + } + + label, err := bldr.GetLabel(m.Builder, conf.ErrorMessageLabel) + if err != nil { + return err + } + + label.SetLabel(fmt.Sprintf("ERROR: %s", uiErr)) + label.Show() + + dialog.Show() + + return nil +} + +// HideErrorDialog handles the hide error dialog signal. +func HideErrorDialog(m *model.Model) error { + dialog, err := bldr.GetDialog(m.Builder, conf.ErrorDialog) + if err != nil { + return err + } + + dialog.Hide() + + return nil +} diff --git a/internal/ui/header.go b/internal/ui/header.go new file mode 100644 index 0000000..569130e --- /dev/null +++ b/internal/ui/header.go @@ -0,0 +1,201 @@ +package ui + +import ( + "fmt" + "log" + "os" + "slices" + "sort" + "strings" + + "github.com/gotk3/gotk3/gtk" + "varia.zone/hui/internal/bldr" + "varia.zone/hui/internal/conf" + "varia.zone/hui/internal/model" +) + +// SetHeaderBarSubtitle sets the header bar subtitle. +func SetHeaderBarSubtitle(m *model.Model, siteName string) error { + headerBar, err := bldr.GetHeaderBar(m.Builder, conf.HeaderBar) + if err != nil { + return fmt.Errorf("SetHeaderBarSubtitle: unable to retrieve '%s': %s", conf.HeaderBar, err) + } + + headerBar.SetSubtitle(siteName) + + return nil +} + +// getSiteButtons retrieves the site related header bar buttons. +func getSiteButtons(m *model.Model) (*gtk.Box, *gtk.Button, error) { + box, err := bldr.GetBox(m.Builder, conf.OpenSiteBox) + if err != nil { + return nil, nil, err + } + + btn, err := bldr.GetButton(m.Builder, conf.CreateSiteButton) + if err != nil { + return nil, nil, err + } + + return box, btn, nil +} + +// ShowSiteButtons shows site related buttons. +func ShowSiteButtons(m *model.Model) error { + box, btn, err := getSiteButtons(m) + if err != nil { + return fmt.Errorf("ShowSiteButtons: %s", err) + } + + box.Show() + btn.Show() + + return nil +} + +// HideSiteButtons shows site related buttons. +func HideSiteButtons(m *model.Model) error { + box, btn, err := getSiteButtons(m) + if err != nil { + return fmt.Errorf("HideSiteButtons: %s", err) + } + + box.Hide() + btn.Hide() + + return nil +} + +func getContentButtons(m *model.Model) (*gtk.Separator, []*gtk.Button, error) { + sep, err := bldr.GetSeparator(m.Builder, conf.HeaderBarSeparator) + if err != nil { + return nil, nil, err + } + + var btns []*gtk.Button + for _, name := range []string{conf.CreatePostButton, conf.PreviewButton} { + btn, err := bldr.GetButton(m.Builder, name) + if err != nil { + return nil, nil, err + } + btns = append(btns, btn) + } + + return sep, btns, err +} + +// ShowContentButtons shows content related buttons. +func ShowContentButtons(m *model.Model) error { + sep, btns, err := getContentButtons(m) + if err != nil { + return fmt.Errorf("HideContentButtons: %s", err) + } + + sep.Show() + + for _, btn := range btns { + btn.Show() + } + + return nil +} + +// HideContentButtons shows content related buttons. +func HideContentButtons(m *model.Model) error { + sep, btns, err := getContentButtons(m) + if err != nil { + return fmt.Errorf("HideContentButtons: %s", err) + } + + sep.Hide() + + for _, btn := range btns { + btn.Hide() + } + + return nil +} + +// OpenMenuChoice responds to a clicked "existing menu" selection. +func OpenMenuChoice(m *model.Model, menuItem *gtk.MenuItem) error { + label := menuItem.GetLabel() + splitLabel := strings.Split(label, " ") + siteName := strings.TrimSpace(splitLabel[len(splitLabel)-1]) + + m.CurrentSite = siteName + + if err := SetHeaderBarSubtitle(m, m.CurrentSite); err != nil { + return fmt.Errorf("OpenMenuChoice: unable to set header bar subtitle: %s", err) + } + + return nil +} + +// gatherSiteNames gathers the site names from disk. +func gatherSiteNames(m *model.Model) error { + sitesDir := conf.SitesDir(m.User.HomeDir) + + entries, err := os.ReadDir(sitesDir) + if err != nil { + return fmt.Errorf("gatherSiteNames: unable to read '%s'", sitesDir) + } + + for _, entry := range entries { + siteName := entry.Name() + if !slices.Contains(m.SiteNames, siteName) { + m.SiteNames = append(m.SiteNames, siteName) + } + } + + sort.Strings(m.SiteNames) + + return err +} + +// PopulateOpenSiteMenu populates the open site menu with site names. +func PopulateOpenSiteMenu(m *model.Model) error { + menu, err := bldr.GetMenu(m.Builder, conf.OpenSiteMenu) + if err != nil { + return err + } + + var forEachErr error + menu.GetChildren().Foreach(func(o interface{}) { + menuItem, ok := o.(*gtk.Widget) + if !ok { + forEachErr = fmt.Errorf("PopulateOpenSiteMenu: unable to retrieve MenuItem") + return + } + menu.Remove(menuItem) + }) + if forEachErr != nil { + return err + } + + if err := gatherSiteNames(m); err != nil { + return err + } + + for idx, siteName := range m.SiteNames { + menuItem, err := gtk.MenuItemNew() + if err != nil { + return err + } + + menuItem.SetLabel(fmt.Sprintf("%d. %s", idx, siteName)) + menuItem.ShowAll() + + menu.Append(menuItem) + + menuItem.Connect("activate", func() { + if err := OpenMenuChoice(m, menuItem); err != nil { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("PopulateOpenSiteMenu: unable to show error dialog: %s", err)) + } + } + }) + } + + return nil +} diff --git a/internal/ui/init.go b/internal/ui/init.go new file mode 100644 index 0000000..f4d0891 --- /dev/null +++ b/internal/ui/init.go @@ -0,0 +1,155 @@ +package ui + +import ( + "log" + "os" + "time" + + "bytes" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "path/filepath" + + "github.com/codeclysm/extract" + "github.com/gotk3/gotk3/glib" + "varia.zone/hui/internal/conf" + "varia.zone/hui/internal/model" +) + +// PostInitSpinner handles UI tasks after Hugo is downloaded. +func PostInitSpinner(m *model.Model) error { + if err := SwitchStackPage(m, conf.EmptyStackPage); err != nil { + return err + } + + if err := ShowSiteButtons(m); err != nil { + return err + } + + return nil +} + +// Initialise initialises the program. +func Initialise(m *model.Model) error { + msg := "Downloading Hugo... this could take a minute..." + if err := SwitchStackPage(m, conf.BusyStackPage, msg); err != nil { + return err + } + + go func() { + if err := initialise(m.User.HomeDir); err != nil { + glib.IdleAdd(func() bool { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + return false + }) + } + }() + + go func() { + for { + if _, err := os.Stat(conf.HugoBinPath(m.User.HomeDir)); err == nil { + break + } + time.Sleep(1) + } + + glib.IdleAdd(func() bool { + if err := PostInitSpinner(m); err != nil { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("main: unable to show error dialog: %s", err)) + } + } + return false + }) + }() + + return nil +} + +// initialise deals with initial setup functionality. +func initialise(homeDir string) error { + if err := createDirectories(homeDir); err != nil { + return err + } + + if err := downloadHugo(homeDir); err != nil { + return err + } + + return nil +} + +// httpGetFile downloads a file from the internet. +func httpGetFile(filepath, url string) error { + out, err := os.Create(filepath) + if err != nil { + return fmt.Errorf("httpGetFile: unable to create '%s': %s", filepath, err) + } + defer out.Close() + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("httpGetFile: unable to HTTP GET '%s'", url) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("httpGetFile: HTTP GET response code %v for '%s'", resp.StatusCode, url) + } + + _, err = io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("httpGetFile: unable to copy HTTP GET response to disk: %s", err) + } + + return nil +} + +// downloadHugo downloads Hugo. +func downloadHugo(homeDir string) error { + hugoTarPath := filepath.Join(conf.HugoDir(homeDir), fmt.Sprintf("hugo-%s.tar.gz", conf.HugoVersion)) + + if _, err := os.Stat(conf.HugoBinPath(homeDir)); err == nil { + return nil + } else if errors.Is(err, os.ErrNotExist) { + if err := httpGetFile(hugoTarPath, conf.HugoReleaseURL); err != nil { + return err + } + } + + fpath, err := ioutil.ReadFile(hugoTarPath) + if err != nil { + return fmt.Errorf("downloadHugo: unable to read file '%s': %s", hugoTarPath, err) + } + + if err := extract.Gz(context.TODO(), bytes.NewBuffer(fpath), conf.HugoDir(homeDir), nil); err != nil { + return fmt.Errorf("downloadHugo: unable to extract file '%s': %s", hugoTarPath, err) + } + + return nil +} + +// createDirectories creates all necessary application directories. +func createDirectories(homeDir string) error { + paths := []string{ + conf.DataDir(homeDir), + conf.HugoDir(homeDir), + conf.SitesDir(homeDir), + } + + for _, fpath := range paths { + if _, err := os.Stat(fpath); err != nil && os.IsNotExist(err) { + if err := os.Mkdir(fpath, 0764); err != nil { + return fmt.Errorf("createDirectories: unable to create '%s': %s", fpath, err) + } + } + } + + return nil +} diff --git a/internal/ui/new.go b/internal/ui/new.go new file mode 100644 index 0000000..2da359f --- /dev/null +++ b/internal/ui/new.go @@ -0,0 +1,128 @@ +package ui + +import ( + "fmt" + "log" + "os/exec" + "path/filepath" + "sort" + + "github.com/go-git/go-git/v5" + "github.com/gotk3/gotk3/glib" + "varia.zone/hui/internal/bldr" + "varia.zone/hui/internal/conf" + "varia.zone/hui/internal/model" +) + +// hugoNewSite creates a new Hugo site. +func hugoNewSite(m *model.Model, siteName string) error { + newSiteDir := conf.NewSiteDir(m.User.HomeDir, siteName) + hugoBinPath := conf.HugoBinPath(m.User.HomeDir) + + cmd := exec.Command(hugoBinPath, "new", "site", newSiteDir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("unable to create new site: %s", err) + } + + // TODO(d1): allow users to choose a default theme + if _, err := git.PlainClone(filepath.Join(newSiteDir, "themes", "nostyleplease"), false, &git.CloneOptions{ + URL: "https://github.com/g-hanwen/hugo-theme-nostyleplease", + }); err != nil { + return fmt.Errorf("unable to clone 'nostyleplease' theme: %s", err) + } + + return nil +} + +// ShowNewSiteDialog shows the new site dialog. +func ShowNewSiteDialog(m *model.Model) error { + dialog, err := bldr.GetDialog(m.Builder, conf.NewSiteDialog) + if err != nil { + return err + } + + dialog.Show() + + return nil +} + +// HideNewSiteDialog hides the new site dialog. +func HideNewSiteDialog(m *model.Model) error { + dialog, err := bldr.GetDialog(m.Builder, conf.NewSiteDialog) + if err != nil { + return err + } + + dialog.Hide() + + return nil +} + +// CreateNewSite handles the creation of a new site. +func CreateNewSite(m *model.Model) error { + if err := HideNewSiteDialog(m); err != nil { + return fmt.Errorf("CreateNewSite: %s", err) + } + + entry, err := bldr.GetEntry(m.Builder, conf.NewSiteNameEntry) + if err != nil { + return fmt.Errorf("CreateNewSite: %s", err) + } + + siteName, err := entry.GetText() + if err != nil { + return fmt.Errorf("CreateNewSite: %s", err) + } + + msg := fmt.Sprintf("Creating new site '%s'...", siteName) + if err := SwitchStackPage(m, conf.BusyStackPage, msg); err != nil { + return fmt.Errorf("CreateNewSite: %s", err) + } + + go func() { + if err := hugoNewSite(m, siteName); err != nil { + glib.IdleAdd(func() bool { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("CreateNewSite: unable to show error dialog: %s", err)) + } + + return false + }) + } + + glib.IdleAdd(func() bool { + m.CurrentSite = siteName + + if err := SetHeaderBarSubtitle(m, m.CurrentSite); err != nil { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("CreateNewSite: unable to show error dialog: %s", err)) + } + } + + m.SiteNames = append(m.SiteNames, siteName) + sort.Strings(m.SiteNames) + + if err := PopulateOpenSiteMenu(m); err != nil { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("CreateNewSite: unable to show error dialog: %s", err)) + } + } + + if err := SwitchStackPage(m, conf.ContentStackPage); err != nil { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("CreateNewSite: unable to show error dialog: %s", err)) + } + } + + if err := ShowContentButtons(m); err != nil { + if err := ShowErrDialog(m, err); err != nil { + log.Fatal(fmt.Errorf("CreateNewSite: unable to show error dialog: %s", err)) + } + } + + return false + }) + }() + + return nil +} diff --git a/internal/ui/preview.go b/internal/ui/preview.go new file mode 100644 index 0000000..7023cc7 --- /dev/null +++ b/internal/ui/preview.go @@ -0,0 +1,34 @@ +package ui + +import ( + "os/exec" + "runtime" + + "varia.zone/hui/internal/model" +) + +func openLocalhost(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "darwin": + cmd = "open" + default: + cmd = "xdg-open" + } + + args = append(args, url) + + return exec.Command(cmd, args...).Start() +} + +// PreviewSite previews the site locally. +func PreviewSite(m *model.Model) error { + // TODO: run serve with generated port + // ensure no port clash by checking model + + url := "http://localhost:1313" + openLocalhost(url) + return nil +} diff --git a/internal/ui/stack.go b/internal/ui/stack.go new file mode 100644 index 0000000..ffcab1c --- /dev/null +++ b/internal/ui/stack.go @@ -0,0 +1,52 @@ +package ui + +import ( + "fmt" + + "varia.zone/hui/internal/bldr" + "varia.zone/hui/internal/conf" + "varia.zone/hui/internal/model" +) + +var allStackPages = []string{ + conf.BusyStackPage, + conf.ContentStackPage, + conf.EmptyStackPage, + conf.InitStackPage, +} + +// SwitchStackPage switches to a chosen stack page. +func SwitchStackPage(m *model.Model, stackPageName string, args ...string) error { + for _, spn := range allStackPages { + stackPage, err := bldr.GetBox(m.Builder, spn) + if err != nil { + return fmt.Errorf("ShowStackPage: %s", err) + } + + if spn == stackPageName { + stackPage.Show() + + if spn == conf.BusyStackPage { + if err := showBusyMessage(m, args...); err != nil { + return fmt.Errorf("ShowStackPage: %s", err) + } + } + } else { + stackPage.Hide() + } + } + + return nil +} + +func showBusyMessage(m *model.Model, args ...string) error { + label, err := bldr.GetLabel(m.Builder, conf.BusyStackLabel) + if err != nil { + return fmt.Errorf("showBusyMessage: %s", err) + } + + msg := args[0] + label.SetLabel(msg) + + return nil +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..42da6b9 --- /dev/null +++ b/makefile @@ -0,0 +1,14 @@ +HUI := ./cmd/hui +BUILD_FLAGS := "-v" +LD_FLAGS := -ldflags="-s -w" + +default: run + +build: + @go build $(BUILD_FLAGS) $(LD_FLAGS) $(HUI) + +update: + @go get -u ./... && go mod tidy + +run: + @go run $(LD_FLAGS) $(HUI) diff --git a/ui/hui.glade b/ui/hui.glade new file mode 100644 index 0000000..c39a877 --- /dev/null +++ b/ui/hui.glade @@ -0,0 +1,705 @@ + + + + + + False + dialog + + + False + 8 + 8 + 8 + 8 + 8 + 8 + vertical + 2 + + + False + end + + + Close + True + True + True + + + + True + True + 0 + + + + + False + False + 0 + + + + + True + False + center + center + 8 + 8 + 8 + 8 + 8 + 8 + True + vertical + + + True + False + Something went wrong 🙈 + + + + + + + False + True + 0 + + + + + True + False + + + False + True + 1 + + + + + True + True + 1 + + + + + + + False + dialog + + + False + 8 + 8 + 8 + 8 + 8 + 8 + vertical + 2 + + + False + end + + + Cancel + True + True + True + 5 + + + False + True + end + 0 + + + + + False + False + 0 + + + + + True + False + Existing site from Git URL + + + False + True + 1 + + + + + True + True + 2 + 2 + 2 + 2 + 2 + 2 + accessories-text-editor + ssh://git@example.org:foo.git + + + False + True + 2 + + + + + True + False + 12 + + + + + + Clone + True + True + True + 5 + + + False + True + end + 1 + + + + + False + True + 3 + + + + + True + False + Existing site from the File system + + + False + True + 4 + + + + + True + False + + + False + True + 6 + + + + + True + False + end + + + Select + True + True + True + + + True + True + 0 + + + + + False + True + 8 + + + + + + + True + False + + + False + + + True + False + + + + False + center + center + vertical + + + True + False + center + center + 5 + Welcome to Hughie 🎉 + + + + + + + False + True + 0 + + + + + True + False + vertical + + + True + False + center + center + It looks like this is your first time here. We need to download Hugo and set up the file system. Click "Setup" to get started + center + True + 50 + + + + + + False + True + 0 + + + + + Setup + True + True + True + center + center + 5 + 10 + + + + False + True + 1 + + + + + False + True + 3 + + + + + initStackPage + initStackPage + + + + + True + False + center + center + vertical + + + True + False + vertical + + + True + False + Open or create a new site to get started + + + + + + False + True + 0 + + + + + False + True + 0 + + + + + contentStackPage + contentStackPage + 1 + + + + + False + vertical + + + True + False + center + center + vertical + + + True + False + + + False + True + 0 + + + + + True + False + True + + + False + True + 1 + + + + + True + True + 0 + + + + + page0 + page0 + 2 + + + + + False + vertical + + + True + False + center + center + True + TODO: implement + + + + + + False + True + 1 + + + + + page1 + page1 + 3 + + + + + + + True + False + Hughie + 0 + True + + + True + False + Open an existing site + 5 + + + Open + True + True + True + + + False + True + 0 + + + + + True + False + + + False + True + 1 + + + + + True + True + False + True + openSiteMenu + False + + + True + False + pan-down-symbolic + + + + + False + True + 2 + + + + + + + True + True + True + Create new site + 5 + + + + True + False + document-new-symbolic + + + + + 1 + + + + + False + + + 2 + + + + + True + True + Create new post + 5 + + + True + False + text-editor + + + + + 3 + + + + + True + True + Preview site in browser + 5 + + + + True + False + view-reveal-symbolic + + + + + 4 + + + + + True + True + True + 15 + + + True + False + open-menu-symbolic + + + + + end + 5 + + + + + + + False + center + dialog + center + mainWindow + mainWindow + + + False + center + center + 8 + 8 + 8 + 8 + 8 + 8 + vertical + 2 + + + False + end + + + Cancel + True + True + True + 5 + + + + False + True + end + 0 + + + + + Create + True + True + True + 5 + + + + False + True + end + 1 + + + + + False + False + 0 + + + + + True + False + start + New site name + + + False + True + 0 + + + + + True + True + 2 + 2 + 2 + 2 + 2 + 2 + accessories-text-editor + another.varia.zone + + + False + True + 2 + + + + + +