Persona files
Persona files
Built-in personas ship with homebot. Your files go in
$XDG_CONFIG_HOME/homebot/personas/<id>.toml (normally
~/.config/homebot/personas/<id>.toml). A local file with a built-in ID
overrides that built-in. IDs are 1–32 ASCII characters: a lowercase letter
followed by lowercase letters, digits or hyphens. none is reserved for
turning the voice off. Run homebot persona ids for the current list of
messages a persona may reword.
schema = 1
id = "tester" # optional; must match tester.toml
name = "Tester"
emoji = "🧪"
tagline = "A careful eye on the details"
extends = "neatpik" # optional; omitted means the embedded Neatpik
lang = "en" # optional
[colors]
accent = { light = "#31466B", dark = "#AFC4E9" }
ok = { light = "#245D38", dark = "#9DE0AF" }
muted = { light = 240, dark = 248 }
[lines]
"inbox.header.count" = ["{n:# thing needs|# things need} you."]
schema = 1, a name and one emoji are required. id is optional but, when
present, must match the filename. The name is 1–32 characters. The tagline
is one line of at most 100 characters and cannot contain $ or a backtick.
lang is a BCP 47 language tag; English is the default. Colours may be
#RRGGBB or ANSI indices 0–255. Each light/dark token omitted here is
inherited from the parent. extends = "none" stops the chain at that file:
the embedded Neatpik supplies nothing, so a colour the chain does not set
stays the terminal’s own (persona show says terminal default for it), and
a message with no line is said by the catalog’s English. Any other chain ends with the embedded Neatpik, then the catalog’s
English. extends = "neatpik" is the default, so writing it is the same as
omitting it: both mean the embedded copy, never a local neatpik.toml (that
file only matters when you pick neatpik itself). Explicit parent chains may
have at most four files, the persona included (the embedded Neatpik does not
count), and cannot cycle. A [colors] that is not a table of accent, ok
and muted, each with light and dark, makes the persona unusable. For
each message ID,
the first file in that chain with usable lines supplies all its variants.
File safety and checks
User files must be regular files of at most 256 KiB. A symlink is accepted only
when it points to a regular file. Pipes, devices, sockets and folders are
refused; reading cannot block the background helper on a pipe. Unknown TOML
keys, invalid syntax, a bad header, an invalid ID or an invalid parent make the
persona unusable. An invalid line variant is dropped; usable variants remain,
and an ID with none falls through to its parent. homebot persona validate <id> loads the whole chain, as show and preview do, and prints every
finding of every file in it to stderr. Each finding starts with the path of the
file it is in, so a problem in a parent is not taken for one in the file you
named. The path is relative to the config directory (personas/tester.toml)
when the file is under it, and absolute otherwise. A missing or broken parent,
a chain of more than four files and a cycle make the persona unusable, like a
bad header in any of its files; invalid lines are reported as errors too, and
validate exits with an error for them, although at load such a variant is
only dropped (as said above). validate <file.toml> does the same for a file
you have not installed yet, with the file as the first of the chain and the
files it extends found by ID among the installed ones. With no argument it
checks the persona in use; when that is none, there is no file to check and
it says the neutral voice is selected. If homebot.yml cannot be loaded, the
config error is reported as persona use reports it and the exit is 1; the
persona named by HOMEBOT_PERSONA, if any, is still checked, and with none set
there is nothing to check. Naming none is refused: it is reserved and has no
file. Findings go to stderr, like every other warning and error of the CLI, so
the stdout of show and preview is only what they print. homebot persona use <id> refuses a persona with any error in its chain, even a single bad line
variant that would otherwise just be dropped, and homebot persona list marks
such a persona [unusable] with its first error, the same verdict use and
validate give: a persona is judged with the files it extends, so one under a
broken parent, in a cycle or in a chain of five files is [unusable] too. When
the error is in another file than the persona’s own, the reason starts with
that file’s path, relative to the config directory when the file is under it.
list still prints every persona when homebot.yml cannot be loaded, reports
that error as use does, exits 1 and marks none as current.
Line IDs must be in homebot persona ids and marked for voice. A variant must
parse, preserve required and fixed placeholders, plural counts (a count the
English shows must still be shown, as a bare {n} or in a plural form with
#: {n} {n:step|steps} and {n:# step|# steps} both show it,
{n:step|steps} alone hides it), leading/trailing whitespace, a leading
{name}: prefix, a leading ,, ; or (, placeholders that touch in the
English ({a}{b}) still touching, punctuation that protects an appended
command, and the original number of line breaks. An empty list of variants is
an error too. The validator rejects control and format characters, including
terminal escapes and bidi controls. In a line, it allows only the same newlines
as the English; the emoji field alone may contain the joiner and variation
marks needed for one emoji. It warns about low colour contrast, uncertain emoji
width, duplicate or long variants, non-English language tags and possible added
facts. A variant beyond the hard length limit is rejected. Run homebot persona preview <id> to see the voice in both themes, or add --event <message-id>
and --all-variants to inspect individual lines. A slot the inbox fills with
another message or a count is filled the same way (3 things need you (1 urgent)); any other slot shows a generic sample such as <profile>.
What is voiced
A persona words what homebot prints, not what it stores. The reasons
homebot link gives for a link it will not make (a name that breaks the
layout rules, something in the way in $HOME) are in the persona’s voice when
printed, in link, link --dry-run, the inbox item and the error of the
link unpack action. They are English, whatever the persona, in anything
homebot stores, and when stdout is piped or --plain is set.
Selection and rotation
HOMEBOT_PERSONA (when nonempty) wins over persona: in homebot.yml; the
default is Neatpik. none selects the neutral voice. A missing or unusable
choice falls back to the embedded Neatpik, even if a broken local
neatpik.toml shadows the built-in. A configured failure raises
persona_broken in the inbox; a failing environment override warns once on a
terminal. The neutral English is used when a message has no usable variant.
For each message ID, homebot picks one variant per process and tries not to
repeat the previous process’s choice. Previous picks are stored per machine
in $XDG_CACHE_HOME/homebot/voice.json; a cache read or write failure only
changes the pick. The main nudge line uses a stable pick keyed on the inbox
items; notifications and the nudge’s urgent suffix always use the same variant
for a persona. Neither changes wording on every refresh. This state
stays outside the home repo.
Generated file fields and voice IDs
These tables come from the accepted persona fields and the message catalog.
They are updated by go run ./tools/gendocs docs.
Fields
| Field | Type | Required | Meaning |
|---|---|---|---|
schema |
integer | yes | File format version; must be 1. |
id |
string | no | Optional ID; when present, it must match the filename. |
name |
string | yes | Speaker name, 1 to 32 characters. |
emoji |
string | yes | One emoji grapheme, without ASCII characters. |
tagline |
string | no | One line, at most 100 characters; no dollar sign or backtick. |
extends |
string | no | Explicit parent ID; none stops explicit parent traversal, then embedded Neatpik remains the fallback. |
lang |
string | no | BCP 47 language tag; English is the default. |
colors |
object | no | Accent, ok and muted light/dark colours; missing values inherit. |
lines |
object | no | Voice message IDs, each with an array of variants. |
Voice IDs
| ID | Kind | English |
|---|---|---|
action.error.unavailable |
error | {name}: this action is not available in this build |
action.link.error.layout |
error | {action} the repo breaks {n:a layout rule|# layout rules}: |
action.link.error.unknown_tree |
error | {action} unknown tree "{tree}" (use {autolink} or {private}) |
action.link.status.changed |
status | {action} {n} changed |
action.link.warning.in_way |
warning | , {n} in the way (see: {cmd}) |
action.run.error.exit |
error | {action} "{command}" exited with |
action.unlock.status.decrypted |
status | {action} {n} decrypted |
action.unlock.status.nothing |
status | {action} nothing to decrypt |
action.unlock.warning.conflicts |
warning | , {n} conflicts (see: {self}{unlock}) |
cleanup.diff.backup_is_link |
status | The backup is a link to {dest}. |
cleanup.diff.kind_mismatch |
status | The backup is a {backup_kind}, but {path} is a {now_kind} now. |
cleanup.diff.only_copy |
status | Nothing is at {path} now; this backup is the only copy. |
cleanup.diff.same |
status | Identical to what is linked there now. |
cleanup.error.merge_not_repo |
error | {path} isn't a file in the repo, so there is nothing to merge into |
cleanup.error.scratch_git |
error | {git} {subcommand} in the merge scratch repo failed. |
cleanup.error.use_not_repo |
error | {path} isn't a file in the repo, so the backup can't go there |
cleanup.hint.terminal |
hint | Review them in a terminal: |
cleanup.status.nothing_taken |
status | Nothing taken from the backup. |
cleanup.status.summary |
status | Deleted {deleted}, used {used}, kept {kept}. |
cleanup.status.summary_merged |
status | Deleted {deleted}, used {used}, kept {kept}; merged lines {merged:# time|# times}. |
cleanup.success.deleted |
success | deleted |
cleanup.success.merged |
success | merged into {repo_path}; {self}{save} commits it |
cleanup.success.none |
success | No backups to review. |
cleanup.success.used |
success | copied into {repo_path} and deleted the backup; {self}{save} commits it |
cli.error.args_hint |
hint | See: {homebot}{command_space} |
cli.error.unknown_action |
error | No action named "{name}". See: |
cli.error.unknown_command |
error | Unknown command "{name}". See: |
cli.error.version_args |
error | {version} takes no arguments, not "{argument}". See: |
config.error.autosave |
error | {file}: {autosave} must be a mapping |
config.error.autosave_commit |
error | {file}: {commit} must be |
config.error.autosave_push |
error | {file}: {push} must be |
config.error.export_duplicate |
error | {file}: {exports_open}{i}{exports_close}: duplicate export name "{name}" (give each one {profiles} that don't overlap) |
config.error.export_mapping |
error | {where}: expected a mapping with |
config.error.export_out_absolute |
error | {where}: {out_key} {out} of export "{name}" is an absolute path; give a path inside the repo, relative to it |
config.error.export_out_git |
error | {where}: {out_key} {out} of export "{name}" is in {git_dir}, which belongs to git |
config.error.export_out_managed |
error | {where}: {out_key} {out} of export "{name}" is {managed}, which homebot keeps for itself; put exports under |
config.error.export_out_managed_inside |
error | {where}: {out_key} {out} of export "{name}" is inside {managed}, which homebot keeps for itself; put exports under |
config.error.export_out_outside |
error | {where}: {out_key} {out} of export "{name}" leads outside the repo |
config.error.export_out_private |
error | {where}: {out_key} {out} of export "{name}" is in {private}, where your secrets are |
config.error.export_out_root |
error | {where}: {out_key} {out} of export "{name}" is the repo itself; name a file or folder inside it |
config.error.export_out_symlink |
error | {where}: {out_key} {out} of export "{name}" goes through a symlink to {target}, which is outside the repo, the repo itself, or a path homebot keeps for itself |
config.error.export_profiles |
error | {where}.{profiles}: expected a list of profile names |
config.error.export_required |
error | {where}: {key_name} is required |
config.error.exports |
error | {file}: {exports} must be a list |
config.error.limits |
error | {file}: {limits} must be a mapping |
config.error.limits_max |
error | {file}: {max_file} must be a positive number |
config.error.link_gone |
error | {file}: "{link}" is gone. Wrap a folder's name in {marks} in {autolink} to keep it a real folder, e.g. |
config.error.name |
error | {where}: {name_key} must be a word, the command you call your homebot by, such as |
config.error.name_syntax |
error | {where}: {name_key} "{value}" must be |
config.error.not_mapping |
error | {file}: expected a mapping at the top level |
config.error.old_name |
error | homebot reads {config_file} now: rename {path} to |
config.error.persona |
error | {where}: {key} must be a string: |
config.error.persona_syntax |
error | {where}: {key} "{value}" must be |
config.error.profiles |
error | {file}: {profiles} must be a mapping |
config.error.profiles_default |
error | {file}: {default_key} must be a profile name |
config.error.profiles_hosts |
error | {file}: {hosts_key} must map host names to profile names |
config.error.string_list |
error | {where}: expected a string or a list of strings |
config.error.triage |
error | {file}: {triage} must be a mapping |
config.error.triage_watch |
error | {file}: {watch} entries end in {star} or {dot_star} (got "{value}") |
config.error.unknown_key |
error | {where}: unknown key "{key_name}" (expected {keys}) |
config.error.unknown_key_under |
error | {where}: unknown key "{key_name}" under {parent} (expected {keys}) |
config.error.unknown_os |
error | {where}: unknown OS "{value}" (use {linux} or {macos}) |
config.error.yaml |
error | {file}: invalid {yaml}: |
conflict.different |
error | already exists with different content |
conflict.file_vs_folder |
error | is a file, but the repo has a folder here |
conflict.folder_vs_file |
error | is a folder, but the repo has a file here |
conflict.symlink_outside |
error | is a symlink to {dest}, outside this repo |
context.error.no_config |
error | No {config} found. Run {self} inside your home repo, or set {env}. |
context.error.no_home |
error | {env} is not set. |
context.error.platform |
error | homebot supports {oses}, not {platform}. |
context.hint.setup |
hint | New here? This clones your home repo or starts one: |
export.error.empty_file |
error | produced an empty file; kept the previous |
export.error.empty_folder |
error | produced an empty folder; kept the previous |
export.error.exit |
error | exited with {code} |
export.error.kept_aside |
error | {error} |
export.error.out_is_file |
error | wrote a folder, but {out} is a file; kept the file. Remove it yourself, or give the export another |
export.error.out_is_folder |
error | wrote a file, but {out} is a folder; kept the folder. Remove it yourself, or give the export another |
export.error.unknown |
error | No export named {names} for this OS. Exports here: {known}. |
export.reason.not_installed |
status | {tool} is not installed |
export.status.none |
status | No exports configured for this OS in {config}. |
export.warning.left_behind |
warning | The temporary folder {path} could not be removed. It is safe to delete. |
fsutil.error.bad_json |
error | {path} is not valid {format}. Fix or delete it (it is machine state, safe to delete). |
git.error.failed |
error | {git} {args} failed. |
helper.error.launchctl |
error | {command} failed. |
helper.error.no_systemd |
error | No systemd user session here, so the hourly helper can't be installed. |
helper.error.systemctl |
error | {command}{args} failed. |
identity.error.config_failed |
error | {git_config} {scope} {setting} {value} failed. |
identity.hint.set_once |
hint | Set it once: |
identity.hint.track |
hint | Track it so every machine gets it: |
identity.prompt.email |
prompt | Your email: |
identity.prompt.name |
prompt | Your name: |
identity.status.skipped |
hint | Skipped. Set it later: |
identity.success.saved |
success | saved {name} <{email}> in |
identity.warning.unknown |
warning | Git doesn't know who you are yet, and {bot} commits as you. |
inbox.backups.title |
inbox | {n:# backup|# backups} that {self}{link} moved aside: compare, then delete or keep |
inbox.behind.title |
inbox | {n:# new commit|# new commits} on the remote |
inbox.blocked.title |
inbox | A commit was refused |
inbox.broken.title |
inbox | Could not check |
inbox.diverged.title |
inbox | This machine and the remote both have new commits |
inbox.done.heading |
status | Done for you since you last looked: |
inbox.export_failing.title |
inbox | Export {name} is failing |
inbox.gitconfig_shadow.detail |
inbox | Git reads it after {repo_config}, so its name, email or signing win. Move it aside; {self}{cleanup} then shows it, and anything this machine needs goes in {local_config}. |
inbox.gitconfig_shadow.title |
inbox | {gitconfig} overrides the git config from the repo |
inbox.header.clear |
success | nothing needs you. Home is in order. |
inbox.header.count |
inbox | {n:# thing needs|# things need} you |
inbox.helper_no_linger.detail |
inbox | systemd ends your timers with your last session; with lingering on it keeps them running |
inbox.helper_no_linger.title |
inbox | The background helper stops when you log out |
inbox.helper_stale.title |
inbox | The background helper last ran |
inbox.link_conflict.title |
inbox | {n:# file is|# files are} in the way of links |
inbox.link_invalid.title |
inbox | {n:# layout error|# layout errors} in the repo; those parts aren't linked |
inbox.link_pending.title |
inbox | {n:# link|# links} to make or fix |
inbox.missing_profile.detail |
inbox | Write the steps that set up this machine |
inbox.missing_profile.title |
inbox | Profile {profile} does not exist yet |
inbox.more |
hint | …and {n} more: |
inbox.name_blocked.title |
inbox | The name {name} is blocked on this machine |
inbox.name_helper_path.title |
inbox | The helper cannot use the name {name} yet |
inbox.name_no_bin.title |
inbox | The name {name} needs a bin directory here |
inbox.name_shadowed.detail |
inbox | The other program is at {path}. |
inbox.name_shadowed_other.title |
inbox | Another program shadows the name |
inbox.name_shadowed_ours.title |
inbox | The name {name} shadows another program |
inbox.never_pushed.title |
inbox | Not backed up yet: nothing pushed to the remote |
inbox.never_unpacked.last |
inbox | (last unpacked: {other}) |
inbox.never_unpacked.title |
inbox | Profile {profile} was never unpacked here |
inbox.next_hint |
hint | Do the first one now: |
inbox.no_gitleaks.title |
inbox | {tool} is missing, so every commit is refused |
inbox.no_guard.title |
inbox | The commit guard is not installed |
inbox.no_helper.detail |
inbox | It seals, exports, links and reminds you every hour |
inbox.no_helper.title |
inbox | The background helper is not installed |
inbox.no_identity.detail |
inbox | Commits by {self}{save} and the hourly autosave fail without a name and email. |
inbox.no_identity.title |
inbox | Git doesn't know who you are |
inbox.no_keys.title |
inbox | Secrets are not set up yet |
inbox.no_profile.detail |
inbox | Set {default_key} or {hosts_key} in |
inbox.no_profile.title |
inbox | No profile chosen for |
inbox.no_remote.title |
inbox | Not backed up: the repo has no remote |
inbox.no_repo.title |
inbox | {repo} is not a git repository |
inbox.no_signing_key.detail |
inbox | Every commit fails until it's here. Restore it ({self}{unlock}), or stop signing on this machine only: |
inbox.no_signing_key.title |
inbox | Commits are signed with {key_path}, which this machine doesn't have |
inbox.notify.body |
notification | {title}{more}. Run: |
inbox.notify.more |
notification | (+{n} more) |
inbox.notify.title |
notification | {bot} needs you |
inbox.nudge |
nudge | {bot}: {n:# thing needs|# things need} you{urgent}, run: |
inbox.profile_changed.title |
inbox | Profile {profile} changed since the last unpack |
inbox.secret_conflict.title |
inbox | {n:# secret|# secrets} changed here and on another machine |
inbox.secret_locked.title |
inbox | {n:# secret is|# secrets are} still locked |
inbox.secret_unsealed.title |
inbox | {n:# secret|# secrets} not sealed yet |
inbox.secret_upstream.title |
inbox | {n:# secret|# secrets} changed on another machine |
inbox.tidy_failed.title |
inbox | Automatic {step_name} failed |
inbox.triage.title |
inbox | {n:# new thing|# new things} to sort: track, ignore or snooze |
inbox.uncommitted.title |
inbox | {n:# uncommitted change|# uncommitted changes} |
inbox.unpack_preview |
inbox | It runs every step without asking; to see them first: {self} |
inbox.unpushed.title |
inbox | {n:# commit|# commits} not pushed |
inbox.unreachable.title |
inbox | Last reached the remote |
inventory.exports.failing |
error | {name} failing |
inventory.exports.never |
status | {name} never |
inventory.exports.none |
status | none configured |
inventory.exports.ok |
status | {name} |
inventory.helper.installed |
status | installed · last ran |
inventory.helper.missing |
warning | not installed ({self}{setup}) |
inventory.links.invalid |
error | , {n} layout errors |
inventory.links.value |
status | {ok} ok, {to_fix} to fix, {blocked} blocked, {locked} locked |
inventory.profile.changed |
warning | , changed since |
inventory.profile.never |
status | never unpacked here |
inventory.profile.none |
warning | none chosen for this machine |
inventory.profile.unpacked |
status | unpacked |
inventory.profile.value |
status | {profile} · |
inventory.repo.never_pushed |
warning | (never pushed: {self}{remote}) |
inventory.repo.no_remote |
warning | (no remote: {self}{remote}) |
inventory.repo.not_repo |
error | not a git repository |
inventory.repo.upstream |
status | → |
inventory.repo.value |
status | {branch}{upstream} · {ahead} ahead, {behind} behind, {uncommitted} uncommitted |
inventory.secrets.count |
status | {n} |
inventory.secrets.no_keys |
warning | no keys yet ({self}{keys_init}) |
inventory.secrets.none |
status | none yet |
inventory.triage.value |
status | {to_sort} to sort, {ignored} ignored |
keys.error.mismatch |
error | The passphrases didn't match; nothing was saved. |
keys.hint.commit |
hint | Commit {identity} and {recipients}: |
keys.hint.passphrase |
hint | Leave it empty and {bot} makes up a strong one and prints it. Save it in your password manager: without it, the secrets in this repo can't be recovered. |
keys.prompt.confirm |
prompt | Confirm passphrase: |
keys.prompt.passphrase |
prompt | Enter passphrase (leave empty to autogenerate a secure one): |
keys.status.autogenerated |
status | using autogenerated passphrase "" |
keys.status.creating |
status | Creating your secrets key. {bot} asks for a passphrase next. |
keys.success.created |
success | Keys created. The public key: |
layout.both_layers |
error | provided by both {providers}; keep it in one place |
layout.dot_parens |
error | "{raw}" names no folder: {dot_names} would link outside the folder it is in; rename it |
layout.double_parens |
error | "{raw}": wrap the name in one pair of {marks}, like "{open}{name}" |
layout.empty_parens |
error | "{marks}" has no name inside |
layout.file_parens |
error | {marks} marks folders only: rename "{raw}" to "" |
layout.inner_file_parens |
error | {path}: {marks} marks folders only: rename "{raw}" to "" |
layout.inside_repo |
error | would be linked at {target}, inside the home repo; rename it |
layout.must_mark |
error | rename "{raw}" to "{open}{raw}{close}"{every_layer}: it holds {inner}, which must not be a link, so it can't be one either |
layout.old_name |
error | "{raw}" is the old layout: rename it to "" |
layout.outside_folder |
error | would be linked at {target}, outside the folder it is in; rename it |
layout.spelling |
error | spelled "{marked}" in {where_marked} but "{plain}" in {where_plain}; use one spelling everywhere |
layout.top_dot |
error | "{raw}": top-level names get their dot from homebot; rename it to "{open}{name}" |
link.error.backup_exists |
error | Cannot move {target} aside: {backup} is already there. Try again in a second. |
link.hint.backup |
hint | To move what is in the way aside (renamed {backup_pattern}, nothing is deleted): |
link.hint.compare |
hint | Compare and delete them when you have a minute: |
link.status.dry_run_ok |
status | {n} already linked |
link.status.interrupted |
status | Stopped; nothing else was moved or linked. Run {self}{link} again to finish. |
link.status.moved_aside |
status | moved aside |
link.summary.base |
status | {n:# change|# changes}, {ok} already linked |
link.summary.blocked |
warning | , {n} blocked |
link.summary.invalid |
error | , {n:# layout error|# layout errors} in the repo |
link.summary.locked |
status | , {n} locked ({self}{unlock}) |
lock.status.nothing |
status | Nothing to seal. |
lock.status.wiped |
status | Sealed {sealed}, removed {n:# plaintext file|# plaintext files}. |
lock.success.sealed |
success | sealed |
lock.warning.kept |
warning | kept {private}{name} (not safely sealed; resolve it first) |
manifest.error.action_name |
error | {where}: "{name}" is not a valid action name |
manifest.error.args |
error | {where}: arguments must be a value or a list of values |
manifest.error.description |
error | {file}: {key} must be text |
manifest.error.include |
error | {file}: {key} must be a profile name or a list of them |
manifest.error.loop |
error | Profiles include each other in a loop: |
manifest.error.name |
error | {where}: {key} must be text |
manifest.error.no_profile |
error | No profile named "{name}" in {dir}. |
manifest.error.no_profile.available |
error | No profile named "{name}" in {dir}. Available: |
manifest.error.not_mapping |
error | {file}: expected a mapping with |
manifest.error.old_extension |
error | Profiles end in {ext} now: rename {path} to {name} |
manifest.error.on |
error | {where}: {key} must be {oses}, or a list of them |
manifest.error.on_os |
error | {where}: unknown OS "{value}" in {key} (use {oses}) |
manifest.error.one_action |
error | {where}: each step needs exactly one action, found |
manifest.error.one_action.none |
error | {where}: each step needs exactly one action, found none |
manifest.error.step_shape |
error | {where}: expected |
manifest.error.steps |
error | {file}: {key} must be a list |
manifest.error.unknown_key |
error | {file}: unknown key "{key_name}" (expected {keys}) |
manifest.error.unknown_variable |
error | {where}: unknown variable {braced_key} (available: {vars}) |
manifest.error.yaml |
error | {file}: invalid {yaml}: |
name.error.refused |
error | Cannot use {name}: {reason} ({path}) |
name.error.refused_no_path |
error | Cannot use {name}: |
name.hint.path |
hint | Add ~/.local/bin to your PATH to type {name}; homebot works now. |
name.reason.foreign |
error | a foreign file already has that name; it was left untouched |
name.reason.in_repo |
error | the home repo already places that name in ~/.local/bin |
name.reason.no_homebot |
error | there is no stable homebot binary to link to |
name.reason.on_path |
error | a program with that name is on PATH |
name.reason.reserved |
error | homebot uses it as a command |
name.reason.shell |
error | the shell uses that word |
name.reason.synced_bin |
error | another tool manages ~/.local/bin |
name.reason.syntax |
error | use 2 to 32 lowercase letters, digits and hyphens, starting with a letter |
name.reason.temp_blocked |
error | the temporary link is occupied; your current link was kept |
name.reason.tool |
error | homebot runs a program by that name |
name.status.configured |
status | Configured name: |
name.status.effective |
status | Effective name here: |
name.status.off |
success | Name turned off; homebot works as before. |
name.status.old_stops |
warning | The old name {name} stops working; no forwarding link is left. |
name.status.set |
success | Name set to {name}. |
name.warning.in_work_tree |
warning | Your ~/.local/bin is inside a git work tree; another tool could commit this link. |
name.why.bad_record |
status | The machine's name record at {path} cannot be read. |
name.why.config_off |
status | The machine still has a name link, but homebot.yml has no name. |
name.why.link_broken |
status | The recorded link at {path} is missing or points elsewhere. |
name.why.no_record |
status | This machine has not recorded that name yet. |
name.why.not_on_path |
status | The name's link is not on this process's PATH. |
name.why.recorded_other |
status | This machine still uses {recorded}; run homebot name {configured} to finish the change. |
name.why.shadowed |
status | Another program at {path} comes before the name's link on PATH. |
next.hint.no_fix |
hint | No automatic fix for this one; the detail above says what to change. |
next.hint.run_yourself |
hint | Run this yourself: |
next.success.nothing |
success | Nothing needs you. |
pack.error.already_in_repo |
error | {repo_path} is already in the repo. Pack the new things inside it instead. |
pack.error.inside_managed |
error | {path} is inside {dir}, which is already managed. It is in the repo already. |
pack.error.is_repo |
error | {path} is (or contains) the home repo itself. |
pack.error.layer_not_linked |
error | The {layer} layer is not linked on {os}, so a file packed there could not be linked back (use {layers}). |
pack.error.link_failed |
error | Moved {path} into the repo but could not link it back. Link it with: |
pack.error.managed |
error | {path} is already managed. |
pack.error.missing |
error | {path} does not exist. |
pack.error.nested_repo |
error | {path} is a git repository of its own: the home repo would hold only a pointer to it, and its files would never reach the remote. Remove its {git_dir}, or leave it out of the home. |
pack.error.no_args |
error | What should be packed? e.g. |
pack.error.no_keys |
error | Create keys before packing secrets: |
pack.error.outside_home |
error | Only things inside your home folder can be packed (got {path}). |
pack.error.parens |
error | "{name}" is wrapped in {marks}, which the repo uses to mark folders that must not be links, so it can't be tracked. |
pack.error.plain_parent |
error | {repo_path} is a plain folder, so it can't hold a new folder that must stay real. Rename it to {open}{name}{close} in every layer first. |
pack.error.private_reserved |
error | {path} can't go into {private}: git would stage it as plaintext there. {readme}, {gitignore}, {gitkeep} and {recipients} files, {age_glob} files and {git_dir} folders are the repo's own; rename or remove it first. |
pack.error.source_not_removed |
error | Copied {path} into the repo as {repo_path}, but could not remove the original: {reason} |
pack.error.symlink |
error | {path} is a symlink. Pack the real file it points to instead. |
pack.error.top_level |
error | Only dotfiles and {exceptions} can be tracked at the top of your home folder (got {home_prefix}{name}). |
pack.error.unknown_layer |
error | Unknown layer "{layer}" (use {layers}). |
pack.error.unreadable |
error | Can't read {path}, so homebot can't check it for a git repository of its own. Nothing was moved; make it readable, or leave it out of the home. |
pack.hint.commit |
hint | Commit it with: |
pack.status.moved |
status | moved {from} → |
pack.success.packed |
success | packed {from} → |
pack.success.sealed_suffix |
success | (sealed {n}) |
persona.success.created |
success | Created persona file: |
persona.success.greeting |
success | {bot} is ready. |
persona.success.selected |
success | {id} is selected. |
remote.error.bad_name |
error | "{name}" won't do: |
remote.error.create |
error | {cli} couldn't create {name}. |
remote.error.no_address |
error | {cli} created {name} but didn't say its address. Add it with: |
remote.error.set_url |
error | Couldn't set the remote to {url}. |
remote.error.unreachable |
error | Can't reach the remote. |
remote.error.unreachable_login |
error | Can't reach the remote: git has no login for {https} here. |
remote.error.unreachable_ssh |
error | Can't reach it over ssh either. |
remote.heading.none |
status | Nothing is backed up yet: the repo has no remote. |
remote.hint.create_in_terminal |
hint | Run this in a terminal to create a private {forges} repository for it: |
remote.hint.existing |
hint | Use a repository you already have: |
remote.hint.first_save |
hint | Nothing committed yet. This commits and pushes: |
remote.hint.have_key |
hint | You have an ssh key, so the same repository can be reached over ssh: |
remote.hint.https |
hint | There's no ssh key in {ssh_dir}, so the remote uses {https}. To let git use {cli}'s login: |
remote.hint.log_in |
hint | Log git in: |
remote.hint.log_in_or_ssh |
hint | Log git in to that host, or add an ssh key to {ssh_dir} and switch to the ssh address: |
remote.hint.login |
hint | {cli} is installed but not logged in. To offer {forge} too: {cli} |
remote.hint.or_existing |
hint | Or use a repository you already have: |
remote.hint.or_ssh |
hint | Or add an ssh key to {ssh_dir} and switch to the ssh address: |
remote.hint.pick |
hint | pick one of the letters above |
remote.hint.rename |
hint | This repo has a remote named {names}, but {bot} pushes to the one named {origin}. Rename it: |
remote.hint.to_switch |
hint | To switch: |
remote.prompt.name |
prompt | Name for the new private {forge} repository [{suggested}]: |
remote.prompt.switch |
prompt | Switch to it? |
remote.prompt.url |
prompt | URL: |
remote.status.kept_https |
status | Kept {https}. To switch later: |
remote.status.not_now |
status | Not now. When you're ready: |
remote.status.up_to_date |
status | Already backed up: the remote has every commit. |
remote.success.created |
success | created {name}, a private {forge} repository |
remote.success.pushed |
success | pushed the repo is backed up |
remote.success.reachable |
success | reachable |
remote.success.remote |
success | remote |
remote.warning.waiting |
warning | The remote has new commits. Commit or discard the changes here, then: |
save.error.diverged |
error | This machine and the remote changed the same files. Resolve it by hand: |
save.error.fast_forward |
error | Could not fast-forward to the remote. |
save.error.git_add |
error | {git_add} failed. |
save.error.push |
error | Push failed. |
save.error.rebase_stuck |
error | This machine and the remote changed the same files, and git could not undo its rebase: {repo_abs} is still in the middle of one. Resolve the conflicts and run {continue_cmd}, or give up on it with: |
save.error.refused |
error | Commit refused: |
save.status.nothing |
status | Nothing new to commit. |
save.success.committed |
success | committed {n:# file|# files} |
save.success.pulled |
success | pulled {n:# commit|# commits} |
save.success.pushed |
success | pushed {n:# commit|# commits} |
save.success.sealed |
success | sealed {n:# secret|# secrets} |
save.warning.unreachable |
warning | Couldn't reach the remote, so nothing was pushed. To see why: |
save.warning.waiting |
warning | The remote has new commits. Commit or discard the changes here, then save again. |
secrets.error.cancelled |
error | Stopped at the passphrase prompt; nothing changed. |
secrets.error.decrypt |
error | Could not decrypt {private}{name}{age}. |
secrets.error.empty_recipients |
error | {recipients} has no public key in it. Restore it from the repo's history. |
secrets.error.encrypt |
error | Could not encrypt {private}{name}. |
secrets.error.encrypt_key |
error | Encrypting the new key failed; nothing was saved. |
secrets.error.identity_unreadable |
error | {identity} is not a passphrase-encrypted age key. Restore it from the repo's history. |
secrets.error.keys_exist |
error | Keys already exist ({identity}, {recipients}). Delete both first to start over. |
secrets.error.no_identity |
error | No {identity} yet. Create keys first: |
secrets.error.no_recipients |
error | No {recipients} yet. Create keys first: |
secrets.error.no_terminal |
error | There is no terminal to ask for the passphrase on. Run this in a terminal. |
secrets.error.passphrase |
error | Could not decrypt {identity} (wrong passphrase?) |
secrets.error.reserved |
error | Refusing to write over {path}: it is not a secret. |
secrets.error.save_gitignore |
error | Writing {path} failed, so nothing was set up: without it, git would take the plaintext secrets in {private}. |
secrets.error.save_key |
error | Saving the new key to {path} failed; nothing was saved. |
secrets.error.unsupported_ssh_key |
error | Line {line} of {recipients} holds a key age cannot encrypt to ({key_type}). Use an {ed25519} key, or an {rsa} key of at least {bits}; homebot never seals to fewer keys than the file lists. |
setup.error.clone_auth_gh |
error | git couldn't sign in to GitHub to clone {url}. Sign in with the GitHub CLI, which sets git up to use that login, then run {setup} again: |
setup.error.clone_auth_key |
error | git couldn't sign in to {host} to clone {url}. Add an ssh key to your account there, then run {setup} again. |
setup.error.clone_failed |
error | Couldn't clone {url}. Check the address and that you can reach it, then run {setup} again. |
setup.error.git_too_old |
error | Your git is {found}, and homebot needs {minimum} or newer: its commit guard relies on a git command that older versions lack, so every save would be refused. Update git, then run {command} again. |
setup.error.git_too_old_brew |
error | Your git is {found}, and homebot needs {minimum} or newer: its commit guard relies on a git command that older versions lack, so every save would be refused. Install a newer one with Homebrew, then run {setup} again: |
setup.error.home_taken |
error | {path} is there but holds no {config}, so it isn't a home repo and setup won't touch it. Move it aside, or set {env} to another folder, then run {setup} again: |
setup.error.install_failed |
error | {command} failed. Install {tools} yourself, then run {setup} again. |
setup.error.name_syntax |
error | {flag} {name} must be |
setup.error.no_terminal |
error | No home repo yet, and setup starts or clones one only in a terminal. Run it in one: |
setup.error.not_a_home_repo |
error | {url} has no {config}, so it isn't a home repo. It is in {path} now: move it aside, then run {setup} again. |
setup.error.persona_syntax |
error | {flag} {persona} must |
setup.error.remove_flags |
error | {remove_flag} cannot be combined with {name_flag} or |
setup.error.root_relative |
error | {env} is "{value}", which is not a full path, so homebot would find the repo only from this folder. Set it to a full path, or unset it, then run {setup} again. |
setup.error.tools_missing |
error | Missing: {tools}. The commit guard needs it to check every commit. Install it, then run {setup} again: |
setup.error.xdg_relative |
error | {name} is "{value}", which is not a full path, so the background helper can't use it. Set it to a full path, or unset it, then run {command} again. |
setup.hint.keys |
hint | Secrets aren't set up yet. To keep passwords and tokens in the repo, encrypted, create the keys: |
setup.hint.name_path |
hint | Add {bin} to your {path_env} in {file} to type {name}; homebot works now. |
setup.hint.xdg |
hint | The helper keeps the {xdg_glob} folders it was set up with. After changing {config_env}, {data_env}, {state_env} or {cache_env}, run {self}{setup} again. |
setup.hint.zshrc |
hint | Add this to your {zshrc} so new terminals tell you when something needs you: |
setup.prompt.backup |
prompt | {n:# file is|# files are} in the way of links. Move them aside (nothing is deleted) and link? |
setup.prompt.change_name |
prompt | This repo calls it {repo} on every machine. Change the shared name to {flag}? |
setup.prompt.install_tools |
prompt | Missing: {tools}. Install now with {command}? |
setup.prompt.keys |
prompt | Keep passwords and tokens in the repo, encrypted? This creates your keys. |
setup.prompt.name |
prompt | Give your homebot a command name? For example {example} ({enter} keeps homebot): |
setup.prompt.name_path |
prompt | Put {bin} on your {path_env} in {file} so the name works? |
setup.prompt.save |
prompt | Commit and push now? |
setup.prompt.save_local |
prompt | Commit now? |
setup.prompt.triage |
prompt | {n:# thing|# things} in your home could be tracked. Sort them now, one key each? |
setup.prompt.unpack |
prompt | Run {n:this step|these # steps}? |
setup.prompt.url |
prompt | URL of your home repo, or {example} on GitHub: |
setup.prompt.zshrc |
prompt | Add it to {file} now? |
setup.status.changed_here |
status | Changed on this machine: |
setup.status.cloning |
status | Cloning {url} into {path}… |
setup.status.guard_present |
status | Commit guard already installed. |
setup.status.name_blocked |
status | no name link for {name} was made here |
setup.status.name_link |
status | name link: {bin}{name} points to |
setup.status.name_uncommitted |
status | The new name is in {config} but stays uncommitted with your other work; your next save carries it. |
setup.status.no_repo |
status | No home repo yet. |
setup.status.open_shell |
status | From now on, type {name}. Open a new terminal first; on some Linux systems, log out and back in. |
setup.status.path_added |
status | Added {bin} to {file}. |
setup.status.path_line |
status | added the {path_env} startup line |
setup.status.persona |
status | Persona in this repo: |
setup.status.removed |
status | Background helper removed. |
setup.status.repo_name |
status | This repo calls your homebot {name}. |
setup.status.repo_name_wins |
status | This repo calls it {repo}; {name_flag} {flag} was ignored without a terminal. |
setup.status.zsh_only |
status | Your shell is {shell}. The startup line and the prompt count are {zsh}-only for now. |
setup.status.zshrc_in_repo |
status | That file links into your home repo, so {path} changed there. |
setup.status.zshrc_linked |
status | {file} is a link to {target}, and setup writes only through a link to a file in your home repo. Add the line there yourself. |
setup.success.created |
success | created {path}, a new home repo with a {profile} profile |
setup.success.guard |
success | installed commit guard ({hook_key}) |
setup.success.helper |
success | installed |
setup.success.zshrc |
success | added the startup line to |
setup.warning.clone_failed |
warning | Couldn't clone {url}. |
setup.warning.name_blocked |
warning | {name} cannot be used as a command here: {reason}. Homebot will keep working. |
setup.warning.name_invalid |
warning | {name} must be {rule}. Try another name. |
setup.warning.no_profile |
warning | No profile for this machine ({host}) yet, so nothing was unpacked. Name one in {config} under {hosts} or {default}, then: |
setup.warning.no_repo |
warning | {root} is not a git repo yet; skipped the commit guard. |
setup.warning.path_link |
warning | Cannot safely edit {file} (link target: {target}). Add {bin} to your {path_env} yourself. |
tidy.did.committed |
status | committed {n:# file|# files} |
tidy.did.exported |
status | refreshed |
tidy.did.guard |
status | installed the commit guard |
tidy.did.linked |
status | fixed {n:# link|# links} |
tidy.did.name_off |
status | removed this machine's name link |
tidy.did.named |
status | kept this machine's name link in sync |
tidy.did.pulled |
status | pulled {n:# commit|# commits} |
tidy.did.pushed |
status | pushed {n:# commit|# commits} |
tidy.did.sealed |
status | sealed {n:# secret|# secrets} |
tidy.error.autosave_no_guard |
error | skipped, because the commit guard could not be installed this run (see the guard step) |
tidy.error.lock |
error | Could not lock {path}: {reason}. tidy takes a file lock so two runs never overlap, and the filesystem holding the state directory does not support it. Point {env} at a local disk. |
tidy.status.inbox_count |
status | {n:# thing|# things} in the inbox. |
tidy.status.locked |
status | Another tidy is running. |
tidy.status.nothing |
status | Nothing to tidy. |
triage.error.git_add |
error | {git_add} {key} failed. |
triage.error.ignore_list |
error | {file}: {ignore} must be a list of paths |
triage.error.line_break |
error | {key} has a line break in its name, which a {gitignore} line can't hold. Nothing was written; rename it, or snooze it. |
triage.error.old_file |
error | homebot reads {decisions} now: rename {path} to |
triage.hint.header_suffix |
hint | Each answer is saved right away; {quit} stops. |
triage.hint.pick |
hint | pick one of the letters above |
triage.status.header |
status | {n} things to decide. |
triage.success.gitignored |
success | added /{key} to |
triage.success.ignored |
success | will never ask about {key} again |
triage.success.none |
success | Nothing to sort. Everything has a decision. |
triage.success.packed |
success | packed {key} into |
triage.success.snoozed |
success | snoozed {key} for |
triage.success.staged |
success | staged |
unlink.hint.helper |
hint | The background helper links them again within the hour, as {self}{link} would. To keep a file unlinked, take it out of the repo. |
unlink.status.removed |
status | Removed {n:# link|# links}. To put them back: |
unlock.error.conflict |
error | conflict {private}{name}: changed here and in the repo |
unlock.hint.ours |
hint | Keep this machine's versions: |
unlock.hint.theirs |
hint | Or take the repo's: |
unlock.prompt.passphrase |
prompt | Enter passphrase: |
unlock.status.linked |
status | linked {n:# secret|# secrets} |
unlock.success.unlocked |
success | unlocked {n:# secret|# secrets} |
unlock.success.verified_suffix |
success | , verified |
unpack.error.from_nan |
error | {from_flag} takes a step number |
unpack.error.from_range |
error | {from_flag} must be between {first_step} and |
unpack.error.no_action |
error | Step {i}: no action named "{action}". See: |
unpack.error.no_profile |
error | Which profile? Pass one ({example}) or set {default_key} in {config_file}. |
unpack.error.step_exit |
error | Step {i} ({step}) exited with {code}. |
unpack.error.step_failed |
error | Step {i} failed: |
unpack.hint.resume |
hint | Fix it, then resume: |
unpack.status.header |
status | Unpacking {profile} on {host} ({os}): {n} steps |
unpack.status.header_dry_run |
status | Would unpack {profile} on {host} ({os}): {n} steps |
unpack.success.done |
success | Unpacked {profile}. |