Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 12s
CI & Build / Web typecheck and unit tests (push) Successful in 8s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Android / Core and FFI clippy and tests (push) Successful in 1m3s
CI & Build / integration (push) Successful in 1m37s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m14s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m3s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 8m42s
Android / Build the server image (push) Successful in 1s
Milestone 546 "Bring the repo to the comment standard (rule 32) and compose defaults (rule 198)", step #6224 "Comment standard sweep: Rust core and desktop client". Doc comments and comments across inkwell-core, the Tauri app and desktop/packaging now say what the code does and the non-obvious why in the present tense. Gone: milestone tags (M10.x, M11.3, M13, M304, M315, M314, milestone 325), issue and Scribe-note numbers, family-idea and family-rule citations, the run numbers and commit hashes behind past bugs, and the narration of what used to be (the stable channel before it held its own bundles, CARGO_PKG_VERSION as the shown version, the MANIFEST_TAG split, the store's missing tests, the old PKGBUILD). The schema's migration notes keep what each step does and why, without "until now". The forge is called the forge rather than Forgejo. Hazards stay as facts: the dev-rolling tag that must not shadow the branch, the manifest matched on this build's version, the APK's fixed name surviving the prune, the LIKE guard in v9, -wal/-shm moving with the renamed store, panic = abort ending the app. 34 files, comments only: a comment-stripping diff of every changed file against HEAD is identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
425 lines
19 KiB
Bash
Executable File
425 lines
19 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# Inkwell desktop — one-command Linux installer.
|
|
#
|
|
# curl -fsSL https://notes.example.com/install.sh | sh (from your server)
|
|
# curl -fsSL https://git.fabledsword.com/bvandeusen/inkwell/raw/branch/dev/desktop/packaging/install.sh | sh
|
|
#
|
|
# FROM A SERVER, the script installs the build that server holds — the same one its
|
|
# Account page offers — and asks for a device token to download it with, because the
|
|
# bytes are not for anyone who can reach the port. The server writes its own address
|
|
# into the copy it serves (src/inkwell/installer.py), so nothing needs editing.
|
|
# Everything below about channels is the FORGE path; a server serves one build, and
|
|
# which channel that is was decided when its image was built.
|
|
#
|
|
# Two channels, the SAME two the app's own updater offers (src-tauri/src/update.rs):
|
|
# stable (default) — the rolling build from every merge to `main`.
|
|
# dev — the rolling build from every green push to `dev`.
|
|
# Both are fixed-tag releases: the tag never moves and the assets are pruned to the
|
|
# current build, so the tag alone names the newest one. The two channels are the
|
|
# same shape, and nothing here special-cases either.
|
|
# Pick one with `--channel dev` or `INKWELL_CHANNEL=dev`. Through a pipe the options go
|
|
# after a `--`: curl -fsSL <url> | sh -s -- --channel dev
|
|
#
|
|
# Served from `dev` rather than `main`: `main` exists but trails day-to-day work by
|
|
# a long way, so the copy there would install an older script. Move the documented
|
|
# URL to `main` after a dev→main merge lands, not before.
|
|
#
|
|
# Fetches the LATEST published release on the chosen channel for this machine's
|
|
# architecture and installs it, ending with a working app + menu entry. Native-first:
|
|
# * Arch/CachyOS (pacman) -> the native .pkg.tar.zst (system libs; needs sudo).
|
|
# * Debian/Ubuntu (dpkg+apt) -> the native .deb (system libs; needs sudo).
|
|
# * everything else (Fedora/openSUSE/…) -> the de-bundled AppImage,
|
|
# installed user-locally (no sudo). The AppImage's graphics libs are
|
|
# stripped in CI (see debundle-graphics.sh), so it uses the host GPU stack
|
|
# and renders where a stock Tauri AppImage would black-window.
|
|
#
|
|
# POSIX sh (dash-safe) so `curl … | sh` works everywhere. Dependency-light and
|
|
# auditable on purpose — read it before you pipe it.
|
|
set -eu
|
|
|
|
INSTANCE="https://git.fabledsword.com"
|
|
REPO="bvandeusen/inkwell"
|
|
API="$INSTANCE/api/v1/repos/$REPO"
|
|
|
|
say() { printf '==> %s\n' "$1"; }
|
|
die() { printf 'error: %s\n' "$1" >&2; exit 1; }
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Inkwell desktop installer.
|
|
|
|
install.sh [--server URL] [--channel stable|dev]
|
|
|
|
--server URL install from this Inkwell server (set already when the
|
|
script came from one)
|
|
--channel stable from the forge: newest build from main (default)
|
|
--channel dev from the forge: rolling build from the latest green push to `dev`
|
|
-h, --help this text
|
|
|
|
The options can also come from INKWELL_SERVER and INKWELL_CHANNEL. Through a pipe, pass
|
|
them after `--`: curl -fsSL <url> | sh -s -- --channel dev
|
|
|
|
From a server, the download needs a device token: make one in the web app under
|
|
Account → Linked devices and paste it when asked. A token typed at the prompt is
|
|
revoked again once the download is done. INKWELL_TOKEN supplies one without a prompt,
|
|
and is left alone, since whoever set it is managing it.
|
|
USAGE
|
|
}
|
|
|
|
# The address of the server that served this script. Written by that server
|
|
# (src/inkwell/installer.py rewrites exactly this line) and empty in the copy the
|
|
# forge serves, which is what keeps the forge path the default there.
|
|
INKWELL_SERVER_DEFAULT=""
|
|
|
|
# --- channel ----------------------------------------------------------------
|
|
channel="${INKWELL_CHANNEL:-stable}"
|
|
channel_asked="${INKWELL_CHANNEL:-}"
|
|
server="${INKWELL_SERVER:-$INKWELL_SERVER_DEFAULT}"
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--channel)
|
|
[ $# -ge 2 ] || die "--channel needs a value (stable or dev)."
|
|
channel="$2"; channel_asked="$2"; shift 2 ;;
|
|
--channel=*) channel="${1#*=}"; channel_asked="$channel"; shift ;;
|
|
--server)
|
|
[ $# -ge 2 ] || die "--server needs an address (https://…)."
|
|
server="$2"; shift 2 ;;
|
|
--server=*) server="${1#*=}"; shift ;;
|
|
-h | --help) usage; exit 0 ;;
|
|
*) die "unknown option: $1 (try --help)" ;;
|
|
esac
|
|
done
|
|
case "$channel" in
|
|
stable | dev) : ;;
|
|
*) die "unknown channel '$channel' — expected stable or dev." ;;
|
|
esac
|
|
|
|
have curl || die "curl is required."
|
|
|
|
# --- server address ---------------------------------------------------------
|
|
# Checked HERE as well as by the server that wrote it: this value goes into every
|
|
# URL below, and a script should not trust a string because of where it came from.
|
|
# The same shape the server enforces — a scheme, then only characters that cannot
|
|
# mean anything to a shell or a URL parser beyond what they say.
|
|
server="${server%/}"
|
|
if [ -n "$server" ]; then
|
|
case "$server" in
|
|
http://?* | https://?*) : ;;
|
|
*) die "the server address must start with http:// or https:// (got: $server)." ;;
|
|
esac
|
|
case "$server" in
|
|
*[!A-Za-z0-9:/._~-]*) die "the server address has characters an address cannot have (got: $server)." ;;
|
|
esac
|
|
if [ -n "$channel_asked" ]; then
|
|
say "Note: a server serves the one build it holds; --channel only applies to the forge."
|
|
fi
|
|
fi
|
|
|
|
# --- architecture gate ------------------------------------------------------
|
|
# Only x86_64 is built. The release-asset naming carries the arch, but with one
|
|
# arch shipping we match by file extension below and just guard the arch here.
|
|
arch="$(uname -m)"
|
|
case "$arch" in
|
|
x86_64 | amd64) : ;;
|
|
*) die "Inkwell ships x86_64 Linux builds only right now (this machine: $arch)." ;;
|
|
esac
|
|
|
|
tmp="$(mktemp -d)"
|
|
trap 'rm -rf "$tmp"' EXIT INT TERM
|
|
|
|
# Every download goes through here, so the server path's token is sent on all of them
|
|
# and on nothing else. From a FILE, not the command line, where any user on the
|
|
# machine could read it out of `ps` while curl runs.
|
|
fetch() {
|
|
if [ -n "$server" ]; then
|
|
curl -fSL -H @"$tmp/auth" -o "$2" "$1"
|
|
else
|
|
curl -fSL -o "$2" "$1"
|
|
fi
|
|
}
|
|
|
|
# The download against the digest its server published, BEFORE it reaches pacman,
|
|
# dpkg or a menu entry: a truncated file installs as something broken rather than
|
|
# failing. The forge path publishes no digest per bundle, so it passes an empty one
|
|
# and this checks nothing there.
|
|
verify() {
|
|
[ -n "$2" ] || return 0
|
|
if have sha256sum; then
|
|
got="$(sha256sum "$1" | cut -d' ' -f1)"
|
|
elif have shasum; then
|
|
got="$(shasum -a 256 "$1" | cut -d' ' -f1)"
|
|
else
|
|
die "can't check the download: neither sha256sum nor shasum is installed."
|
|
fi
|
|
[ "$got" = "$2" ] || die "the download doesn't match what the server published (sha256 $got, expected $2). Nothing was installed."
|
|
}
|
|
|
|
pkg_sha=""; deb_sha=""; appimage_sha=""
|
|
|
|
if [ -n "$server" ]; then
|
|
# --- resolve from a server ----------------------------------------------------
|
|
# `/api/client/<platform>` is public, so what the server holds is known before any
|
|
# token is asked for. The download URL is BUILT here from the platform id, never read
|
|
# from the reply — the same rule the apps follow (core/src/sync/client.rs): a reply
|
|
# that named some other host would otherwise be fetched, with the token attached.
|
|
say "Finding the Inkwell build $server holds…"
|
|
|
|
# One string field out of the flat JSON object the server returns. sed rather than
|
|
# a parser, for the same reason as the forge path's grep: no jq on most machines.
|
|
jfield() {
|
|
printf '%s' "$1" | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1
|
|
}
|
|
meta() { curl -fsS "$server/api/client/$1" 2>/dev/null || true; }
|
|
|
|
pkg_meta="$(meta linux-pacman)"
|
|
deb_meta="$(meta linux-deb)"
|
|
appimage_meta="$(meta linux-appimage)"
|
|
[ -n "$pkg_meta" ] || [ -n "$deb_meta" ] || [ -n "$appimage_meta" ] ||
|
|
die "$server has no Linux client to install (or isn't an Inkwell server — is the address right?)."
|
|
|
|
pkg_url=""; deb_url=""; appimage_url=""
|
|
if [ -n "$pkg_meta" ]; then
|
|
pkg_url="$server/api/client/linux-pacman/download"; pkg_sha="$(jfield "$pkg_meta" sha256)"
|
|
fi
|
|
if [ -n "$deb_meta" ]; then
|
|
deb_url="$server/api/client/linux-deb/download"; deb_sha="$(jfield "$deb_meta" sha256)"
|
|
fi
|
|
if [ -n "$appimage_meta" ]; then
|
|
appimage_url="$server/api/client/linux-appimage/download"; appimage_sha="$(jfield "$appimage_meta" sha256)"
|
|
fi
|
|
# One build, four bundles: any of them names the version.
|
|
version="$(jfield "$pkg_meta$deb_meta$appimage_meta" version)"
|
|
say "Installing ${version:-unknown} from $server"
|
|
|
|
# The token, from the environment or from the person at the keyboard. Read from the
|
|
# TERMINAL, not stdin: through `curl | sh`, stdin is this script.
|
|
token="${INKWELL_TOKEN:-}"
|
|
prompted=""
|
|
if [ -z "$token" ]; then
|
|
{ [ -r /dev/tty ] && [ -w /dev/tty ]; } ||
|
|
die "the download needs a device token and there's no terminal to ask on; set INKWELL_TOKEN."
|
|
printf 'A device token is needed to download from %s.\n' "$server" > /dev/tty
|
|
printf 'Make one in the web app under Account → Linked devices (it is revoked once the download is done).\n' > /dev/tty
|
|
printf 'Token: ' > /dev/tty
|
|
stty -echo < /dev/tty 2>/dev/null || true
|
|
IFS= read -r token < /dev/tty || true
|
|
stty echo < /dev/tty 2>/dev/null || true
|
|
printf '\n' > /dev/tty
|
|
prompted=1
|
|
fi
|
|
[ -n "$token" ] || die "no token given."
|
|
# The server's tokens are URL-safe base64. Anything else is a paste gone wrong, and a
|
|
# newline in it would be a second header.
|
|
case "$token" in
|
|
*[!A-Za-z0-9_-]*) die "that doesn't look like a device token (they're letters, digits, - and _)." ;;
|
|
esac
|
|
( umask 077; printf 'Authorization: Bearer %s\n' "$token" > "$tmp/auth" )
|
|
|
|
else
|
|
# --- resolve from the forge, for this channel --------------------------------
|
|
say "Finding the latest Inkwell build on the $channel channel…"
|
|
|
|
# ONE lookup, both channels. Each is a release whose tag never moves and whose assets
|
|
# are pruned to the current build, so the tag alone names the newest build on that
|
|
# channel — which is exactly what an installer wants and what the in-app updater
|
|
# already reads.
|
|
# The channel's RELEASE TAG, which is not always its name: `dev` publishes on the
|
|
# tag `dev-rolling`, because a tag called `dev` would shadow the branch of the same
|
|
# name and break `git push origin dev`. Same mapping as
|
|
# packaging/channel-tag.sh — inlined because this script is fetched alone through a
|
|
# pipe and has nothing to source.
|
|
case "$channel" in
|
|
dev) tag=dev-rolling ;;
|
|
*) tag="$channel" ;;
|
|
esac
|
|
json="$(curl -fsSL "$API/releases/tags/$tag" 2>/dev/null)" ||
|
|
die "the $channel channel has nothing published yet."
|
|
|
|
# Pull asset URLs straight out of the release JSON (no jq). Anchored on the closing
|
|
# quote so a `…AppImage.sig` URL can't be truncated into a match of its own.
|
|
asset_url() {
|
|
printf '%s' "$json" | grep -oE "https?://[^\"]+$1\"" | head -1 | tr -d '"'
|
|
}
|
|
appimage_url="$(asset_url '\.AppImage')"
|
|
deb_url="$(asset_url '\.deb')"
|
|
pkg_url="$(asset_url '\.pkg\.tar\.[a-z]+')"
|
|
version="$(printf '%s' "$json" | grep -oE '"tag_name":"[^"]+"' | head -1 | sed -E 's/.*:"([^"]+)".*/\1/')"
|
|
[ -n "$appimage_url" ] || [ -n "$deb_url" ] || [ -n "$pkg_url" ] ||
|
|
die "the $channel release (${version:-unknown}) has no installable Linux asset."
|
|
say "Installing ${version:-unknown} from the $channel channel"
|
|
fi
|
|
|
|
# A token typed at the prompt was made for this one install; once the bytes are here
|
|
# it has nothing left to do, so it goes. Best-effort: the install does not depend on
|
|
# it, and the person is told if it is still live.
|
|
revoke_token() {
|
|
[ -n "$server" ] && [ -n "$prompted" ] || return 0
|
|
if curl -fsS -o /dev/null -X DELETE -H @"$tmp/auth" "$server/api/auth/devices/self" 2>/dev/null; then
|
|
say "Revoked the download token."
|
|
else
|
|
say "Couldn't revoke the download token; remove it under Account → Linked devices."
|
|
fi
|
|
}
|
|
|
|
# Tell the app which channel it was installed from. The installer is the only thing
|
|
# that knows; without this the app keeps its own `stable` default, and a dev install
|
|
# would check the stable feed, which advertises an OLDER version, and report "up to
|
|
# date" forever.
|
|
#
|
|
# A plain file rather than a write into the app's SQLite store: shell has no business
|
|
# knowing that schema, and a file it can't misread is the narrowest possible contract.
|
|
# The app reads it at startup (src-tauri/src/update.rs, INSTALL_MARKER) and only acts
|
|
# when the value CHANGED, so switching channel in the app isn't undone on next launch.
|
|
#
|
|
# The directory is Tauri's app-data dir for identifier com.fabledsword.inkwell;
|
|
# both sides hardcode it, so a change to the identifier has to change both.
|
|
#
|
|
# From a server, the sibling `install-server` is written instead: which server the app
|
|
# came from, for the updater to follow. The channel marker is
|
|
# left alone on that path, because a server's channel is whatever its image was built
|
|
# with and nothing here can know it.
|
|
record_channel() {
|
|
marker_dir="${XDG_DATA_HOME:-$HOME/.local/share}/com.fabledsword.inkwell"
|
|
# Best-effort: a failure here costs the channel setting, not the install, and a
|
|
# native install run as root would only be writing into root's home anyway.
|
|
mkdir -p "$marker_dir" 2>/dev/null || return 0
|
|
if [ -n "$server" ]; then
|
|
printf '%s\n' "$server" > "$marker_dir/install-server" 2>/dev/null || true
|
|
else
|
|
printf '%s\n' "$channel" > "$marker_dir/install-channel" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Both native paths install system-wide, so they need root. Resolved once here
|
|
# rather than duplicated per branch; the AppImage path below never calls this.
|
|
need_root() {
|
|
if [ "$(id -u)" -eq 0 ]; then sudo=""; else
|
|
have sudo || die "a native install needs root; re-run as root or install sudo."
|
|
sudo="sudo"
|
|
fi
|
|
say "Installing (you may be prompted for your password)…"
|
|
}
|
|
|
|
# Both native paths are package-manager-owned, so the app cannot replace itself
|
|
# in place (update.rs refuses, by design). Say so at the end of those paths rather
|
|
# than letting someone discover it from a greyed-out button.
|
|
native_update_note() {
|
|
printf ' A package-manager install can'\''t update itself in-app.\n'
|
|
if [ -n "$server" ]; then
|
|
printf ' Re-run this script from %s to move to the build it holds.\n' "$server"
|
|
elif [ "$channel" = "dev" ]; then
|
|
printf ' Re-run this script with --channel dev to move to a newer dev build.\n'
|
|
else
|
|
printf ' Re-run this script to move to a newer release.\n'
|
|
fi
|
|
}
|
|
|
|
# --- native pacman path (Arch/CachyOS/Manjaro) ------------------------------
|
|
# Preferred over the AppImage on Arch: pacman pulls webkit2gtk-4.1 itself and the
|
|
# app then runs against the host graphics stack, which is what avoids the
|
|
# EGL_BAD_PARAMETER black window. It also means the
|
|
# app is tracked by the package manager and uninstalls cleanly.
|
|
if have pacman && [ -n "$pkg_url" ]; then
|
|
say "Arch-family system detected — installing the native pacman package"
|
|
# Keep the published filename: pacman -U expects a *.pkg.tar.* name and refuses
|
|
# a file that doesn't look like a package, whatever its actual contents.
|
|
# From a server the URL ends in `/download`, so the name comes from the platform.
|
|
if [ -n "$server" ]; then pkg_file="$tmp/inkwell.pkg.tar.zst"; else pkg_file="$tmp/$(basename "$pkg_url")"; fi
|
|
fetch "$pkg_url" "$pkg_file"
|
|
verify "$pkg_file" "$pkg_sha"
|
|
revoke_token
|
|
need_root
|
|
$sudo pacman -U --noconfirm "$pkg_file"
|
|
record_channel
|
|
say "Done. Launch Inkwell from your application menu, or run inkwell."
|
|
native_update_note
|
|
exit 0
|
|
fi
|
|
|
|
# --- native .deb path (Debian/Ubuntu) ---------------------------------------
|
|
if have dpkg && have apt-get && [ -n "$deb_url" ]; then
|
|
say "Debian-family system detected — installing the native .deb"
|
|
fetch "$deb_url" "$tmp/inkwell.deb"
|
|
verify "$tmp/inkwell.deb" "$deb_sha"
|
|
revoke_token
|
|
need_root
|
|
# apt-get resolves the .deb's dependencies (webkit2gtk etc.). dpkg is the
|
|
# fallback if this apt is too old for local-file installs — it leaves the deps
|
|
# unconfigured, so `apt-get -f install` is what actually completes that path.
|
|
$sudo apt-get install -y "$tmp/inkwell.deb" ||
|
|
{ $sudo dpkg -i "$tmp/inkwell.deb" || true; $sudo apt-get -f install -y; }
|
|
record_channel
|
|
say "Done. Launch Inkwell from your application menu."
|
|
native_update_note
|
|
exit 0
|
|
fi
|
|
|
|
# --- universal AppImage path (user-local, no sudo) --------------------------
|
|
# Install into ~/Applications/Inkwell.AppImage — the SAME location the app's
|
|
# own self-integration uses (src/integration.rs) — so the running app sees
|
|
# itself already installed and never makes a second copy or menu entry.
|
|
say "Installing the de-bundled AppImage (user-local, no sudo)"
|
|
[ -n "$appimage_url" ] || die "the $channel release (${version:-unknown}) has no AppImage asset."
|
|
|
|
apps_dir="$HOME/Applications"
|
|
dest="$apps_dir/Inkwell.AppImage"
|
|
mkdir -p "$apps_dir"
|
|
say "Downloading the AppImage…"
|
|
fetch "$appimage_url" "$tmp/Inkwell.AppImage"
|
|
verify "$tmp/Inkwell.AppImage" "$appimage_sha"
|
|
revoke_token
|
|
chmod +x "$tmp/Inkwell.AppImage"
|
|
mv -f "$tmp/Inkwell.AppImage" "$dest"
|
|
|
|
# Menu entry — written to match integration.rs verbatim (same paths + fields),
|
|
# so the app reports is_integrated=true and won't duplicate it.
|
|
apps_menu="$HOME/.local/share/applications"
|
|
icons_dir="$HOME/.local/share/icons"
|
|
mkdir -p "$apps_menu" "$icons_dir"
|
|
|
|
# Best-effort: pull the real icon out of the AppImage (.DirIcon) so the menu
|
|
# entry looks right immediately. Extraction is a non-GUI unsquash (no FUSE, no
|
|
# black-window risk); if it fails we fall back to the themed name and the app
|
|
# writes its embedded icon on first launch anyway.
|
|
icon_ref="inkwell"
|
|
if ( cd "$tmp" && "$dest" --appimage-extract .DirIcon >/dev/null 2>&1 ) \
|
|
&& cp -L "$tmp/squashfs-root/.DirIcon" "$icons_dir/inkwell.png" 2>/dev/null; then
|
|
icon_ref="$icons_dir/inkwell.png"
|
|
fi
|
|
rm -rf "$tmp/squashfs-root" 2>/dev/null || true
|
|
|
|
# StartupWMClass is the BINARY name, not the product name and not the AppImage
|
|
# filename: the AppImage's AppRun execs usr/bin/inkwell, and GTK derives
|
|
# WM_CLASS from whatever it ends up running. Anything else here means the window
|
|
# never associates with this entry and the taskbar shows a second, generic icon.
|
|
cat > "$apps_menu/inkwell.desktop" <<EOF
|
|
[Desktop Entry]
|
|
Type=Application
|
|
Name=Inkwell
|
|
Comment=Capture a fleeting thought in a second
|
|
Exec=$dest %U
|
|
Icon=$icon_ref
|
|
Terminal=false
|
|
Categories=Utility;Office;
|
|
StartupWMClass=inkwell
|
|
EOF
|
|
|
|
have update-desktop-database && update-desktop-database "$apps_menu" >/dev/null 2>&1 || true
|
|
|
|
# Convenience CLI launcher.
|
|
mkdir -p "$HOME/.local/bin"
|
|
ln -sf "$dest" "$HOME/.local/bin/inkwell"
|
|
|
|
record_channel
|
|
|
|
say "Installed to $dest"
|
|
printf ' Launch it from your application menu, or run \033[1minkwell\033[0m'
|
|
printf ' (if ~/.local/bin is on your PATH).\n'
|
|
# This is the one path where the app can update itself, so say what it will follow.
|
|
if [ -z "$server" ] && [ "$channel" = "dev" ]; then
|
|
printf ' In-app updates will follow the \033[1mdev\033[0m channel.'
|
|
printf ' Change it in Sync → App updates.\n'
|
|
fi
|