aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorArpit Chakladar <arpitchakladar+git@gmail.com>2026-09-02 17:02:13 +0530
committerArpit Chakladar <arpitchakladar+git@gmail.com>2026-09-02 17:02:13 +0530
commit627c1f723302f949e28d9df5a3acb380186b4c44 (patch)
tree6c2f2a9506a5926fd5176061706db9f3641803f1 /modules
parent9f214ab3276ff432f54a07d7d79f87c1a8be0991 (diff)
parent894f96eb131427041c2bb726b7b798edf4f683dc (diff)
downloadhome-manager-config-627c1f723302f949e28d9df5a3acb380186b4c44.tar.gz
home-manager-config-627c1f723302f949e28d9df5a3acb380186b4c44.zip
Merge branch 'calcurse'
Diffstat (limited to 'modules')
-rw-r--r--modules/office/calcurse/calcurse-sync.sh279
-rw-r--r--modules/office/calcurse/conf42
-rw-r--r--modules/office/calcurse/default.nix93
-rw-r--r--modules/office/calcurse/hooks/post-save16
-rw-r--r--modules/office/calcurse/hooks/pre-load16
-rw-r--r--modules/office/calcurse/keys51
-rw-r--r--modules/private/calcurse.example.nix7
7 files changed, 500 insertions, 4 deletions
diff --git a/modules/office/calcurse/calcurse-sync.sh b/modules/office/calcurse/calcurse-sync.sh
new file mode 100644
index 0000000..8323bff
--- /dev/null
+++ b/modules/office/calcurse/calcurse-sync.sh
@@ -0,0 +1,279 @@
+#!/usr/bin/env bash
+#
+# Keeps calcurse's data directory in git, so you can back it up / sync
+# it across machines via a remote (e.g. a private GitHub repo).
+#
+# Usage:
+# calcurse-sync init [remote-url] Turn the calcurse data dir into a git
+# repo and (optionally) attach a remote.
+# Safe to re-run — no-ops if a repo
+# already exists. If no remote is given,
+# falls back to $CALCURSE_SYNC_REMOTE,
+# then (if interactive) a prompt.
+# calcurse-sync sync Commit any changes and push. Initializes
+# the repo automatically on first run.
+# calcurse-sync pull Pull down changes from the remote.
+# calcurse-sync status Show git status of the data directory.
+# calcurse-sync remote <url> Add/update the remote after the fact.
+# calcurse-sync help Show this message.
+#
+# Defaults to $XDG_DATA_HOME/calcurse (usually ~/.local/share/calcurse).
+# Override with $CALCURSE_DATA_DIR if yours lives elsewhere.
+# Override the default remote used on first init with $CALCURSE_SYNC_REMOTE.
+
+set -euo pipefail
+
+DATA_DIR="${CALCURSE_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/calcurse}"
+BRANCH="main"
+SYNC_TIMEOUT="8" # seconds before a network op gives up
+
+info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
+warn() { printf '\033[1;33m==> warning:\033[0m %s\n' "$*" >&2; }
+error() { printf '\033[1;31m==> error:\033[0m %s\n' "$*" >&2; }
+die() { error "$*"; exit 1; }
+
+require_git() {
+ command -v git >/dev/null 2>&1 \
+ || die "git is not installed or not on PATH. Install it and try again."
+}
+
+require_data_dir() {
+ if [ ! -d "$DATA_DIR" ]; then
+ die "calcurse data directory not found at: $DATA_DIR
+Run calcurse at least once so it can create its data files, or set
+CALCURSE_DATA_DIR to point at the right location."
+ fi
+}
+
+require_repo() {
+ if [ ! -d "$DATA_DIR/.git" ]; then
+ die "no git repository found in $DATA_DIR
+Run 'calcurse-sync init' first to set one up."
+ fi
+}
+
+repo_exists() {
+ [ -d "$DATA_DIR/.git" ]
+}
+
+has_remote() {
+ git -C "$DATA_DIR" remote get-url origin >/dev/null 2>&1
+}
+
+has_upstream() {
+ git -C "$DATA_DIR" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
+}
+
+# e.g. "25th May, 2026 at 12:34 PM"
+readable_timestamp() {
+ local day suffix month_year time_part
+
+ day=$(date +%-d)
+ case "$day" in
+ 1|21|31) suffix="st" ;;
+ 2|22) suffix="nd" ;;
+ 3|23) suffix="rd" ;;
+ *) suffix="th" ;;
+ esac
+
+ month_year=$(date +"%B, %Y")
+ time_part=$(date +"%I:%M %p" | sed 's/^0//')
+
+ printf '%s%s %s at %s' "$day" "$suffix" "$month_year" "$time_part"
+}
+
+# Initializes the git repo, makes an initial commit if there's anything to
+# commit, and attaches a remote. The remote is resolved in this order:
+# explicit arg > $CALCURSE_SYNC_REMOTE > interactive prompt (tty only) >
+# skipped. Shared by `init` (explicit/manual) and `sync`/activation
+# (automatic, first-run-on-a-new-device).
+do_init() {
+ local remote_url="${1:-${CALCURSE_SYNC_REMOTE:-}}"
+
+ info "No git repository found in $DATA_DIR — initializing one."
+ git -C "$DATA_DIR" init -b "$BRANCH" >/dev/null
+
+ # calcurse sometimes drops lock/swap files in here; keep them out of git
+ if [ ! -e "$DATA_DIR/.gitignore" ]; then
+ cat > "$DATA_DIR/.gitignore" <<'EOF'
+*.lock
+*.swp
+EOF
+ fi
+
+ git -C "$DATA_DIR" add -A
+ if ! git -C "$DATA_DIR" diff --cached --quiet; then
+ git -C "$DATA_DIR" commit -m "sync: $(readable_timestamp)" >/dev/null
+ info "Created initial commit."
+ else
+ warn "Nothing to commit yet (data directory is empty)."
+ fi
+
+ if [ -z "$remote_url" ] && [ -t 0 ]; then
+ read -r -p "Remote URL to push to (leave blank to skip): " remote_url
+ fi
+
+ if [ -n "$remote_url" ]; then
+ git -C "$DATA_DIR" remote add origin "$remote_url"
+ info "Pushing initial commit to $remote_url"
+ if git -C "$DATA_DIR" push -u origin "$BRANCH"; then
+ info "Repo initialized and pushed."
+ else
+ error "Push failed. The remote was still added — check the URL and your"
+ error "credentials, then try 'calcurse-sync sync' again."
+ exit 1
+ fi
+ else
+ warn "No remote configured. This repo will only track history locally"
+ warn "until you run 'calcurse-sync remote <url>' (or set CALCURSE_SYNC_REMOTE)."
+ fi
+}
+
+cmd_init() {
+ require_git
+ require_data_dir
+
+ if repo_exists; then
+ info "Repository already exists in $DATA_DIR — nothing to do."
+ return 0
+ fi
+
+ do_init "${1:-}"
+}
+
+cmd_remote() {
+ require_git
+ require_repo
+
+ local url="${1:-}"
+ [ -n "$url" ] || die "usage: calcurse-sync remote <url>"
+
+ if has_remote; then
+ git -C "$DATA_DIR" remote set-url origin "$url"
+ info "Updated remote 'origin' to $url"
+ else
+ git -C "$DATA_DIR" remote add origin "$url"
+ info "Added remote 'origin' pointing to $url"
+ fi
+}
+
+cmd_sync() {
+ require_git
+ require_data_dir
+
+ if ! repo_exists; then
+ do_init
+ return 0
+ fi
+
+ cd "$DATA_DIR"
+ git add -A
+
+ if ! git diff --cached --quiet; then
+ git commit -m "sync: $(readable_timestamp)" >/dev/null
+ info "Committed changes."
+ else
+ info "No local changes to commit."
+ fi
+
+ if ! has_remote; then
+ warn "No remote configured for this repo — commit saved locally only."
+ warn "Run 'calcurse-sync remote <url>' to enable pushing."
+ return 0
+ fi
+
+ if has_upstream && [ -z "$(git log '@{u}..HEAD' --oneline)" ]; then
+ info "Already up to date with the remote."
+ return 0
+ fi
+
+ info "Pushing to remote..."
+ if has_upstream; then
+ push_cmd=(timeout "$SYNC_TIMEOUT" git push)
+ else
+ push_cmd=(timeout "$SYNC_TIMEOUT" git push -u origin "$BRANCH")
+ fi
+
+ if "${push_cmd[@]}"; then
+ info "Sync complete."
+ else
+ status=$?
+ if [ "$status" -eq 124 ]; then
+ warn "Push timed out after ${SYNC_TIMEOUT}s — commit saved locally, will"
+ warn "retry on next sync."
+ return 0
+ fi
+ error "Push failed."
+ error "This usually means the remote has commits you don't have locally"
+ error "(e.g. synced from another machine). Run 'calcurse-sync pull'"
+ error "to merge them in, then sync again. If that's not it, check your"
+ error "network connection and git credentials."
+ exit "$status"
+ fi
+}
+
+cmd_pull() {
+ require_git
+ require_data_dir
+ require_repo
+
+ if ! has_remote; then
+ warn "no remote configured for this repo — skipping pull."
+ return 0
+ fi
+
+ cd "$DATA_DIR"
+
+ if [ -n "$(git status --porcelain)" ]; then
+ warn "You have uncommitted changes in $DATA_DIR."
+ warn "Run 'calcurse-sync sync' first so a pull can't clobber them."
+ die "aborting pull to avoid data loss."
+ fi
+
+ info "Pulling from remote (timeout ${SYNC_TIMEOUT}s)..."
+ if timeout "$SYNC_TIMEOUT" git pull --rebase origin "$BRANCH"; then
+ info "Up to date."
+ else
+ status=$?
+ if [ "$status" -eq 124 ]; then
+ warn "Pull timed out after ${SYNC_TIMEOUT}s — continuing with local data."
+ warn "Set \$CALCURSE_SYNC_TIMEOUT to a higher value if your connection"
+ warn "is just slow, not down."
+ return 0
+ fi
+ error "Pull failed — likely a merge conflict."
+ error "Resolve conflicts manually in $DATA_DIR, then run:"
+ error " git -C \"$DATA_DIR\" rebase --continue"
+ error "(or 'git rebase --abort' to back out)."
+ exit 1
+ fi
+}
+
+cmd_status() {
+ require_git
+ require_data_dir
+ require_repo
+
+ git -C "$DATA_DIR" status
+}
+
+cmd_help() {
+ sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
+}
+
+main() {
+ local sub="${1:-help}"
+ shift || true
+
+ case "$sub" in
+ init) cmd_init "$@" ;;
+ sync) cmd_sync "$@" ;;
+ pull) cmd_pull "$@" ;;
+ status) cmd_status "$@" ;;
+ remote) cmd_remote "$@" ;;
+ help|-h|--help) cmd_help ;;
+ *) die "unknown command: $sub (try 'calcurse-sync help')" ;;
+ esac
+}
+
+main "$@"
diff --git a/modules/office/calcurse/conf b/modules/office/calcurse/conf
new file mode 100644
index 0000000..98aa9af
--- /dev/null
+++ b/modules/office/calcurse/conf
@@ -0,0 +1,42 @@
+# Appearance
+appearance.calendarview=monthly
+appearance.compactpanels=no
+appearance.defaultpanel=calendar
+appearance.layout=1
+appearance.headerline=yes
+appearance.eventseparator=yes
+appearance.dayseparator=yes
+appearance.emptyline=yes
+appearance.emptyday=--
+appearance.notifybar=yes
+appearance.sidebarwidth=0
+appearance.theme=green on black
+appearance.todoview=show-completed
+appearance.headingpos=right-justified
+
+# Daemon
+daemon.enable=no
+daemon.log=no
+
+# Format
+format.inputdate=1
+format.notifydate=%a %F
+format.notifytime=%T
+format.appointmenttime=%H:%M
+format.outputdate=%D
+format.dayheading=%B %e, %Y
+
+# General
+general.autogc=no
+general.autosave=yes
+general.confirmdelete=yes
+general.confirmquit=yes
+general.firstdayofweek=monday
+general.multipledays=yes
+general.periodicsave=0
+general.systemevents=yes
+
+# Notification
+notification.warning=300
+notification.command=printf '\a'
+notification.notifyall=flagged-only
diff --git a/modules/office/calcurse/default.nix b/modules/office/calcurse/default.nix
index 03bf9c1..c72896e 100644
--- a/modules/office/calcurse/default.nix
+++ b/modules/office/calcurse/default.nix
@@ -5,18 +5,103 @@
pkgs,
...
}:
+let
+ calcurseSync = pkgs.writeShellScriptBin "calcurse-sync" (builtins.readFile ./calcurse-sync.sh);
+
+ calcursePackage = pkgs.symlinkJoin {
+ name = "calcurse-wrapped";
+ paths = [
+ pkgs.bash
+ pkgs.calcurse
+ ]
+ ++ lib.optionals config.development.nixvim.enable [ config.development.nixvim.package ]
+ ++ lib.optionals config.office.calcurse.sync.enable [ calcurseSync ];
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ postBuild = ''
+ ${lib.optionalString config.development.nixvim.enable ''
+ wrapProgram $out/bin/calcurse --set PAGER "nvim"
+ ''}
+ ${lib.optionalString config.office.calcurse.sync.enable ''
+ wrapProgram $out/bin/calcurse-sync \
+ --prefix PATH : ${
+ lib.makeBinPath [
+ config.development.git.package
+ pkgs.coreutils
+ pkgs.gnused
+ ]
+ } \
+ ${lib.optionalString (config.office.calcurse.sync.remote != null) ''
+ --set CALCURSE_SYNC_REMOTE ${lib.escapeShellArg config.office.calcurse.sync.remote}
+ ''}
+ ''}
+ '';
+ meta = {
+ mainProgram = "calcurse";
+ };
+ };
+in
{
options.office.calcurse = {
enable = lib.mkEnableOption "Enables calcurse.";
package = lib.mkOption {
type = lib.types.package;
readOnly = true;
- default = pkgs.calcurse;
+ default = calcursePackage;
description = "The calcurse package to use.";
};
+ sync = {
+ enable = lib.mkEnableOption "Enables git-backed syncing of the calcurse data directory (builds calcurse-sync, installs the sync hooks, and runs an initial 'calcurse-sync init' on activation).";
+ remote = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Git remote URL for the calcurse data directory. When set, calcurse-sync uses it automatically on first init instead of prompting.";
+ };
+ };
};
- config = lib.mkIf config.office.calcurse.enable {
- home.packages = [ config.office.calcurse.package ];
- };
+ config = lib.mkIf config.office.calcurse.enable (
+ lib.mkMerge [
+ {
+ home.packages = [ config.office.calcurse.package ];
+ xdg.configFile."calcurse/conf" = {
+ source = ./conf;
+ force = true;
+ };
+ xdg.configFile."calcurse/keys" = {
+ source = ./keys;
+ force = true;
+ };
+ }
+ (lib.mkIf config.office.calcurse.sync.enable {
+ xdg.configFile."calcurse/hooks/pre-load" = {
+ source = ./hooks/pre-load;
+ executable = true;
+ force = true;
+ };
+ xdg.configFile."calcurse/hooks/post-save" = {
+ source = ./hooks/post-save;
+ executable = true;
+ force = true;
+ };
+
+ programs.git.includes = lib.mkIf config.development.git.enable [
+ {
+ condition = "gitdir:${config.xdg.dataHome}/calcurse/";
+ contents = {
+ user = {
+ name = "Calcurse of ${config.home.username}";
+ email = "${config.home.username}@calcurse.localhost";
+ };
+ commit.gpgSign = false;
+ tag.gpgSign = false;
+ };
+ }
+ ];
+
+ home.activation.calcurseSyncInit = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
+ run ${config.office.calcurse.package}/bin/calcurse-sync init || true
+ '';
+ })
+ ]
+ );
}
diff --git a/modules/office/calcurse/hooks/post-save b/modules/office/calcurse/hooks/post-save
new file mode 100644
index 0000000..5d2ab1d
--- /dev/null
+++ b/modules/office/calcurse/hooks/post-save
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# Commits and pushes the calcurse data dir after every save.
+#
+# Hooks hand calcurse's terminal over to whatever they run and calcurse
+# waits for them to exit before redrawing, so this backgrounds the actual
+# sync instead of blocking the UI on a git push. Output goes to a log
+# file rather than stdout/stderr, since hooks aren't meant to be
+# interactive.
+
+log="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse/hooks.log"
+
+(
+ printf '[%s] post-save\n' "$(date '+%Y-%m-%d %H:%M:%S')"
+ calcurse-sync sync
+) >>"$log" 2>&1 &
+disown
diff --git a/modules/office/calcurse/hooks/pre-load b/modules/office/calcurse/hooks/pre-load
new file mode 100644
index 0000000..bd87af3
--- /dev/null
+++ b/modules/office/calcurse/hooks/pre-load
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# Pulls the latest calcurse data before calcurse reads it, so a fresh
+# session (or a ^R reload) starts from wherever you last left off on
+# another machine.
+#
+# Runs synchronously, unlike post-save, since the point is to have
+# up-to-date data before it's loaded. Never fails calcurse's startup,
+# though: if the pull can't complete (offline, no remote configured
+# yet, a conflict), calcurse just opens with whatever's on disk.
+
+log="${XDG_DATA_HOME:-$HOME/.local/share}/calcurse/hooks.log"
+
+{
+ printf '[%s] pre-load\n' "$(date '+%Y-%m-%d %H:%M:%S')"
+ calcurse-sync pull
+} >>"$log" 2>&1 || true
diff --git a/modules/office/calcurse/keys b/modules/office/calcurse/keys
new file mode 100644
index 0000000..1f45145
--- /dev/null
+++ b/modules/office/calcurse/keys
@@ -0,0 +1,51 @@
+# Calcurse keys configuration file
+# actions and valid keys; bindings are `action key1 key2 ...` lines.
+
+generic-cancel ESC
+generic-select SPC
+generic-credits @
+generic-help ?
+generic-quit q Q
+generic-save s S ^S
+generic-reload R
+generic-copy c
+generic-paste p ^V
+generic-change-view TAB
+generic-prev-view KEY_BTAB
+generic-import i I
+generic-export x X
+generic-goto g G
+generic-other-cmd o O
+generic-config-menu C
+generic-redraw ^R
+generic-add-appt ^A
+generic-add-todo ^T
+generic-prev-day T ^H
+generic-next-day t ^L
+generic-prev-week W ^K
+generic-next-week w
+generic-prev-month M
+generic-next-month m
+generic-prev-year Y
+generic-next-year y
+generic-scroll-down ^N
+generic-scroll-up ^P
+generic-goto-today ^G
+generic-command :
+move-right l L RGT
+move-left h H LFT
+move-down j J DWN
+move-up k K UP
+start-of-week 0
+end-of-week $
+add-item a A
+del-item d D
+edit-item e E
+view-item v V RET
+pipe-item |
+flag-item !
+repeat r
+edit-note n N
+view-note >
+raise-priority +
+lower-priority -
diff --git a/modules/private/calcurse.example.nix b/modules/private/calcurse.example.nix
new file mode 100644
index 0000000..16ee9e8
--- /dev/null
+++ b/modules/private/calcurse.example.nix
@@ -0,0 +1,7 @@
+# Calcurse - Template for configuring calcurse, specially syncing
+{ ... }:
+{
+ config = {
+ office.calcurse.sync.remote = "YOUR_REPOSITORY_URL";
+ };
+}