Initial commit
This commit is contained in:
commit
74c912dda7
59
main.go
Normal file
59
main.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
app "./pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CLI flags
|
||||||
|
var (
|
||||||
|
title string
|
||||||
|
icon string
|
||||||
|
browser string
|
||||||
|
browserExec string
|
||||||
|
url string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.StringVar(&title, "title", "", "App launcher title, default uses the web page title")
|
||||||
|
flag.StringVar(&icon, "icon", "", "App icon image (filesystem or URL), default uses the site favicon")
|
||||||
|
flag.StringVar(&browser, "browser", "", "Browser executable, full path or command, default firefox or chromium")
|
||||||
|
flag.StringVar(&browserExec, "exec", "", "Manually provide the browser exec string, like `firefox --ssb %s` or `chromium --app=%s`, "+
|
||||||
|
"default is auto-detected based on Firefox and Chrome/ium.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Detect defaults.
|
||||||
|
if browser == "" {
|
||||||
|
detected, err := app.DetectBrowser()
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to detect your web browser. Provide the -browser or -exec options to set one manually")
|
||||||
|
}
|
||||||
|
browser = detected
|
||||||
|
log.Printf("Detected browser: %s", browser)
|
||||||
|
}
|
||||||
|
if browserExec == "" {
|
||||||
|
detected, err := app.DetectExec(browser)
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to detect the browser exec line. Provide the -exec option to set one manually")
|
||||||
|
}
|
||||||
|
browserExec = detected
|
||||||
|
log.Printf("Browser exec line for PWA: '%s'", browserExec)
|
||||||
|
}
|
||||||
|
|
||||||
|
url = flag.Arg(0)
|
||||||
|
if url == "" {
|
||||||
|
panic("Usage: pwa-launcher [options] <url>")
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(app.Parameters{
|
||||||
|
Title: title,
|
||||||
|
URL: url,
|
||||||
|
Exec: browserExec,
|
||||||
|
Icon: icon,
|
||||||
|
})
|
||||||
|
}
|
85
pkg/app.go
Normal file
85
pkg/app.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parameters to run the pwa-launcher app.
|
||||||
|
type Parameters struct {
|
||||||
|
Title string
|
||||||
|
Icon string
|
||||||
|
iconData image.Image
|
||||||
|
URL string
|
||||||
|
Exec string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the main app logic.
|
||||||
|
func Run(p Parameters) error {
|
||||||
|
// If no title or icon given, parse the site.
|
||||||
|
if p.Title == "" || p.Icon == "" {
|
||||||
|
insights, err := Parse(p.URL)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Insights error: %s", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("insights: %+v", insights)
|
||||||
|
if p.Title == "" && insights.Title != "" {
|
||||||
|
p.Title = insights.Title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if icons, err := DetectIcons(p.URL); err == nil {
|
||||||
|
log.Printf("detected icons: %+v", icons)
|
||||||
|
insights.Icons = append(insights.Icons, icons...)
|
||||||
|
} else {
|
||||||
|
log.Printf("DetectIcons error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Final insights: %+v", insights)
|
||||||
|
|
||||||
|
// Select the best icon.
|
||||||
|
if p.Icon == "" {
|
||||||
|
icon, err := BestIcon(insights)
|
||||||
|
if err != nil {
|
||||||
|
panic("No suitable app icon found, provide one manually with -icon")
|
||||||
|
}
|
||||||
|
p.iconData = icon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we need to get or download an icon?
|
||||||
|
if p.iconData == nil && p.Icon != "" {
|
||||||
|
if strings.HasPrefix(p.Icon, "http:") || strings.HasPrefix(p.Icon, "https:") {
|
||||||
|
// Download an icon from the web.
|
||||||
|
png, err := ParseWebPNG(p.Icon)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Couldn't download -icon from %s: %s", p.Icon, err)
|
||||||
|
}
|
||||||
|
p.iconData = png
|
||||||
|
} else {
|
||||||
|
fh, err := os.Open(p.Icon)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
png, err := ParsePNG(fh)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
p.iconData = png
|
||||||
|
fh.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing a title?
|
||||||
|
if p.Title == "" {
|
||||||
|
panic("Couldn't detect a page title, provide one with the -title option")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the icon and launcher.
|
||||||
|
Install(p)
|
||||||
|
log.Printf("Launcher installed successfully.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
35
pkg/browsers.go
Normal file
35
pkg/browsers.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preferred browsers list.
|
||||||
|
var browsers = []string{
|
||||||
|
"/usr/bin/firefox",
|
||||||
|
"/usr/bin/chromium",
|
||||||
|
"/usr/bin/google-chrome",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectBrowser auto-detects a preferred web browser and returns its path.
|
||||||
|
func DetectBrowser() (string, error) {
|
||||||
|
for _, path := range browsers {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("failed to auto-detect a web browser")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectExec auto-detects the exec line syntax for the selected browser.
|
||||||
|
func DetectExec(browser string) (string, error) {
|
||||||
|
if strings.Contains(browser, "firefox") {
|
||||||
|
return fmt.Sprintf("%s --ssb %%s", browser), nil
|
||||||
|
} else if strings.Contains(browser, "chromium") || strings.Contains(browser, "chrome") {
|
||||||
|
return fmt.Sprintf("%s --app=%%s", browser), nil
|
||||||
|
}
|
||||||
|
return "", errors.New("failed to auto-detect the browser exec line")
|
||||||
|
}
|
138
pkg/image.go
Normal file
138
pkg/image.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BestIcon selects the best PNG icon image.
|
||||||
|
func BestIcon(i Insights) (image.Image, error) {
|
||||||
|
// Ensure all icons are loaded.
|
||||||
|
for _, icon := range i.Icons {
|
||||||
|
if icon.Data == nil {
|
||||||
|
img, err := ParseWebPNG(icon.URL)
|
||||||
|
if err == nil {
|
||||||
|
icon.Data = img
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the biggest icon.
|
||||||
|
var biggest image.Image
|
||||||
|
var maxSize int
|
||||||
|
for _, icon := range i.Icons {
|
||||||
|
w := icon.Width()
|
||||||
|
if w > maxSize && icon.Data != nil {
|
||||||
|
biggest = icon.Data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if biggest == nil {
|
||||||
|
return nil, errors.New("no suitable icon available")
|
||||||
|
}
|
||||||
|
return biggest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePNG parses a PNG image.
|
||||||
|
func ParsePNG(r io.Reader) (image.Image, error) {
|
||||||
|
img, err := png.Decode(r)
|
||||||
|
return img, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWebPNG parses a PNG image by HTTP URL.
|
||||||
|
func ParseWebPNG(url string) (image.Image, error) {
|
||||||
|
resp, err := http.Get(AddScheme(url))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return ParsePNG(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IcoToPNG converts a .ico file into PNG images.
|
||||||
|
func IcoToPNG(r io.Reader) ([]image.Image, error) {
|
||||||
|
var result = []image.Image{}
|
||||||
|
bin, _ := ioutil.ReadAll(r)
|
||||||
|
|
||||||
|
// Check if the image is already a PNG format.
|
||||||
|
if string(bin[1:4]) == "PNG" {
|
||||||
|
fmt.Printf("IcoToPNG: ico is already a png format!")
|
||||||
|
buf := bytes.NewBuffer(bin)
|
||||||
|
img, err := png.Decode(buf)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
return []image.Image{img}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have icotool available.
|
||||||
|
if !HasICOTool() {
|
||||||
|
return result, errors.New("icotool not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a temp directory to extract this icon.
|
||||||
|
dir, err := ioutil.TempDir("", "pwa-launcher")
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("TempDir: %s", err)
|
||||||
|
}
|
||||||
|
log.Printf("Temp dir to extract .ico file: %s", dir)
|
||||||
|
// defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
// Write the .ico binary to the source file.
|
||||||
|
icoFile := filepath.Join(dir, "favicon.ico")
|
||||||
|
fh, err := os.Create(icoFile)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("Write %s: %s", icoFile, err)
|
||||||
|
}
|
||||||
|
fh.Write(bin)
|
||||||
|
fh.Close()
|
||||||
|
|
||||||
|
// Run the commands.
|
||||||
|
cmd := exec.Command(
|
||||||
|
"icotool", "-x", "-o", dir, icoFile,
|
||||||
|
)
|
||||||
|
stdout, err := cmd.CombinedOutput()
|
||||||
|
fmt.Println(stdout)
|
||||||
|
|
||||||
|
// Glom the PNG images.
|
||||||
|
files, _ := filepath.Glob(filepath.Join(dir, "*.png"))
|
||||||
|
for _, file := range files {
|
||||||
|
fh, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if img, err := ParsePNG(fh); err == nil {
|
||||||
|
result = append(result, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
fh.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasICOTool checks if the icotool binary is available.
|
||||||
|
func HasICOTool() bool {
|
||||||
|
_, err := exec.LookPath("icotool")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(
|
||||||
|
"********\n" +
|
||||||
|
"WARNING: command `icotool` was not found on your system.\n" +
|
||||||
|
"Extracting PNG images from .ico files will not be supported.\n" +
|
||||||
|
"To remedy this, install icoutils, e.g. `sudo apt install icoutils`\n" +
|
||||||
|
"********\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
219
pkg/net.go
Normal file
219
pkg/net.go
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Insights parsed from a web URL.
|
||||||
|
type Insights struct {
|
||||||
|
Title string
|
||||||
|
Icons []Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon found for the web URL.
|
||||||
|
type Icon struct {
|
||||||
|
Size string
|
||||||
|
URL string
|
||||||
|
Data image.Image // raw image data, if available
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width returns the icon's width.
|
||||||
|
func (i Icon) Width() int {
|
||||||
|
parts := strings.Split(i.Size, "x")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
w, _ := strconv.Atoi(parts[0])
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse a web page and return page insights.
|
||||||
|
func Parse(url string) (Insights, error) {
|
||||||
|
log.Printf("### Parse HTML on %s for Title and Icon URLs", url)
|
||||||
|
var (
|
||||||
|
result = Insights{
|
||||||
|
Icons: []Icon{},
|
||||||
|
}
|
||||||
|
inTag = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("HTTP error: %s", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
z := html.NewTokenizer(resp.Body)
|
||||||
|
parsing := true
|
||||||
|
for parsing {
|
||||||
|
tt := z.Next()
|
||||||
|
token := z.Token()
|
||||||
|
switch tt {
|
||||||
|
case html.ErrorToken:
|
||||||
|
fmt.Printf("error: %s\n", z.Err())
|
||||||
|
if z.Err() == io.EOF {
|
||||||
|
// successful error condition
|
||||||
|
parsing = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return result, fmt.Errorf("HTML parsing error: %s", z.Err())
|
||||||
|
|
||||||
|
case html.TextToken:
|
||||||
|
if inTag == "title" && result.Title == "" {
|
||||||
|
result.Title += token.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
case html.StartTagToken:
|
||||||
|
inTag = token.Data
|
||||||
|
|
||||||
|
// Looking for <link rel="shortcut icon">
|
||||||
|
if token.Data == "link" {
|
||||||
|
attr := AttrDict(token)
|
||||||
|
rel, _ := attr["rel"]
|
||||||
|
sizes, _ := attr["sizes"]
|
||||||
|
href, _ := attr["href"]
|
||||||
|
|
||||||
|
// Ensure "//" URIs start with "https://"
|
||||||
|
href = AddScheme(href)
|
||||||
|
|
||||||
|
if rel == "shortcut icon" {
|
||||||
|
log.Printf(`Found <link rel="shortcut icon"> URL: %s`, href)
|
||||||
|
if sizes == "" {
|
||||||
|
sizes = "16x16"
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an ico file, extract the PNGs.
|
||||||
|
if filepath.Ext(href) == ".ico" {
|
||||||
|
log.Printf("The favicon is a .ico file, extracting PNGs")
|
||||||
|
if resp, err := http.Get(href); err == nil {
|
||||||
|
if pngs, err := IcoToPNG(resp.Body); err == nil {
|
||||||
|
for _, png := range pngs {
|
||||||
|
size := png.Bounds().Size()
|
||||||
|
result.Icons = append(result.Icons, Icon{
|
||||||
|
Size: fmt.Sprintf("%dx%d", size.X, size.Y),
|
||||||
|
URL: href,
|
||||||
|
Data: png,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
log.Printf("Error extracting PNG from %s: %s", href, err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
} else {
|
||||||
|
log.Printf("HTTP error downloading %s: %s", href, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Icons = append(result.Icons, Icon{
|
||||||
|
Size: sizes,
|
||||||
|
URL: href,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case html.EndTagToken, html.SelfClosingTagToken:
|
||||||
|
inTag = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectIcons checks well-known icon URLs on a base domain.
|
||||||
|
func DetectIcons(weburl string) ([]Icon, error) {
|
||||||
|
var result = []Icon{}
|
||||||
|
|
||||||
|
uri, err := url.Parse(weburl)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := uri.Scheme + "://" + uri.Host
|
||||||
|
log.Printf("### Auto-detecting Icon URLs from site %s", baseURL)
|
||||||
|
|
||||||
|
tryURI := []string{
|
||||||
|
"/apple-touch-icon.png",
|
||||||
|
"/apple-touch-icon-180x180.png",
|
||||||
|
"/apple-touch-icon-152x152.png",
|
||||||
|
"/apple-touch-icon-144x144.png",
|
||||||
|
"/apple-touch-icon-120x120.png",
|
||||||
|
"/apple-touch-icon-114x114.png",
|
||||||
|
"/apple-touch-icon-76x76.png",
|
||||||
|
"/apple-touch-icon-72x72.png",
|
||||||
|
"/apple-touch-icon-57x57.png",
|
||||||
|
"/apple-touch-icon-60x60.png",
|
||||||
|
"/favicon.ico",
|
||||||
|
}
|
||||||
|
for _, uri := range tryURI {
|
||||||
|
resp, err := http.Get(baseURL + uri)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// PNG images?
|
||||||
|
if filepath.Ext(uri) == ".png" {
|
||||||
|
png, err := ParsePNG(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
size := png.Bounds().Size()
|
||||||
|
result = append(result, Icon{
|
||||||
|
Size: fmt.Sprintf("%dx%d", size.X, size.Y),
|
||||||
|
URL: baseURL + uri,
|
||||||
|
})
|
||||||
|
} else if filepath.Ext(uri) == ".ico" {
|
||||||
|
// Extract the PNG images from the icon.
|
||||||
|
pngs, err := IcoToPNG(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, png := range pngs {
|
||||||
|
size := png.Bounds().Size()
|
||||||
|
result = append(result, Icon{
|
||||||
|
Size: fmt.Sprintf("%dx%d", size.X, size.Y),
|
||||||
|
URL: baseURL + uri,
|
||||||
|
Data: png,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Found icon: %s", uri)
|
||||||
|
_ = resp
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttrDict converts an HTML Token attributes list into a hash map.
|
||||||
|
func AttrDict(token html.Token) map[string]string {
|
||||||
|
var result = map[string]string{}
|
||||||
|
for _, attr := range token.Attr {
|
||||||
|
result[attr.Key] = attr.Val
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddScheme ensures an HTTP URL has a valid scheme, converting "//" into
|
||||||
|
// "https://"
|
||||||
|
func AddScheme(uri string) string {
|
||||||
|
if strings.HasPrefix(uri, "//") {
|
||||||
|
return "https:" + uri
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
101
pkg/templates.go
Normal file
101
pkg/templates.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/png"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Launcher is the source code template for a desktop launcher.
|
||||||
|
const Launcher = `[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Name={{ .Name }}
|
||||||
|
GenericName={{ .Name }}
|
||||||
|
Comment=Progressive Web App
|
||||||
|
Exec={{ .Exec }}
|
||||||
|
Icon={{ .Icon }}
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
StartupNotify=true
|
||||||
|
Categories=Network;
|
||||||
|
`
|
||||||
|
|
||||||
|
// Vars for the launcher template.
|
||||||
|
type Vars struct {
|
||||||
|
Name string
|
||||||
|
Exec string
|
||||||
|
Icon string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the launcher and icon.
|
||||||
|
func Install(p Parameters) {
|
||||||
|
var (
|
||||||
|
HOME = os.Getenv("HOME")
|
||||||
|
basename = UID(p)
|
||||||
|
)
|
||||||
|
|
||||||
|
if HOME == "" {
|
||||||
|
panic("No $HOME variable found for current user!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directories exist.
|
||||||
|
for _, path := range []string{
|
||||||
|
filepath.Join(HOME, ".local", "share", "applications"),
|
||||||
|
filepath.Join(HOME, ".config", "pwa-launcher"),
|
||||||
|
} {
|
||||||
|
err := os.MkdirAll(path, 0755)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to mkdir %s: %s", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the PNG image.
|
||||||
|
var iconPath = filepath.Join(HOME, ".config", "pwa-launcher", basename+".png")
|
||||||
|
log.Printf("Write icon to: %s", iconPath)
|
||||||
|
icoFH, err := os.Create(iconPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer icoFH.Close()
|
||||||
|
if err := png.Encode(icoFH, p.iconData); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the desktop launcher.
|
||||||
|
var launcherPath = filepath.Join(HOME, ".local", "share", "applications", basename+".desktop")
|
||||||
|
log.Printf("Write launcher to: %s", launcherPath)
|
||||||
|
|
||||||
|
fh, err := os.Create(launcherPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
t := template.Must(template.New("launcher").Parse(Launcher))
|
||||||
|
err = t.Execute(fh, Vars{
|
||||||
|
Name: p.Title,
|
||||||
|
Exec: fmt.Sprintf(p.Exec, p.URL),
|
||||||
|
Icon: iconPath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UID generates a unique name for the launcher and icon files.
|
||||||
|
func UID(p Parameters) string {
|
||||||
|
var name = "pwa-"
|
||||||
|
|
||||||
|
uri, err := url.Parse(p.URL)
|
||||||
|
if err == nil {
|
||||||
|
name += strings.Replace(uri.Host, ":", "_", -1) + "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
name += fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
return name
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user