diff --git a/I38.md b/I38.md index 24dd026..ed71e92 100644 --- a/I38.md +++ b/I38.md @@ -382,6 +382,21 @@ To reconfigure I38 completely, run the `i38.sh` script again. *GNOME/MATE comparison:* Much more text-based configuration compared to the graphical settings dialogs in GNOME/MATE. +## Updating I38 + +I38 checks for updates in the background during your desktop session. If a newer version is found, I38 sends one notification and then stops checking for the rest of that session. If the server is unavailable or your network is down, I38 stays quiet and tries again later. + +To update I38 on this system: + +1. Open a terminal with `MODKEY` + `Return`. +2. Type `cd "I38CHECKOUTPATH"` and press Enter. +3. Type `git pull` and press Enter. +4. Type `./i38.sh` and press Enter. + +Running `./i38.sh` regenerates your I38 configuration and refreshes the installed scripts. Your personal shortcuts in `~/.config/i3/customizations` are preserved. + +If you only need to copy the latest scripts and do not want to regenerate the full i3 configuration, you can use `./i38.sh -u` instead. + ## Getting Help If you need assistance with I38, you can: diff --git a/i38.sh b/i38.sh index 3b1b29b..dde229e 100755 --- a/i38.sh +++ b/i38.sh @@ -14,6 +14,8 @@ i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/i3" i3msg="i3-msg" configFile="${PWD}/I38_preferences.conf" +i38VersionFile="${i3Path}/i38-version" +i38CheckoutPath="${PWD}" # Dialog accessibility export DIALOGOPTS='--no-lines --visit-items' @@ -449,6 +451,7 @@ update_scripts() { existingPinHash="$screenlockPinHash" fi cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Updating scripts..." -1 -1 + write_i38_version write_desktop_shortcuts_template if [[ -n "$existingPinHash" ]]; then screenlockPinHash="$existingPinHash" @@ -521,6 +524,22 @@ EOF return 0 } +i38_current_commit() { + if ! command -v git &> /dev/null; then + return 1 + fi + git -C "$PWD" rev-parse --verify HEAD 2> /dev/null +} + +write_i38_version() { + mkdir -p "${i3Path}" + if i38Commit="$(i38_current_commit)"; then + printf '%s\n' "$i38Commit" > "$i38VersionFile" + else + rm -f "$i38VersionFile" + fi +} + # Array of command line arguments declare -A command=( @@ -859,6 +878,7 @@ write_waytray_config # Create the i3 configuration directory. mkdir -p "${i3Path}" +write_i38_version write_desktop_shortcuts_template # Move scripts into place cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Moving scripts into place and writing config..." -1 -1 @@ -1254,6 +1274,7 @@ if command -v waytray-daemon &> /dev/null ; then fi echo "exec_always --no-startup-id ${i3Path}/scripts/i38-clipboard.py --daemon" echo "exec_always --no-startup-id ${i3Path}/scripts/desktop.sh" +echo "exec --no-startup-id ${i3Path}/scripts/i38-update-check.sh" if [[ $dex -eq 0 ]]; then echo '# Start XDG autostart .desktop files using dex. See also' echo '# https://wiki.archlinux.org/index.php/XDG_Autostart' @@ -1474,7 +1495,8 @@ sed -i -e "s|BROWSER|${webBrowser}|g" \ -e "s|SCREENREADER|${screenReader}|g" \ -e "s|RATPOISONKEY|${escapeKey}|g" \ -e "s|TEXTEDITOR|${textEditor}|g" \ - -e "s|FILEBROWSER|${fileBrowser}|g" "${i3Path}/I38.html" + -e "s|FILEBROWSER|${fileBrowser}|g" \ + -e "s|I38CHECKOUTPATH|${i38CheckoutPath}|g" "${i3Path}/I38.html" # Create the firstrun file touch "${i3Path}/firstrun" diff --git a/scripts/i38-update-check.sh b/scripts/i38-update-check.sh new file mode 100755 index 0000000..61b96d0 --- /dev/null +++ b/scripts/i38-update-check.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +# This file is part of I38. +# +# I38 is free software: you can redistribute it and/or modify it under the terms +# of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. + +set -u + +remoteUrl="${I38_UPDATE_REMOTE_URL:-https://git.stormux.org/storm/I38}" +remoteRef="${I38_UPDATE_REMOTE_REF:-refs/heads/master}" +initialDelay="${I38_UPDATE_INITIAL_DELAY:-90}" +checkInterval="${I38_UPDATE_CHECK_INTERVAL:-21600}" +notifyInterval="${I38_UPDATE_NOTIFY_INTERVAL:-604800}" +networkTimeout="${I38_UPDATE_NETWORK_TIMEOUT:-20}" + +scriptDir="$(unset CDPATH; cd -- "$(dirname -- "$0")" && pwd)" +i3Dir="$(dirname -- "$scriptDir")" +versionFile="${I38_VERSION_FILE:-${i3Dir}/i38-version}" +stateDir="${XDG_STATE_HOME:-${HOME}/.local/state}/I38" +notifyState="${stateDir}/update-check-notified" + +valid_commit() { + [[ "${1:-}" =~ ^[0-9a-fA-F]{40}$ ]] +} + +read_installed_commit() { + local commit + + [[ -r "$versionFile" ]] || return 1 + IFS= read -r commit < "$versionFile" || return 1 + valid_commit "$commit" || return 1 + printf '%s\n' "$commit" +} + +read_remote_commit() { + local output commit + + if command -v timeout > /dev/null; then + output="$(timeout "$networkTimeout" git ls-remote "$remoteUrl" "$remoteRef" 2> /dev/null)" || return 1 + else + output="$(git ls-remote "$remoteUrl" "$remoteRef" 2> /dev/null)" || return 1 + fi + commit="${output%%[[:space:]]*}" + valid_commit "$commit" || return 1 + printf '%s\n' "$commit" +} + +already_notified() { + local remoteCommit="$1" + local lastCommit lastTime now + + [[ -r "$notifyState" ]] || return 1 + { + IFS= read -r lastCommit + IFS= read -r lastTime + } < "$notifyState" || return 1 + + [[ "$lastCommit" == "$remoteCommit" ]] || return 1 + [[ "$lastTime" =~ ^[0-9]+$ ]] || return 1 + now="$(date +%s)" + (( now - lastTime < notifyInterval )) +} + +remember_notification() { + local remoteCommit="$1" + + mkdir -p "$stateDir" || return 0 + { + printf '%s\n' "$remoteCommit" + date +%s + } > "$notifyState" 2> /dev/null || true +} + +send_update_notification() { + local remoteCommit="$1" + + notify-send \ + "I38 update available" \ + "A newer I38 version is available. Update your checkout, then run i38.sh -u or rerun i38.sh to refresh your setup." \ + 2> /dev/null || return 1 + remember_notification "$remoteCommit" +} + +if ! command -v git > /dev/null || ! command -v notify-send > /dev/null; then + exit 0 +fi + +sleep "$initialDelay" + +while :; do + installedCommit="$(read_installed_commit)" || exit 0 + remoteCommit="$(read_remote_commit)" + remoteStatus=$? + + if [[ $remoteStatus -eq 0 && "$remoteCommit" != "$installedCommit" ]]; then + if ! already_notified "$remoteCommit"; then + send_update_notification "$remoteCommit" + exit 0 + fi + fi + + sleep "$checkInterval" +done