Browse Source

Adds readme, makefile, manpage, and completes the rest of the commands

sloum 2 years ago
  1. 37
  2. 54
  3. 128
  4. 99


@ -0,0 +1,37 @@
GOCMD := go
BINARY := spacewalk
PREFIX := /usr/local
MAN1DIR := ${MANDIR}/man1
.PHONY: build
${GOCMD} build -o ${BINARY}
.PHONY: install
install: install-bin install-man clean
.PHONY: install-man
install-man: spacewalk.1
gzip -k ./spacewalk.1
install -d ${DESTDIR}${MAN1DIR}
install -m 0644 ./spacewalk.1.gz ${DESTDIR}${MAN1DIR}
.PHONY: install-bin
install-bin: build
install -d ${DESTDIR}${BINDIR}
install -m 0755 ./${BINARY} ${DESTDIR}${BINDIR}
.PHONY: clean
${GOCMD} clean
rm -f ./spacewalk.1.gz 2> /dev/null
.PHONY: uninstall
uninstall: clean
rm -f ${DESTDIR}${MAN1DIR}/spacewalk.1.gz


@ -0,0 +1,54 @@
# spacewalk
spacewalk is a site aggregation tool for the gemini protocol. It is inspired by moku-pona, which provides a similar service for gopher users.
## Requirements
The Go toolchain is required to build spacewalk. spacewalk was developed with Go version 1.12.4 and has not been tested with any earlier versions.
## Installation
A Makefile is included and installing for the system should be as simple as:
sudo make install
\* sudo may or may not be encessary depending on your system setup
To just take it for a spin by installing locally just run `make` from within the repo. This will generate a local file that can be run with `./spacewalk [command] [options...]`.
## Features
### Flights
spacewalk uses the concept of _flights_. Each flight represents a `text/gemini` formatted page that will be generated. Each of these pages can have as many gemini URLs associated with it as is desired.
Having the ability to have multiple _flights_ all managed by the same system allows for the powerful ability to create oldschool web-index style pages where you set up different themes or categories and have individual feeds underneath them. This allows for an organized system of curated links that, when spacewalk is set to run as a cron job or the like, get updated automatically.
For example: one might have the following _flights_ registered with their system:
- music
- blogs
- news
- cycling
They could create an index page that links to each flight's output file and they now have a really nice way to share the links they want in organized categories and have all of the content get put in order of newest to oldest.
_Flights_ are flexible though and you certainly do not need to use more than one if one does the job ;)
### Launches
Generating the output is called a _launch_ in spacewalk parlance. You can launch all of your flights at once with one command, or launch them individually. This allows for flexibility if you want to have your _flights_ update at different times.
### Output
spacewalk outputs a _flight_ as a `text/gemini` formatted document with a link to each page contained in the flight log. The links are titled with a title chosen at the time the url was added to the flight log. The links are listed in order of newest to oldest.
In addition, spacewalk allows for optionally adding in a header and/or footer. These are statically linked files and are concatenated together with the link listing at launch time.
## Thats all folks...
That is the basic information. A manpage _is_ included and will be installed when the `make install` option is used (it can also be read directly out of the repo folder if you prefer).
I developed this for fun and do not know how much maintenance I will be doing... but if you encounter something that doesn't seem right, please let me know by opening an issue for sending me an e-mail (can be found in the man page).


