diff options
| author | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2023-03-26 21:17:01 +0300 | 
|---|---|---|
| committer | Yaroslav de la Peña Smirnov <yps@yaroslavps.com> | 2023-03-26 21:17:01 +0300 | 
| commit | 3aa790585e8c3c8cff37fc065f34989477c5bdc3 (patch) | |
| tree | 037ecb9fcad12e583deacb224b234a0f6456c696 | |
| download | takeoff-3aa790585e8c3c8cff37fc065f34989477c5bdc3.tar.gz takeoff-3aa790585e8c3c8cff37fc065f34989477c5bdc3.zip  | |
Initial commit
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 26 | ||||
| -rw-r--r-- | config.go | 220 | ||||
| -rw-r--r-- | config/instances | 4 | ||||
| -rw-r--r-- | config/langs | 3 | ||||
| -rw-r--r-- | go.mod | 5 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | static/main.css | 73 | ||||
| -rw-r--r-- | takeoff.go | 62 | ||||
| -rw-r--r-- | templates/index.html | 56 | 
10 files changed, 454 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2221094 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +takeoff diff --git a/README.md b/README.md new file mode 100644 index 0000000..bd32316 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Takeoff! + +A very crude start page. + +## Raison d'être + +I just wanted a quick way of searching through different searXNG instances. It's +a very crude and overkill implementation (it could have easily been made without +a backend, i.e. html/css-only), but I might add some other bells and whistles +here and there for which the backend might be of help. + +It was made just to serve my needs as a start page and nothing more. + +## Configuration + +The searXNG instances should be listed in `config/instances`, one per each line +without `http(s)://`. + +Languages to be made available as an option should be listed in `config/langs` +where each line is a separate language with the first column being the language +code and the second column (separated by a single tab character) the +human-friendly name of the language. + +## TODO + +* Weather at server location diff --git a/config.go b/config.go new file mode 100644 index 0000000..7164e93 --- /dev/null +++ b/config.go @@ -0,0 +1,220 @@ +package main + +import ( +	"log" +	"os" +	"path" +	"strings" +	"sync/atomic" + +	"github.com/fsnotify/fsnotify" +) + +type Lang struct { +	Code string +	Name string +} + +// Wrapper over atomic.Value; contains []string +type atomicStringSlice struct { +	v atomic.Value +} +// Wrapper over atomic.Value; contains []Lang +type atomicLangSlice struct { +	v atomic.Value +} + +// A config that is just a list of lines of text +type lineListConfig struct { +	path  string +	items atomicStringSlice +} + +// A config that is list of two-column lines sepparated by a tab character +type langListConfig struct { +	path  string +	langs atomicLangSlice +} + +// Interface for all types of configs that can be updated/reloaded +type updatableConfig interface { +	update() error +} + +// The data type that will watch for changes in some updatableConfig types and +// call their update() methods on changes +type configWatcher struct { +	lists   map[string]updatableConfig +	watcher *fsnotify.Watcher +	dirpath string +} + +var watcher configWatcher + +func (v *atomicStringSlice) Load() []string { +	return v.v.Load().([]string) +} + +func (v *atomicStringSlice) Store(val []string) { +	v.v.Store(val) +} + +func (v *atomicLangSlice) Load() []Lang { +	return v.v.Load().([]Lang) +} + +func (v *atomicLangSlice) Store(val []Lang) { +	v.v.Store(val) +} + +// Re-read the file of this and update the items list. +func (c *lineListConfig) update() error { +	data, err := os.ReadFile(c.path) +	if err != nil { +		return err +	} + +	// Cut the string into substrings line by line omitting empty lines. +	s := string(data) +	sep := "\n" +	items := make([]string, 0, 8) +	for { +		if i := strings.Index(s, sep); i >= 0 { +			sub := s[:i] +			if len(sub) > 0 { +				items = append(items, sub) +			} +			s = s[i+len(sep):] +			continue +		} +		break +	} +	c.items.Store(items) + +	return nil +} + +// Return the slice of strings atomically +func (c *lineListConfig) getList() []string { +	return c.items.Load() +} + +// Re-read the file of this and update the items list. +func (c *langListConfig) update() error { +	data, err := os.ReadFile(c.path) +	if err != nil { +		return err +	} + +	// Cut the string into substring line by line omitting empty lines, and +	// sepparating the code and the name of the language. +	s := string(data) +	sep := "\n" +	langs := make([]Lang, 0, 8) +	for { +		if i := strings.Index(s, sep); i >= 0 { +			sub := s[:i] +			if len(sub) > 0 { +				cols := strings.SplitN(sub, "\t", 2) +				if len(cols) < 2 { +					log.Println("WARNING: bad language format:", sub) +				} else { +					langs = append(langs, Lang{ cols[0], cols[1] }) +				} +			} +			s = s[i+len(sep):] +			continue +		} +		break +	} +	c.langs.Store(langs) + +	return nil +} + +// Return the slice of strings atomically +func (c *langListConfig) getList() []Lang { +	return c.langs.Load() +} + +// Make a new lineListConfig from the file that is inside the configWatcher dir +// with the name give in this method. The name of the file will be automatically +// watched by the configWatcher. Returns a new instance of a lineListConfig if +// there were no errors. +func newLineListConfig(name string) (*lineListConfig, error) { +	var err error +	path := path.Join(watcher.dirpath, name) + +	c := &lineListConfig{path: path} +	err = c.update() +	watcher.lists[path] = c + +	return c, err +} + +// Make a new langListConfig; the same story as with lineListConfig +func newLangListConfig(name string) (*langListConfig, error) { +	var err error +	path := path.Join(watcher.dirpath, name) + +	c := &langListConfig{path: path} +	err = c.update() +	watcher.lists[path] = c + +	return c, err +} + +// Initialize and start the config watcher. +func startConfigWatcher(path string) error { +	var err error + +	watcher.lists = make(map[string]updatableConfig) +	watcher.dirpath = path + +	watcher.watcher, err = fsnotify.NewWatcher() +	if err != nil { +		return err +	} +	err = watcher.watcher.Add(path) +	if err != nil { +		return err +	} + +	go func() { +		for { +			select { +			case e, ok := <-watcher.watcher.Events: +				if !ok { +					return +				} +				if e.Has(fsnotify.Write) { +					cl, ok := watcher.lists[e.Name] +					if ok { +						if err := cl.update(); err != nil { +							log.Println( +								"read error: couldn't update", e.Name, ":", err, +							) +							break +						} +						log.Println("reloaded config list from file:", e.Name) +					} +				} +			case err, ok := <-watcher.watcher.Errors: +				if !ok { +					return +				} +				log.Println("watcher error:", err) +			} +		} +	}() + +	return err +} + +// Stop the configList watcher. +func stopConfigWatcher() { +	err := watcher.watcher.Close() +	if err != nil { +		log.Println("watcher error: on close:", err) +	} +} diff --git a/config/instances b/config/instances new file mode 100644 index 0000000..6d840c1 --- /dev/null +++ b/config/instances @@ -0,0 +1,4 @@ +search.rhscz.eu +paulgo.io +searx.fmac.xyz +searx.be diff --git a/config/langs b/config/langs new file mode 100644 index 0000000..c0412b6 --- /dev/null +++ b/config/langs @@ -0,0 +1,3 @@ +en-US	🇺🇸 English +es-MX	🇲🇽 Español +ru-RU	🇷🇺 Русский @@ -0,0 +1,5 @@ +module git.yaroslavps.com/takeoff + +go 1.16 + +require github.com/fsnotify/fsnotify v1.6.0 @@ -0,0 +1,4 @@ +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..568171e --- /dev/null +++ b/static/main.css @@ -0,0 +1,73 @@ +* { + 	box-sizing: border-box; + 	color: #fcf8e2; + 	font-family: monospace; +} + +body { + 	background-color: #000; +	/* background: #000 repeat center fixed url("/static/grid.png"); */ +	margin: 0; +} + +a { + 	color: #81acc1; +} + +.header-container, +.main-container, +select { + 	background-color: #000; +} + +.header-container, +.main-container { + 	margin: 0 auto; + 	padding: 1em; +} + +.header-container > pre { + 	font-size: 2em; + 	text-align: center; + 	color: #5b8277; +} + +.header-container { + 	max-width: 800px; +	border-top: 1px solid #fcf8e2; +} + +.main-container { + 	max-width: 800px; +	border-bottom: 1px solid #fcf8e2; +} + +input[type="text"] { + 	padding: 0.2em 0.5em; +	background-color: #242424; +} + +#search-form input[type="text"] { +	width: 100%; +} + +.btn { + 	padding: 0.2em 0.5em; + 	background-color: #5b8277; +} + +.btn:hover { + 	background-color: #7fac96; +} + +@media (max-width: 1100px){ + 	 .header-container > pre { + 	 	 font-size: 1em; + 	 } +} + +@media (max-width: 720px){ + 	 .header-container > pre { + 	 	 font-size: 0.7em; + 	 } +} diff --git a/takeoff.go b/takeoff.go new file mode 100644 index 0000000..7f7dd09 --- /dev/null +++ b/takeoff.go @@ -0,0 +1,62 @@ +package main + +import ( +	"flag" +	"html/template" +	"log" +	"net/http" +	"path" +) + +var ( +	addr = flag.String("a", "localhost:8888", "listen address") +	cdir = flag.String("c", "config", "path to configuration directory") +	sdir = flag.String("s", "static", "path to static files directory") +	tdir = flag.String("t", "templates", "path to templates directory") +) + +var ( +	instances *lineListConfig +	langs *langListConfig +) + +var homeTemplate string + +var tData struct { +	Instances []string +	Langs     []Lang +} + +func home(w http.ResponseWriter, r *http.Request) { +	tData.Instances = instances.getList() +	tData.Langs = langs.getList() +	t := template.Must(template.ParseFiles(homeTemplate)) +	t.Execute(w, &tData) +} + +func serve() { +	files := http.FileServer(http.Dir(*sdir)) +	http.HandleFunc("/", home) +	http.Handle("/static/", http.StripPrefix("/static/", files)) +	log.Fatal(http.ListenAndServe(*addr, nil)) +} + +func main() { +	var err error + +	flag.Parse() + +	startConfigWatcher(*cdir) +	defer stopConfigWatcher() + +	instances, err = newLineListConfig("instances") +	langs, err = newLangListConfig("langs") +	if err != nil { +		log.Fatal("FATAL: config list error: ", err) +	} + +	homeTemplate = path.Join(*tdir, "index.html") + +	log.Printf("takeoff v0 listening at http://%s\n", *addr) +	serve() +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..66b552f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +	<head> +		<meta charset="utf-8"> +		<meta content="width=device-width, initial-scale=1" name="viewport"> +		<title>Takeoff!</title> +		<link rel="stylesheet" href="/static/main.css"> +	</head> +	<body> +		<br> +		<div class="header-container"> +    		<pre> +  _________    __ __ __________  ______________ + /_  __/   |  / //_// ____/ __ \/ ____/ ____/ / +  / / / /| | / ,\  / __/ / / / / /_  / /_  / / + / / / ___ |/ /| |/ /___/ /_/ / __/ / __/ /_/ +/_/ /_/  |_/_/ |_/_____/\____/_/   /_/   (_) +			</pre> +		</div> +		<div class="main-container"> +			<form id="search-form" method="post" action="https://{{ index .Instances 0 }}/search"> +				<input type="text" name="q" id="q"> +				<p>Search with:</p> +				<div class="subform"> +					{{ range .Instances }} +					<button class="instance btn" formaction="https://{{ . }}/search"> +						{{ . }} +					</button> +					{{ end }} +				</div> +				<p>Options:</p> +				<div class="subform"> +					<select id="language" name="language"> +						{{ range .Langs }} +						<option value="{{ .Code }}">{{ .Name }}</option> +						{{ end }} +						<option value="all">Default</option> +						<option value="auto">Auto</option> +					</select> +					<select name="time_range" id="time_range"> +						<option value="" selected>Anytime</option> +						<option value="day">Last day</option> +						<option value="week">Last week</option> +						<option value="month">Last month</option> +						<option value="year">Last year</option> +					</select> +					<select name="safesearch" id="safesearch"> +    					<option value="2">SafeSearch: Strict</option> +    					<option value="1">SafeSearch: Moderate</option> +    					<option value="0" selected>SafeSearch: None</option> +					</select> +				</div> +			</form> +		</div> +	</body> +</html>  | 
