Repository: nosarthur/gita

Issue:
Customising edit status symbols 
It would be great to be able to configure the edit status symbols `*_-` to the characters and colors that the user prefers. 

Discussion:

Patch:
diff --git a/README.md b/README.md
index 1ba5d33..a1f7df3 100644
--- a/README.md
+++ b/README.md
@@ -337,16 +337,17 @@ Here `branch` includes both branch name and status.
 
 Another choice is to enable `spaceship_status`, which mimics
 the symbols used in [spaceship-prompt](https://spaceship-prompt.sh/sections/git/#Git-status-git_status).
-For example,
 
-symbol | meaning
----|---
- ∅| local has no remote
-(no string)  | local is the same as remote
-⇕ | local has diverged from remote
- ⇡| local is ahead of remote (good for push)
- ⇣| local is behind remote (good for merge)
+To customize these symbols, add a file in `$XDG_CONFIG_HOME/gita/symbols.csv`.
+The default `spaceship_status` settings corresponds to
+
+```csv
+dirty,staged,untracked,local_ahead,remote_ahead,diverged,in_sync,no_remote
+!,+,?,↑,↓,⇕,,∅
+```
+Only the symbols to be overridden need to be defined.
 
+You can search unicode symbols [here](https://www.compart.com/en/unicode/).
 
 ### customize git command flags
 
diff --git a/gita/info.py b/gita/info.py
index 85041ca..f781cd0 100644
--- a/gita/info.py
+++ b/gita/info.py
@@ -1,8 +1,8 @@
-import os
 import csv
 import subprocess
 from enum import Enum
 from pathlib import Path
+from collections import namedtuple
 from functools import lru_cache, partial
 from typing import Tuple, List, Callable, Dict
 
@@ -41,11 +41,11 @@ class Color(Enum):
 
 
 default_colors = {
-    "no-remote": Color.white.name,
-    "in-sync": Color.green.name,
+    "no_remote": Color.white.name,
+    "in_sync": Color.green.name,
     "diverged": Color.red.name,
-    "local-ahead": Color.purple.name,
-    "remote-ahead": Color.yellow.name,
+    "local_ahead": Color.purple.name,
+    "remote_ahead": Color.yellow.name,
 }
 
 
@@ -196,11 +196,39 @@ def get_commit_time(prop: Dict[str, str]) -> str:
     return f"({result.stdout.strip()})"
 
 
+# better solution exists for python3.7+
+# https://stackoverflow.com/questions/11351032/named-tuple-and-default-values-for-optional-keyword-arguments
+Symbols = namedtuple(
+    "Symbols",
+    "dirty staged untracked local_ahead remote_ahead diverged in_sync no_remote",
+)
+Symbols.__new__.__defaults__ = ("",) * len(Symbols._fields)
+default_symbols = Symbols("*", "+", "?")
+
+
+@lru_cache()
+def get_symbols(baseline: Symbols = default_symbols) -> Dict[str, str]:
+    """
+    return status symbols with customization
+    """
+    symbols = baseline._asdict()
+    symbols[""] = ""
+    custom = {}
+    csv_config = Path(common.get_config_fname("symbols.csv"))
+    if csv_config.is_file():
+        with open(csv_config, "r") as f:
+            reader = csv.DictReader(f)
+            custom = next(reader)
+    symbols.update(custom)
+    return symbols
+
+
 def get_repo_status(prop: Dict[str, str], no_colors=False) -> str:
     head = get_head(prop["path"])
     colors = {situ: Color[name].value for situ, name in get_color_encoding().items()}
     dirty, staged, untracked, situ = _get_repo_status(prop, skip_situ=no_colors)
-    info = f"{head:<10} [{dirty+staged+untracked}]"
+    symbols = get_symbols(default_symbols)
+    info = f"{head:<10} [{symbols[dirty]+symbols[staged]+symbols[untracked]}]"
 
     if no_colors:
         return f"{info:<17}"
@@ -208,19 +236,14 @@ def get_repo_status(prop: Dict[str, str], no_colors=False) -> str:
     return f"{color}{info:<17}{Color.end}"
 
 
-spaceship = {
-    "local-ahead": "⇡",
-    "remote-ahead": "⇣",
-    "diverged": "⇕",
-    "in-sync": "",
-    "no-remote": "∅",
-}
+spaceship = Symbols("!", "+", "?", "↑", "↓", "⇕", "", "∅")
 
 
 def get_spaceship_status(prop: Dict[str, str]) -> str:
     head = get_head(prop["path"])
     dirty, staged, untracked, situ = _get_repo_status(prop)
-    info = f"{head:<10} [{dirty+staged+untracked+spaceship[situ]}]"
+    symbols = get_symbols(spaceship)
+    info = f"{head:<10} [{symbols[dirty]+symbols[staged]+symbols[untracked]+symbols[situ]}]"
     return f"{info:<18}"
 
 
@@ -236,26 +259,26 @@ def _get_repo_status(
     """
     path = prop["path"]
     flags = prop["flags"]
-    dirty = "*" if run_quiet_diff(flags, [], path) else ""
-    staged = "+" if run_quiet_diff(flags, ["--cached"], path) else ""
-    untracked = "?" if has_untracked(flags, path) else ""
+    dirty = "dirty" if run_quiet_diff(flags, [], path) else ""
+    staged = "staged" if run_quiet_diff(flags, ["--cached"], path) else ""
+    untracked = "untracked" if has_untracked(flags, path) else ""
 
     if skip_situ:
         return dirty, staged, untracked, ""
 
     diff_returncode = run_quiet_diff(flags, ["@{u}", "@{0}"], path)
     if diff_returncode == 128:
-        situ = "no-remote"
+        situ = "no_remote"
     elif diff_returncode == 0:
-        situ = "in-sync"
+        situ = "in_sync"
     else:
         common_commit = get_common_commit(path)
         outdated = run_quiet_diff(flags, ["@{u}", common_commit], path)
         if outdated:
             diverged = run_quiet_diff(flags, ["@{0}", common_commit], path)
-            situ = "diverged" if diverged else "remote-ahead"
+            situ = "diverged" if diverged else "remote_ahead"
         else:  # local is ahead of remote
-            situ = "local-ahead"
+            situ = "local_ahead"
     return dirty, staged, untracked, situ
 
 


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 7080428..7c82501 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -130,18 +130,21 @@ class TestLsLl:
         [
             (
                 PATH_FNAME,
-                "repo1 \x1b[31mmaster     [dsu] \x1b[0m msg \nrepo2 \x1b[31mmaster     [dsu] \x1b[0m msg \nxxx   \x1b[31mmaster     [dsu] \x1b[0m msg \n",
+                "repo1 \x1b[31mmaster     [*+?] \x1b[0m msg \nrepo2 \x1b[31mmaster     [*+?] \x1b[0m msg \nxxx   \x1b[31mmaster     [*+?] \x1b[0m msg \n",
             ),
             (PATH_FNAME_EMPTY, ""),
             (
                 PATH_FNAME_CLASH,
-                "repo1 \x1b[31mmaster     [dsu] \x1b[0m msg \nrepo2 \x1b[31mmaster     [dsu] \x1b[0m msg \n",
+                "repo1 \x1b[31mmaster     [*+?] \x1b[0m msg \nrepo2 \x1b[31mmaster     [*+?] \x1b[0m msg \n",
             ),
         ],
     )
     @patch("gita.utils.is_git", return_value=True)
     @patch("gita.info.get_head", return_value="master")
-    @patch("gita.info._get_repo_status", return_value=("d", "s", "u", "diverged"))
+    @patch(
+        "gita.info._get_repo_status",
+        return_value=("dirty", "staged", "untracked", "diverged"),
+    )
     @patch("gita.info.get_commit_msg", return_value="msg")
     @patch("gita.info.get_commit_time", return_value="")
     @patch("gita.common.get_config_fname")
@@ -567,7 +570,7 @@ def test_set_color(mock_get_fname, tmpdir):
     args = argparse.Namespace()
     args.color_cmd = "set"
     args.color = "b_white"
-    args.situation = "no-remote"
+    args.situation = "no_remote"
     with tmpdir.as_cwd():
         csv_config = Path.cwd() / "colors.csv"
         mock_get_fname.return_value = csv_config
@@ -577,11 +580,11 @@ def test_set_color(mock_get_fname, tmpdir):
         items = info.get_color_encoding()
     info.get_color_encoding.cache_clear()  # avoid side effect
     assert items == {
-        "no-remote": "b_white",
-        "in-sync": "green",
+        "no_remote": "b_white",
+        "in_sync": "green",
         "diverged": "red",
-        "local-ahead": "purple",
-        "remote-ahead": "yellow",
+        "local_ahead": "purple",
+        "remote_ahead": "yellow",
     }