feat: init

This commit is contained in:
decentral1se 2024-07-30 14:47:40 +02:00
commit 95fa3784f7
Signed by: decentral1se
GPG Key ID: 03789458B3D0C410
20 changed files with 1978 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
ui/*.glade~
dist/

16
README.md Normal file
View File

@ -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).

127
cmd/hui/main.go Normal file
View File

@ -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))
}

33
go.mod Normal file
View File

@ -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
)

149
go.sum Normal file
View File

@ -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=

182
internal/bldr/get.go Normal file
View File

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

6
internal/conf/bldr.go Normal file
View File

@ -0,0 +1,6 @@
// Package conf deals with application configuration.
package conf
var (
MainWindowGladeFile = "ui/hui.glade"
)

36
internal/conf/dir.go Normal file
View File

@ -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)
}

13
internal/conf/hugo.go Normal file
View File

@ -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)
)

37
internal/conf/widget.go Normal file
View File

@ -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"
)

16
internal/model/model.go Normal file
View File

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

31
internal/ui/about.go Normal file
View File

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

41
internal/ui/error.go Normal file
View File

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

201
internal/ui/header.go Normal file
View File

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

155
internal/ui/init.go Normal file
View File

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

128
internal/ui/new.go Normal file
View File

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

34
internal/ui/preview.go Normal file
View File

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

52
internal/ui/stack.go Normal file
View File

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

14
makefile Normal file
View File

@ -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)

705
ui/hui.glade Normal file
View File

@ -0,0 +1,705 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkDialog" id="errorDialog">
<property name="can-focus">False</property>
<property name="type-hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="margin-left">8</property>
<property name="margin-right">8</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<property name="margin-top">8</property>
<property name="margin-bottom">8</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="errorCloseButton">
<property name="label" translatable="yes">Close</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="hideErrorDialog" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="errorBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-left">8</property>
<property name="margin-right">8</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<property name="margin-top">8</property>
<property name="margin-bottom">8</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="errorStatusLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Something went wrong 🙈</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="size" value="10000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="errorMessageLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkDialog" id="existingSiteDialog">
<property name="can-focus">False</property>
<property name="type-hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="existingSiteBox">
<property name="can-focus">False</property>
<property name="margin-left">8</property>
<property name="margin-right">8</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<property name="margin-top">8</property>
<property name="margin-bottom">8</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="existingSiteButtonBox">
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="cancelexistingSiteButton">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="margin-top">5</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="existingSiteFromGitUrlLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Existing site from Git URL</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="existingSiteGitUrlEntry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin-left">2</property>
<property name="margin-right">2</property>
<property name="margin-start">2</property>
<property name="margin-end">2</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="primary-icon-name">accessories-text-editor</property>
<property name="placeholder-text" translatable="yes">ssh://git@example.org:foo.git</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="existingSiteFromGitUrlBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-bottom">12</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkButton" id="existingSiteFromGitUrlButton">
<property name="label" translatable="yes">Clone</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="margin-top">5</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="existingSiteFromFSLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Existing site from the File system</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="existingSiteFromFSComboxBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="existingSiteFromFSButtonBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="existingSiteFromFSButton">
<property name="label" translatable="yes">Select</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkMenu" id="openSiteMenu">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<object class="GtkWindow" id="mainWindow">
<property name="can-focus">False</property>
<child>
<object class="GtkStack" id="mainWindowStack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<signal name="add" handler="initStackPageShow" swapped="no"/>
<child>
<object class="GtkBox" id="initStackPage">
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="initWelcomeLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Welcome to Hughie 🎉</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="size" value="30000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="initPromptBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="initPromptLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label" translatable="yes">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</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="max-width-chars">50</property>
<attributes>
<attribute name="size" value="11000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="initPromptButton">
<property name="label" translatable="yes">Setup</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-left">5</property>
<property name="margin-top">10</property>
<signal name="clicked" handler="initialise" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="name">initStackPage</property>
<property name="title" translatable="yes">initStackPage</property>
</packing>
</child>
<child>
<object class="GtkBox" id="emptyStackPage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="contentStackPageBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="contentStackPagePlaceholderLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Open or create a new site to get started</property>
<attributes>
<attribute name="size" value="15000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">contentStackPage</property>
<property name="title" translatable="yes">contentStackPage</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="busyStackPage">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="busyStackBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="busyStackLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="busyStackSpinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">page0</property>
<property name="title" translatable="yes">page0</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="contentStackPage">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="vexpand">True</property>
<property name="label" translatable="yes">TODO: implement</property>
<attributes>
<attribute name="size" value="20000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">page1</property>
<property name="title" translatable="yes">page1</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar" id="headerBar">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Hughie</property>
<property name="spacing">0</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkBox" id="openSiteBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Open an existing site</property>
<property name="margin-right">5</property>
<child>
<object class="GtkButton" id="openSiteButton">
<property name="label" translatable="yes">Open</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="openSiteSeparator">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="openSiteMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="popup">openSiteMenu</property>
<property name="use-popover">False</property>
<child>
<object class="GtkImage" id="openSiteMenuIcon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">pan-down-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="createSiteButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Create new site</property>
<property name="margin-right">5</property>
<signal name="clicked" handler="showNewSiteDialog" swapped="no"/>
<child>
<object class="GtkImage" id="createSiteButtonIcon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-new-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="headerBarSeparator">
<property name="can-focus">False</property>
</object>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="createPostButton">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Create new post</property>
<property name="margin-left">5</property>
<child>
<object class="GtkImage" id="createPostButtonIcon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">text-editor</property>
</object>
</child>
</object>
<packing>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="previewButton">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Preview site in browser</property>
<property name="margin-left">5</property>
<signal name="clicked" handler="previewSite" swapped="no"/>
<child>
<object class="GtkImage" id="previewButtonIcon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-reveal-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="primaryMenuButton">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="margin-right">15</property>
<child>
<object class="GtkImage" id="primaryMenuButtonIcon">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">open-menu-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack-type">end</property>
<property name="position">5</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkDialog" id="newSiteDialog">
<property name="can-focus">False</property>
<property name="window-position">center</property>
<property name="type-hint">dialog</property>
<property name="gravity">center</property>
<property name="transient-for">mainWindow</property>
<property name="attached-to">mainWindow</property>
<child internal-child="vbox">
<object class="GtkBox" id="newSiteBox">
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-left">8</property>
<property name="margin-right">8</property>
<property name="margin-start">8</property>
<property name="margin-end">8</property>
<property name="margin-top">8</property>
<property name="margin-bottom">8</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="newSiteButtonBox">
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="cancelCreateSiteButton">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="margin-top">5</property>
<signal name="clicked" handler="hideNewSiteDialog" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="createSiteButton2">
<property name="label" translatable="yes">Create</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="margin-top">5</property>
<signal name="clicked" handler="createNewSite" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="newSiteNameLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">New site name</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="newSiteNameEntry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin-left">2</property>
<property name="margin-right">2</property>
<property name="margin-start">2</property>
<property name="margin-end">2</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="primary-icon-name">accessories-text-editor</property>
<property name="placeholder-text" translatable="yes">another.varia.zone</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>