From b3cd52a8c736bd4e3dda47fe5cbadf39b88a2b17 Mon Sep 17 00:00:00 2001 From: Arsen Musayelyan Date: Fri, 28 May 2021 23:00:17 -0700 Subject: [PATCH] Initial Commit --- .gitignore | 3 + .gitm.toml | 4 + Makefile | 9 ++ README.md | 4 + cmd/webview-permafrost/main.go | 44 ++++++ create.go | 243 +++++++++++++++++++++++++++++++++ default.png | Bin 0 -> 22817 bytes go.mod | 12 ++ go.sum | 102 ++++++++++++++ logging.go | 38 ++++++ main.go | 94 +++++++++++++ permafrost.desktop | 7 + remove.go | 98 +++++++++++++ tmpl.go | 10 ++ 14 files changed, 668 insertions(+) create mode 100644 .gitignore create mode 100644 .gitm.toml create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/webview-permafrost/main.go create mode 100644 create.go create mode 100644 default.png create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logging.go create mode 100644 main.go create mode 100644 permafrost.desktop create mode 100644 remove.go create mode 100644 tmpl.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..558d7b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +/webview-permafrost +/permafrost \ No newline at end of file diff --git a/.gitm.toml b/.gitm.toml new file mode 100644 index 0000000..e4a842f --- /dev/null +++ b/.gitm.toml @@ -0,0 +1,4 @@ +[repos] +origin = "ssh://git@192.168.100.62:2222/Arsen6331/permafrost.git" +gitlab = "git@gitlab.com:moussaelianarsen/permafrost.git" +github = "git@github.com:Arsen6331/permafrost.git" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b01f983 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +all: + go build + go build ./cmd/webview-permafrost + +install: + install -Dm755 permafrost $(PREFIX)/usr/bin/permafrost + install -Dm755 webview-permafrost $(PREFIX)/usr/bin/webview-permafrost + install -Dm644 permafrost.desktop $(PREFIX)/usr/share/applications/permafrost.desktop + install -Dm644 default.png $(PREFIX)/usr/share/pixmaps/permafrost.png \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d08d4a --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Permafrost +Lightweight single-site browser generator using Webview or Chrome/Chromium. + +### Usage diff --git a/cmd/webview-permafrost/main.go b/cmd/webview-permafrost/main.go new file mode 100644 index 0000000..d0c8350 --- /dev/null +++ b/cmd/webview-permafrost/main.go @@ -0,0 +1,44 @@ +package main + +import ( + flag "github.com/spf13/pflag" + "github.com/webview/webview" + "github.com/zserge/lorca" + "os" +) + +func main() { + url := flag.StringP("url", "u", "https://www.arsenm.dev", "URL to open in webview") + debug := flag.BoolP("debug", "d", false, "Enable webview debug mode") + width := flag.IntP("width", "w", 800, "Width of webview window") + height := flag.IntP("height", "h", 600, "Height of webview window") + chrome := flag.Bool("chrome", false, "Use chrome devtools protocol via lorca instead of webview") + flag.Parse() + + if *chrome { + // If chrome does not exist + if lorca.LocateChrome() == "" { + // Display download prompt + lorca.PromptDownload() + // Exit with code 1 + os.Exit(1) + } + // Create new lorca UI + l, _ := lorca.New(*url, "", *width, *height) + defer l.Close() + // Wait until window closed + <-l.Done() + } else { + // Create new webview + w := webview.New(*debug) + defer w.Destroy() + // Set title of webview window + w.SetTitle("WebView SSB") + // Set window size + w.SetSize(*width, *height, webview.HintNone) + // Navigate to specified URL + w.Navigate(*url) + // Run app + w.Run() + } +} diff --git a/create.go b/create.go new file mode 100644 index 0000000..02a8ca0 --- /dev/null +++ b/create.go @@ -0,0 +1,243 @@ +package main + +import ( + "bytes" + _ "embed" + "fmt" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/widget" + "github.com/mat/besticon/ico" + "image" + "image/color" + "image/png" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" +) + +//go:embed default.png +var defLogo []byte + +func createTab(window fyne.Window) *fyne.Container { + // Create entry field for name of SSB + ssbName := widget.NewEntry() + ssbName.SetPlaceHolder("App Name") + + // Create entry field for URL of SSB + ssbURL := widget.NewEntry() + ssbURL.SetPlaceHolder("App URL") + + // Create dropdown menu for category in desktop file + category := widget.NewSelectEntry([]string{ + "AudioVideo", + "Audio", + "Video", + "Development", + "Education", + "Game", + "Graphics", + "Network", + "Office", + "Settings", + "System", + "Utility", + }) + category.PlaceHolder = "Category" + + // Get default logo and decode as png + img, err := png.Decode(bytes.NewReader(defLogo)) + if err != nil { + errDisp(true, err, "Error decoding default logo", window) + } + // Get canvas image from png + defaultIcon := canvas.NewImageFromImage(img) + // Create new container for icon with placeholder line + iconContainer := container.NewMax(canvas.NewLine(color.Black)) + // Set default icon in container + setIcon(defaultIcon, iconContainer) + + // Create image selection dialog + selectImg := dialog.NewFileOpen(func(file fyne.URIReadCloser, err error) { + // Close file at end of function + defer file.Close() + if err != nil { + errDisp(true, err, "Error opening file", window) + } + // If no file selected, stop further execution of function + if file == nil { + return + } + // Get image from file reader + icon := canvas.NewImageFromReader(file, file.URI().Name()) + setIcon(icon, iconContainer) + }, window) + // Create filter constrained to images + selectImg.SetFilter(storage.NewMimeTypeFileFilter([]string{"image/*"})) + + // Create button to use favicon as icon + faviconBtn := widget.NewButton("Use favicon", func() { + // Attempt to parse URL + uri, err := url.ParseRequestURI(ssbURL.Text) + if err != nil { + errDisp(true, err, "Error parsing URL. Note that the scheme (https://) is required.", window) + return + } + // Attempt to get favicon using DuckDuckGo API + res, err := http.Get(fmt.Sprintf("https://external-content.duckduckgo.com/ip3/%s.ico", uri.Host)) + if err != nil { + errDisp(true, err, "Error getting favicon via DuckDuckGo API", window) + return + } + defer res.Body.Close() + // Attempt to decode data as ico file + favicon, err := ico.Decode(res.Body) + if err != nil { + errDisp(true, err, "Error decoding ico file", window) + return + } + // Get new image from decoded data + icon := canvas.NewImageFromImage(favicon) + setIcon(icon, iconContainer) + }) + + // Create vertical container + col := container.NewVBox( + category, + widget.NewButton("Select icon", selectImg.Show), + faviconBtn, + ) + + // Use home directory to get icon path + iconDir := filepath.Join(home, ".config", name, "icons") + + useChrome := widget.NewCheck("Use Chrome (not isolated)", nil) + + // Create new button that creates SSB + createBtn := widget.NewButton("Create", func() { + // Attempt to create SSB + err := createSSB(ssbName.Text, ssbURL.Text, category.Text, useChrome.Checked, iconContainer.Objects[0].(*canvas.Image).Image) + if err != nil { + errDisp(true, err, "Error creating SSB", window) + } + refreshRmBtns(window, iconDir, rmBtns) + ssbName.SetText("") + ssbURL.SetText("") + category.SetText("") + useChrome.SetChecked(false) + setIcon(defaultIcon, iconContainer) + }) + + // Create new vertical container + content := container.New(layout.NewVBoxLayout(), + ssbName, + ssbURL, + // Create dual-column container to house icon and fields + container.NewGridWithColumns(2, + container.NewVBox( + container.NewCenter(widget.NewLabel("Icon")), + iconContainer, + useChrome, + ), + col, + ), + // Add expanding spacer + layout.NewSpacer(), + createBtn, + ) + + // Return base container + return content +} + +func setIcon(img *canvas.Image, logoLayout *fyne.Container) { + // Expand image keeping aspect ratio + img.FillMode = canvas.ImageFillContain + // Set minimum image size to 50x50 + img.SetMinSize(fyne.NewSize(50, 50)) + // Replace image in layout + logoLayout.Objects[0] = img + // Refresh layout (update changes) + logoLayout.Refresh() +} + +func createSSB(ssbName, uri, category string, useChrome bool, icon image.Image) error { + // Parse provided URL for validity + _, err := url.ParseRequestURI(uri) + if err != nil { + return err + } + + // Use home directory to get various paths + configDir := filepath.Join(home, ".config", name) + iconDir := filepath.Join(configDir, "icons", ssbName) + desktopDir := filepath.Join(home, ".local", "share", "applications") + + // Create paths if nonexistent + err = makeDirs(configDir, iconDir, desktopDir) + if err != nil { + return err + } + + // Get paths to resources + iconPath := filepath.Join(iconDir, "icon.png") + desktopPath := filepath.Join(desktopDir, ssbName+".desktop") + + // Create icon file + iconFile, err := os.Create(iconPath) + if err != nil { + return err + } + // Encode icon as png, writing to file + err = png.Encode(iconFile, icon) + if err != nil { + return err + } + iconFile.Close() + + if useChrome { + uri += " --chrome" + } + + // Expand desktop file template with provided data + desktopStr := fmt.Sprintf(DesktopTemplate, + ssbName, + iconPath, + uri, + category, + ) + + // Create new executable desktop file + desktopFile, err := os.OpenFile(desktopPath, os.O_CREATE|os.O_RDWR, 0755) + if err != nil { + return err + } + // Copy expanded desktop file template to file + _, err = io.Copy(desktopFile, strings.NewReader(desktopStr)) + if err != nil { + return err + } + desktopFile.Close() + + return nil +} + +// Make all directories provided if they do not exist +func makeDirs(dirs ...string) error { + // For each directory + for _, dir := range dirs { + // Create directory and parents if required + err := os.MkdirAll(dir, 0755) + if err != nil { + return err + } + } + return nil +} diff --git a/default.png b/default.png new file mode 100644 index 0000000000000000000000000000000000000000..3fbce2f7341b17d742f0eb5ae800e5e527185cbe GIT binary patch literal 22817 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 Jk%nE^e*hivC{_Rf literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d6fecd1 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module permafrost + +go 1.16 + +require ( + fyne.io/fyne/v2 v2.0.3 + github.com/mat/besticon v3.12.0+incompatible + github.com/rs/zerolog v1.22.0 + github.com/spf13/pflag v1.0.3 + github.com/webview/webview v0.0.0-20210330151455-f540d88dde4e + github.com/zserge/lorca v0.1.10 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6fab439 --- /dev/null +++ b/go.sum @@ -0,0 +1,102 @@ +fyne.io/fyne/v2 v2.0.3 h1:qzd2uLLrAVrNeqnLY44QZCsMxZwjoo1my+lMzHicMXY= +fyne.io/fyne/v2 v2.0.3/go.mod h1:nNpgL7sZkDVLraGtQII2ArNRnnl6kHup/KfQRxIhbvs= +github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +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/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fyne-io/mobile v0.1.3-0.20210412090810-650a3139866a h1:3TAJhl8vXyli0tooKB0vd6gLCyBdWL4QEYbDoJpHEZk= +github.com/fyne-io/mobile v0.1.3-0.20210412090810-650a3139866a/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= +github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= +github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= +github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= +github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg= +github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.22.0 h1:XrVUjV4K+izZpKXZHlPrYQiDtmdGiCylnT4i43AAWxg= +github.com/rs/zerolog v1.22.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= +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.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/webview/webview v0.0.0-20210330151455-f540d88dde4e h1:z780M7mCrdt6KiICeW9SGirvQjxDlrVU+n99FO93nbI= +github.com/webview/webview v0.0.0-20210330151455-f540d88dde4e/go.mod h1:rpXAuuHgyEJb6kXcXldlkOjU6y4x+YcASKKXJNUhh0Y= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zserge/lorca v0.1.10 h1:f/xBJ3D3ipcVRCcvN8XqZnpoKcOXV8I4vwqlFyw7ruc= +github.com/zserge/lorca v0.1.10/go.mod h1:bVmnIbIRlOcoV285KIRSe4bUABKi7R7384Ycuum6e4A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8 h1:6WW6V3x1P/jokJBpRQYUJnMHRP6isStQwCozxnU7XQw= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..868f12d --- /dev/null +++ b/logging.go @@ -0,0 +1,38 @@ +package main + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" + "github.com/rs/zerolog" + zlog "github.com/rs/zerolog/log" + "os" +) + +// Set global logger to zerolog +var log = zlog.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + +func errDisp(gui bool, err error, msg string, window ...fyne.Window) { + // If gui is being used + if gui { + // Create new container with message label + content := container.NewVBox( + widget.NewLabel(msg), + ) + if err != nil { + // Add more details dropdown with error label + content.Add(widget.NewAccordion( + widget.NewAccordionItem("More Details", widget.NewLabel(err.Error())), + )) + } + // Create and show new custom dialog with container + dialog.NewCustom("Error", "Ok", content, window[0]).Show() + } else { + if err != nil { + log.Warn().Err(err).Msg(msg) + } else { + log.Warn().Msg(msg) + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..79f0d50 --- /dev/null +++ b/main.go @@ -0,0 +1,94 @@ +package main + +import ( + "fmt" + fyneApp "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + flag "github.com/spf13/pflag" + "image" + "os" + "runtime" +) + +const name = "wvssb" + +var home string + +func init() { + // If not on linux, fatally log + if runtime.GOOS != "linux" { + log.Fatal().Msg("This tool only supports Linux.") + } + var err error + // Get user home directory + home, err = os.UserHomeDir() + if err != nil { + errDisp(false, err, "Error getting user home directory") + } +} + +func main() { + useGui := flag.BoolP("gui", "g", false, "Use GUI (ignores all other flags)") + create := flag.BoolP("create", "c", false, "Create new SSB") + remove := flag.BoolP("remove", "r", false, "Remove an existing SSB") + ssbName := flag.StringP("name", "n", "", "Name of SSB to create or remove") + url := flag.StringP("url", "u", "", "URL of new SSB") + category := flag.StringP("category", "C", "Network", "Category of new SSB") + iconPath := flag.StringP("icon", "i", "", "Path to icon for new SSB") + chrome := flag.Bool("chrome", false, "Use chrome via lorca instead of webview") + flag.ErrHelp = fmt.Errorf("help message for %s", name) + flag.Parse() + + // If --gui provided + if *useGui { + // Start GUI + initGUI() + } else { + // If --create provided + if *create { + // Open icon path provided via --icon + iconFile, err := os.Open(*iconPath) + if err != nil { + errDisp(false, err, "Error opening icon file") + } + defer iconFile.Close() + // Decode icon file into image.Image + icon, _, err := image.Decode(iconFile) + if err != nil { + errDisp(false, err, "Error decoding image from file") + } + // Attempt to create SSB using flag-provided values + err = createSSB(*ssbName, *url, *category, *chrome, icon) + if err != nil { + errDisp(false, err, "Error creating SSB") + } + } else if *remove { + // Attempt to remove ssb of name provided via --name + err := removeSSB(*ssbName) + if err != nil { + errDisp(false, err, "Error removing SSB") + } + } else { + // Show help screen + flag.Usage() + log.Fatal().Msg("Must provide --gui, --create, or --remove") + } + } +} + +func initGUI() { + app := fyneApp.New() + // Create new window with title + window := app.NewWindow("Webview SSB") + + // Create tab container + tabs := container.NewAppTabs( + container.NewTabItem("Create", createTab(window)), + container.NewTabItem("Remove", removeTab(window)), + ) + // Put tabs at the top of the window + tabs.SetTabLocation(container.TabLocationTop) + + window.SetContent(tabs) + window.ShowAndRun() +} diff --git a/permafrost.desktop b/permafrost.desktop new file mode 100644 index 0000000..12abe56 --- /dev/null +++ b/permafrost.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=Permafrost +Icon=/usr/share/pixmaps/permafrost.png +Type=Application +Terminal=false +Exec=permafrost --gui +Categories=Network; \ No newline at end of file diff --git a/remove.go b/remove.go new file mode 100644 index 0000000..6592fb6 --- /dev/null +++ b/remove.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" + "os" + "path/filepath" +) + +var rmBtns *fyne.Container + +func removeTab(window fyne.Window) *fyne.Container { + // Use home directory to get various paths + configDir := filepath.Join(home, ".config", name) + iconDir := filepath.Join(configDir, "icons") + + // Create directories if they do not exist + err := makeDirs(configDir, iconDir) + if err != nil { + errDisp(true, err, "Error creating required directories", window) + } + + // Create new wrapping grid for remove buttons + rmBtns = container.NewGridWrap(fyne.NewSize(125, 75)) + + // Refresh remove buttons, adding any existing SSBs + refreshRmBtns(window, iconDir, rmBtns) + + return rmBtns +} + +func refreshRmBtns(window fyne.Window, iconDir string, rmBtns *fyne.Container) { + // Remove all objects from container + rmBtns.Objects = []fyne.CanvasObject{} + // List files in icon directory + ls, err := os.ReadDir(iconDir) + if err != nil { + errDisp(true, err, "Error listing icon directory", window) + } + for _, listing := range ls { + listingName := listing.Name() + // Get path for SSB icon + listingPath := filepath.Join(iconDir, listingName) + + // Load icon from path + img, err := fyne.LoadResourceFromPath(filepath.Join(listingPath, "icon.png")) + if err != nil { + errDisp(true, err, "Error loading icon as resource", window) + } + + // Create new button with icon + rmBtn := widget.NewButtonWithIcon(listingName, img, func() { + // Create and show new confirmation dialog + dialog.NewConfirm( + "Remove SSB", + fmt.Sprintf("Are you sure you want to remove %s?", listingName), + func(ok bool) { + if ok { + // Attempt to remove SSB + err = removeSSB(listingName) + if err != nil { + errDisp(true, err, "Error removing SSB", window) + } + refreshRmBtns(window, iconDir, rmBtns) + } + }, + window, + ).Show() + }) + // Add button to container + rmBtns.Objects = append(rmBtns.Objects, rmBtn) + } + // Refresh container (update changes) + rmBtns.Refresh() +} + +func removeSSB(ssbName string) error { + // Use home directory to get various paths + configDir := filepath.Join(home, ".config", name) + iconDir := filepath.Join(configDir, "icons", ssbName) + desktopDir := filepath.Join(home, ".local", "share", "applications") + + // Remove icon directory + err := os.RemoveAll(iconDir) + if err != nil { + return err + } + + // Remove desktop file + err = os.Remove(filepath.Join(desktopDir, ssbName+".desktop")) + if err != nil { + return err + } + return nil +} diff --git a/tmpl.go b/tmpl.go new file mode 100644 index 0000000..7602be6 --- /dev/null +++ b/tmpl.go @@ -0,0 +1,10 @@ +package main + +const DesktopTemplate = `#!/usr/bin/env xdg-open +[Desktop Entry] +Name=%s +Icon=%s +Type=Application +Terminal=false +Exec=webview-permafrost --url %s +Categories=%s;`