@ -57,6 +57,12 @@ func validateDataPaths() {
func expandTilde(path string) string {
usr, _ := user.Current()
newpath := strings.Replace(path, "~", usr.HomeDir, -1)
return strings.TrimSpace(newpath)
func displayUsage() {
fmt.Println("spacewalk [stuff] [things]")
@ -78,10 +84,10 @@ func parseArgs() {
earlyExit("Missing flight name.\n- spacewalk create \033[3mflight\033[0m")
case "update":
if len(a) == 3 {
if len(a) == 5 {
update(a[2], a[3], a[4])
} else {
earlyExit("Missing flight name.\n- spacewalk update \033[3mflight\033[0m")
earlyExit("Incorrect arguments.\n- spacewalk update \033[3mflight item value\033[0m")
case "remove":
if len(a) == 3 {
@ -146,7 +152,7 @@ func readManifest() [][]string {
func writeFlight(flight string, records [][]string) error {
p := filepath.Join(fPath, flight)
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, 0644)
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
@ -159,6 +165,21 @@ func writeFlight(flight string, records [][]string) error {
return nil
func writeManifest(data [][]string) error {
p := filepath.Join(swPath, "flight-manifest")
f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
defer f.Close()
w := csv.NewWriter(f)
if err := w.Error(); err != nil {
return err
return nil
func readFlightLog(flight string) [][]string {
p := filepath.Join(fPath, flight)
f, err := os.Open(p)
@ -272,7 +293,6 @@ func create(flight string) {
fmt.Printf("Creating flight: %q\n", flight)
usr, _ := user.Current()
happy := false
for !happy {
@ -281,21 +301,24 @@ func create(flight string) {
if lp == "" {
fmt.Println("Launch path cannot be empty")
} else {
lp = expandTilde(lp)
lp = strings.Replace(lp, "~", usr.HomeDir, -1)
hp, err = getLine("Enter the header path, or leave blank for none: ")
ifErrExit(err, "Error reading from stdin")
if hp == "" {
hp = "none"
} else {
hp = expandTilde(hp)
hp = strings.Replace(hp, "~", usr.HomeDir, -1)
fp, err = getLine("Enter the footer path, or leave blank for none: ")
ifErrExit(err, "Error reading from stdin")
if fp == "" {
fp = "none"
} else {
fp = expandTilde(fp)
fp = strings.Replace(fp, "~", usr.HomeDir, -1)
fmt.Println("Are you happy with the following:")
fmt.Printf("Launch path: %s\nHeader path: %s\nFooter path: %s\n", lp, hp, fp)
yesNo, err := getLine("Type 'yes' to accept, anything else to redo: ")
@ -319,12 +342,55 @@ func create(flight string) {
func update(flight string) {
func update(flight, item, value string) {
manifest := readManifest()
row := -1
for i, r := range manifest {
if r[0] == flight {
row = i
if row < 0 {
earlyExit(fmt.Sprintf("Could not find flight \033[1m%s\033[0m", flight))
value = expandTilde(value)
switch item {
case "launch":
manifest[row][1] = value
case "header":
manifest[row][2] = value
case "footer":
manifest[row][3] = value
case "name":
earlyExit("Changing the name of a flight is not allowed, you can remove the flight and make a new one?")
earlyExit(fmt.Sprintf("No flight manifest item %q. Available items: launch, header, footer", item))
ifErrExit(writeManifest(manifest), "Update error")
fmt.Printf("Successful update of %s's %s to %s\n", flight, item, value)
func remove(flight string) {
manifest := readManifest()
rowFound := false
updated := make([][]string, 0, len(manifest)-1)
for _, r := range manifest {
if r[0] == flight {
rowFound = true
} else {
updated = append(updated, r)
if !rowFound {
earlyExit(fmt.Sprintf("Could not find flight \033[1m%s\033[0m", flight))
ifErrExit(writeManifest(manifest), "Removal error")
fmt.Printf("Successful removal of %s\n", flight)
func add(flight, url, title string) {
@ -339,24 +405,43 @@ func add(flight, url, title string) {
func del(flight, item string) {
fmt.Println(flight, item)
data := readFlightLog(flight)
updatedData := make([][]string, 0, len(data))
removed := false
for _, row := range data {
if row[0] == item {
removed = true
} else {
updatedData = append(updatedData, row)
if !removed {
fmt.Printf("%q was not found in the flight log for \033[1m%s\033[0m", item, flight)
ifErrExit(writeFlight(flight, updatedData), fmt.Sprintf("ERROR: Unable to write %s to file. Continuing with launch, but data will be out of date at next launch.", flight))
fmt.Printf("\033[1mSuccess\033[0m, %q was removed from \033[1m%s\033[0m\n", item, flight)
func launchFlights() {
fmt.Println("Pre-flight check")
records := readManifest()
fmt.Println("Launching all flights")
fmt.Printf("Launching all flights (%d)", len(records))
for _, fl := range records {
fmt.Println("All flights have been launched and statuses displayed")
fmt.Println("All flight launch procedures have been completed")
func launchFlight(flight string) {
fmt.Printf("Launching %s\n", flight)
fmt.Printf("Launching %s --->\n", flight)
manifest := findItemRow(flight, 0, readManifest())
data := readFlightLog(flight)
ch := make(chan []string)
count := 0
for _, capsule := range data {
go checkForUpdate(capsule, ch)
@ -390,18 +475,24 @@ func launchFlight(flight string) {
err = ioutil.WriteFile(strings.TrimSpace(manifest[1]), out.Bytes(), 0644)
f, err := os.OpenFile(strings.TrimSpace(manifest[1]), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR, cannot launch %q: %s\n", flight, err.Error())
fmt.Fprintf(os.Stderr, "\033[1mLAUNCH ABORTED\033[0m for %q: %s\n", flight, err.Error())
defer f.Close()
fmt.Printf("---> %s has been launched.", flight)
func showFlights(flight string) {
records := readManifest()
found := false
for _, row := range records {
if flight != "" && row[0] != flight {
found = true
fmt.Printf("\033[1mFlight Name:\033[0m %s\n", row[0])
fmt.Printf("\033[1mLaunch Path:\033[0m %s\n", row[1])
fmt.Printf("\033[1mHeader Path:\033[0m %s\n", row[2])
@ -409,6 +500,9 @@ func showFlights(flight string) {
if flight != "" {
if !found {
earlyExit(fmt.Sprintf("Could not find flight \033[1m%s\033[0m", flight))
r := readFlightLog(flight)
for _, fl := range r {


@ -0,0 +1,99 @@
.TH "SPACEWALK" "1" "" "Version 1.0" "spacewalk"
\f[B]spacewalk\f[] \- feed manager/generator for the gemini protocol
\f[B]spacewalk\f[] \f[I]command\f[] [\f[I]args...\f[]]
spacewalk lets users generate and manage an unlimited number of feeds
got sites hosted via the gemini protocol. spacewalk can check for
updates to sites and generate an feed page in text/gemini format
sorted by date of last update for each site in the feed. Users can
have as many feeds as they like.
When you run \f[B]spacewalk\f[] with no arguments a help message is
displayed, listing all available commands with their options.
.SS create \f[I]flightname\f[]
Create a new flight (feed).
When running \f[B]spacewalk create\f[] \f[I]flightname\f[] you will be
asked a few quick questions that will help you configure the flight.
You will set a launch path (the filepath for the output file), a header
path, and a footer path. Headers and footers are optional items that
can be concatenated at the beginning and ending of your flight's
output file. If you do not want to use headers and footers leave
the paths blank.
.SS update \f[I]flightname item value\f[]
Update a flight's manifest.
This command allows a user to change a flight's launch path, header path,
or footer path (the item) to the given value.
The following items are valid:
.SS remove \f[I]flightname\f[]
Remove the given flight from the system. Will delete the flight's
manifest and log. This is irreversible. Be careful.
.SS add \f[I]flightname url title\f[]
Add a site represented by the given title and found at the given url
to the given flight. The title will be displayed in the launch
command's output and the url will be where that title links to.
.SS delete \f[I]flightname title\f[]
Removes the site represented by the title from the given flight.
.SS show [\f[I]flightname\f[]]
When run without a flight name \f[B]show\f[] will show the manifest
for each flight. When run with a flight name \f[B]show\f[] will show
the manifest for the given flight as well as the log (list of sites)
for that flight.
.SS launch [\f[I]flightname\f[]]
When run without a flight name \f[B]launch\f[] will launch all available
flights. When run with a flight name \f[B]launch\f[] will launch only
that flight. Launching a flight consists of checking for updates to
every site in the flight's log and generating the output page. The
output page is then saved at the location specified in the flight's
manifest as the \f[I]launch path\f[].
Once a flight is set up with some sites on its flight log a user will
likely want to run the \f[B]launch\f[] command on a timed job at some
regular interval. This can be set up separately for each flight or as
a group by running launch without passing a flight name. When done on
a server, this allows for automatically updated feeds of curated
collections of sites. spacewalk's ability to manage multiple of these
allows for users to create themed indexes that catalog all of their
favorite gemini content in an organized, up to date, and automatic
\f[B]spacewalk\f[] recognizes the environment variable
\f[B]$XDG_DATA_HOME\f[] and will use it for storing data when available.
If it is not set then \f[I]~/.local/share/spacewalk/\f[] will be used
for storing the main flight manifest and a subfolder, \f[I]/flights/\f[]
is used for storing all of the flight logs.
While these can be edited manually, it is highly recommended to use the
program to interact with these files.
Oh, there are likely bugs. This is software after all...
See issues at: <>
spacewalk is developed by sloum < sloum AT >