///|
/// The shrc store: the manifest of managed entries, and the code that writes
/// them into every detected startup file.
///|
/// Read the manifest. A missing or unreadable manifest is an empty store, not
/// an error: the first `shrc set` creates it.
pub fn load_entries() -> Array[Entry] {
let path = manifest_path()
if !@fs.path_exists(path) {
return []
}
let text = @fs.read_file_to_string(path) catch { _ => return [] }
parse_entries(text)
}
///|
/// Persist the manifest, creating `SHRC_HOME` when needed.
pub fn save_entries(entries : Array[Entry]) -> Result[Unit, String] {
match ensure_dir(shrc_home()) {
Ok(_) => ()
Err(msg) => return Err(msg)
}
@fs.write_string_to_file(manifest_path(), serialize_entries(entries)) catch {
_ => return Err("cannot write '\{manifest_path()}'")
}
Ok(())
}
///|
/// Write `entries` into every startup file in `shells`.
///
/// Returns one human-readable message per file that could not be written; an
/// empty array means everything succeeded. Imported entries (adopted from the
/// startup files) are left in place and are not rendered into the managed
/// block, so only entries added by `shrc set` live there.
pub fn apply_to_shells(
entries : Array[Entry],
shells : Array[Shell],
) -> Array[String] {
let managed = []
for entry in entries {
if !entry.imported {
managed.push(entry)
}
}
let errors = []
for shell in shells {
let exists = @fs.path_exists(shell.rc)
let existing = if exists {
@fs.read_file_to_string(shell.rc) catch {
_ => ""
}
} else {
""
}
let updated = sync_block(existing, shell.family, managed)
// Removing the last entry leaves an empty block, and an empty block in a
// file that never existed should not create one.
if exists || updated.length() > 0 {
match ensure_dir(path_dirname(shell.rc)) {
Ok(_) => ()
Err(msg) => {
errors.push(msg)
continue
}
}
@fs.write_string_to_file(shell.rc, updated) catch {
_ => errors.push("cannot write '\{shell.rc}'")
}
}
}
errors
}
///|
/// One line per entry, in the order they are stored. A search-path entry is
/// listed as one row per directory, indexed in search order, instead of one
/// long line — so `shrc ls PATH` shows what the shell will actually search,
/// in order.
pub fn format_ls(entries : Array[Entry]) -> String {
let out = StringBuilder()
for entry in entries {
out.write_string(kind_label(entry.kind))
out.write_string("\t")
if is_search_path_entry(entry) {
out.write_string(entry.name)
out.write_char('\n')
let parts = split_path(entry.value)
if parts.length() == 0 {
out.write_string("\t(empty)\n")
}
let mut i = 0
for part in parts {
out.write_string("\t\{i}\t\{part}\n")
i = i + 1
}
} else {
out.write_string(entry.name)
out.write_string("=")
out.write_string(entry.value)
out.write_char('\n')
}
}
out.to_string()
}
///|
/// The complete picture: the manifest, then every shell shrc would manage and
/// the block it would receive.
pub fn format_all(entries : Array[Entry], shells : Array[Shell]) -> String {
let out = StringBuilder()
out.write_string("# shrc — managed shell configuration\n")
out.write_string("# home: \{shrc_home()}\n")
out.write_string("\n# entries (\{entries.length()})\n")
if entries.length() == 0 {
out.write_string("# (none)\n")
} else {
for entry in entries {
if is_search_path_entry(entry) {
out.write_string("# \{kind_label(entry.kind)} \{entry.name}\n")
let mut i = 0
for part in split_path(entry.value) {
out.write_string("# \{i} \{part}\n")
i = i + 1
}
} else {
out.write_string(
"# \{kind_label(entry.kind)} \{entry.name}=\{entry.value}\n",
)
}
}
}
out.write_string("\n# startup files\n")
for shell in shells {
out.write_string("\n# \{shell.name} \{shell.rc}\n")
if entries.length() == 0 {
out.write_string("# (nothing to write)\n")
} else {
for entry in entries {
out.write_string(render_entry(shell.family, entry))
out.write_char('\n')
}
}
}
out.to_string()
}