refactor: rebuild as an emblem plugin on nemo-repo-core
Everything that was not about drawing emblems moves out: the columns, the properties tab and the context menu are their own plugins now, and the config, git and cache layers come from nemo-repo-core. The indicator list stops being hardcoded. Entries that are not one of the three git derived indicators are matched by marker_directory or marker_file, which is how claude_dir now works and how a user adds their own without touching the code.
This commit is contained in:
1 parent
d81b502300
commit
1b916d4d00
7 files changed
+256
-671
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 DualFroz
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"cache_ttl_seconds": 5,
|
|
||||||
"indicators": {
|
|
||||||
"git_repo": {
|
|
||||||
"enabled": true,
|
|
||||||
"emblem": "emblem-repo-git",
|
|
||||||
"description": "Folder contains a .git directory (or worktree file)"
|
|
||||||
},
|
|
||||||
"claude_dir": {
|
|
||||||
"enabled": true,
|
|
||||||
"emblem": "emblem-repo-claude",
|
|
||||||
"description": "Folder contains a .claude directory"
|
|
||||||
},
|
|
||||||
"uncommitted_changes": {
|
|
||||||
"enabled": true,
|
|
||||||
"emblem": "emblem-repo-dirty",
|
|
||||||
"description": "git status shows staged/unstaged/untracked changes"
|
|
||||||
},
|
|
||||||
"unpushed_commits": {
|
|
||||||
"enabled": true,
|
|
||||||
"emblem": "emblem-repo-unpushed",
|
|
||||||
"description": "Current branch has commits ahead of its upstream (or no upstream at all)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"open_terminal": true,
|
|
||||||
"open_editor": true,
|
|
||||||
"git_status": true,
|
|
||||||
"git_log": true,
|
|
||||||
"git_pull": true
|
|
||||||
},
|
|
||||||
"editor_command": "code",
|
|
||||||
"terminal_command": null
|
|
||||||
}
|
|
||||||
+70
-30
@@ -1,40 +1,80 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Installs the nemo-repo-emblems extension for the current user:
|
# Installs the nemo-repo-emblems plugin for the current user. No root needed.
|
||||||
# - symlinks the extension .py into ~/.local/share/nemo-python/extensions/
|
#
|
||||||
# - symlinks the bundled emblem SVGs into ~/.local/share/icons/hicolor/scalable/emblems/
|
# ./install.sh link the plugin (edits in this checkout go live)
|
||||||
# - creates ~/.config/nemo-repo-emblems.json from the bundled default if missing
|
# ./install.sh --copy copy the plugin instead of linking it
|
||||||
# - refreshes the icon cache and restarts nemo
|
#
|
||||||
|
# Requires nemo-repo-core: https://github.com/dualfroz/nemo-repo-core
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
sourceDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
dataHome="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||||
|
extensionsDir="$dataHome/nemo-python/extensions"
|
||||||
|
coreDir="$dataHome/nemo-repo-core"
|
||||||
|
pluginFile="nemoRepoEmblems.py"
|
||||||
|
|
||||||
EXT_DIR="$HOME/.local/share/nemo-python/extensions"
|
mode="link"
|
||||||
ICON_DIR="$HOME/.local/share/icons/hicolor/scalable/emblems"
|
if [ "${1:-}" = "--copy" ]; then
|
||||||
CONFIG_PATH="$HOME/.config/nemo-repo-emblems.json"
|
mode="copy"
|
||||||
|
elif [ -n "${1:-}" ]; then
|
||||||
mkdir -p "$EXT_DIR" "$ICON_DIR" "$(dirname "$CONFIG_PATH")"
|
echo "usage: $0 [--copy]" >&2
|
||||||
|
exit 2
|
||||||
ln -sf "$SRC_DIR/nemo_repo_emblems.py" "$EXT_DIR/nemo_repo_emblems.py"
|
|
||||||
echo "Linked extension -> $EXT_DIR/nemo_repo_emblems.py"
|
|
||||||
|
|
||||||
for svg in "$SRC_DIR"/emblems/*.svg; do
|
|
||||||
ln -sf "$svg" "$ICON_DIR/$(basename "$svg")"
|
|
||||||
done
|
|
||||||
echo "Linked emblem icons -> $ICON_DIR"
|
|
||||||
|
|
||||||
if [ ! -f "$CONFIG_PATH" ]; then
|
|
||||||
cp "$SRC_DIR/config.default.json" "$CONFIG_PATH"
|
|
||||||
echo "Created default config -> $CONFIG_PATH"
|
|
||||||
else
|
|
||||||
echo "Config already exists, left untouched -> $CONFIG_PATH"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$coreDir/nemoRepoCore" ] && ! python3 -c "import nemoRepoCore" >/dev/null 2>&1; then
|
||||||
|
cat >&2 <<'MESSAGE'
|
||||||
|
nemo-repo-core is not installed, and this plugin is nothing without it:
|
||||||
|
|
||||||
|
git clone https://github.com/dualfroz/nemo-repo-core
|
||||||
|
cd nemo-repo-core && ./install.sh
|
||||||
|
MESSAGE
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$extensionsDir"
|
||||||
|
target="$extensionsDir/$pluginFile"
|
||||||
|
rm -f "$target"
|
||||||
|
if [ "$mode" = "copy" ]; then
|
||||||
|
cp "$sourceDir/$pluginFile" "$target"
|
||||||
|
echo "Copied plugin -> $target"
|
||||||
|
else
|
||||||
|
ln -s "$sourceDir/$pluginFile" "$target"
|
||||||
|
echo "Linked plugin -> $target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
iconDir="$dataHome/icons/hicolor/scalable/emblems"
|
||||||
|
mkdir -p "$iconDir"
|
||||||
|
for svg in "$sourceDir"/emblems/*.svg; do
|
||||||
|
name="$(basename "$svg")"
|
||||||
|
rm -f "$iconDir/$name"
|
||||||
|
if [ "$mode" = "copy" ]; then
|
||||||
|
cp "$svg" "$iconDir/$name"
|
||||||
|
else
|
||||||
|
ln -s "$svg" "$iconDir/$name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "Installed emblem icons -> $iconDir"
|
||||||
|
|
||||||
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
||||||
gtk-update-icon-cache -f -t "$HOME/.local/share/icons/hicolor" 2>/dev/null || true
|
gtk-update-icon-cache -f -t "$dataHome/icons/hicolor" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Restarting nemo..."
|
if ! PYTHONPATH="$coreDir:$extensionsDir" python3 -c "import nemoRepoEmblems" >/dev/null 2>&1; then
|
||||||
nemo -q || true
|
echo >&2
|
||||||
(nemo --no-desktop >/dev/null 2>&1 &) || true
|
echo "The plugin did not import cleanly:" >&2
|
||||||
|
PYTHONPATH="$coreDir:$extensionsDir" python3 -c "import nemoRepoEmblems" >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Import check passed."
|
||||||
|
|
||||||
echo "Done. Edit $CONFIG_PATH to toggle indicators/actions (live-reloaded, no restart needed)."
|
legacyExtension="$extensionsDir/nemo_repo_emblems.py"
|
||||||
|
if [ -e "$legacyExtension" ]; then
|
||||||
|
echo
|
||||||
|
echo "WARNING: the pre-split single file extension is still installed:"
|
||||||
|
echo " $legacyExtension"
|
||||||
|
echo "Running both duplicates emblems, columns and menu entries. Remove it with:"
|
||||||
|
echo " rm \"$legacyExtension\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Restart Nemo to load the plugin: nemo -q"
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
"""nemo-repo-emblems: emblem overlays for repository folders in Nemo.
|
||||||
|
|
||||||
|
Four indicators ship by default: the folder is a git repository, it holds a
|
||||||
|
.claude directory, its working tree is dirty, it has commits the remote does
|
||||||
|
not. Indicators are declared in the config file, so extra ones can be added
|
||||||
|
without touching this file.
|
||||||
|
|
||||||
|
Requires nemo-repo-core: https://github.com/dualfroz/nemo-repo-core
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _ensureCoreOnPath():
|
||||||
|
if importlib.util.find_spec("nemoRepoCore") is not None:
|
||||||
|
return
|
||||||
|
dataHome = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
|
||||||
|
dataDirs = (os.environ.get("XDG_DATA_DIRS") or "/usr/local/share:/usr/share").split(":")
|
||||||
|
for base in [dataHome] + dataDirs:
|
||||||
|
candidate = os.path.join(base, "nemo-repo-core")
|
||||||
|
if os.path.isdir(os.path.join(candidate, "nemoRepoCore")):
|
||||||
|
sys.path.insert(0, candidate)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
_ensureCoreOnPath()
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Nemo", "3.0")
|
||||||
|
from gi.repository import GObject, Nemo
|
||||||
|
|
||||||
|
from nemoRepoCore import (coreConfig, findRepoRoot, hasMarkerDirectory,
|
||||||
|
localDirectoryPath, pluginLogger, repoCache,
|
||||||
|
requireCore)
|
||||||
|
|
||||||
|
requireCore("1.0.0")
|
||||||
|
|
||||||
|
logger = pluginLogger("emblems")
|
||||||
|
|
||||||
|
gitIndicators = ("git_repo", "uncommitted_changes", "unpushed_commits")
|
||||||
|
|
||||||
|
pluginDefaults = {
|
||||||
|
"indicators": {
|
||||||
|
"git_repo": {
|
||||||
|
"enabled": True,
|
||||||
|
"emblem": "emblem-repo-git",
|
||||||
|
"description": "Folder contains a .git directory (or worktree file)",
|
||||||
|
},
|
||||||
|
"claude_dir": {
|
||||||
|
"enabled": True,
|
||||||
|
"emblem": "emblem-repo-claude",
|
||||||
|
"marker_directory": ".claude",
|
||||||
|
"description": "Folder contains a .claude directory",
|
||||||
|
},
|
||||||
|
"uncommitted_changes": {
|
||||||
|
"enabled": True,
|
||||||
|
"emblem": "emblem-repo-dirty",
|
||||||
|
"description": "git status shows staged, unstaged or untracked changes",
|
||||||
|
},
|
||||||
|
"unpushed_commits": {
|
||||||
|
"enabled": True,
|
||||||
|
"emblem": "emblem-repo-unpushed",
|
||||||
|
"description": "Branch is ahead of its upstream, or has no upstream",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
coreConfig.registerDefaults(pluginDefaults)
|
||||||
|
|
||||||
|
|
||||||
|
def _enabledIndicators():
|
||||||
|
"""Config entries that are switched on and name an emblem, by key."""
|
||||||
|
active = {}
|
||||||
|
for key, entry in coreConfig.section("indicators").items():
|
||||||
|
if not isinstance(entry, dict) or not entry.get("enabled"):
|
||||||
|
continue
|
||||||
|
if entry.get("emblem"):
|
||||||
|
active[key] = entry
|
||||||
|
return active
|
||||||
|
|
||||||
|
|
||||||
|
def _markerMatches(path, entry):
|
||||||
|
markerDirectory = entry.get("marker_directory")
|
||||||
|
if markerDirectory and hasMarkerDirectory(path, markerDirectory):
|
||||||
|
return True
|
||||||
|
markerFile = entry.get("marker_file")
|
||||||
|
return bool(markerFile) and os.path.isfile(os.path.join(path, markerFile))
|
||||||
|
|
||||||
|
|
||||||
|
class RepoEmblemProvider(GObject.GObject, Nemo.InfoProvider,
|
||||||
|
Nemo.NameAndDescProvider):
|
||||||
|
|
||||||
|
def update_file_info(self, file):
|
||||||
|
path = localDirectoryPath(file)
|
||||||
|
if path is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
active = _enabledIndicators()
|
||||||
|
for key, entry in active.items():
|
||||||
|
if key not in gitIndicators and _markerMatches(path, entry):
|
||||||
|
file.add_emblem(entry["emblem"])
|
||||||
|
|
||||||
|
repoRoot = findRepoRoot(path)
|
||||||
|
if repoRoot is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if "git_repo" in active:
|
||||||
|
file.add_emblem(active["git_repo"]["emblem"])
|
||||||
|
|
||||||
|
wantsDirty = "uncommitted_changes" in active
|
||||||
|
wantsUnpushed = "unpushed_commits" in active
|
||||||
|
if not (wantsDirty or wantsUnpushed):
|
||||||
|
return
|
||||||
|
|
||||||
|
repoCache.registerFileObject(repoRoot, file)
|
||||||
|
info = repoCache.get(repoRoot)
|
||||||
|
if info.error:
|
||||||
|
return
|
||||||
|
|
||||||
|
if wantsDirty and info.dirty:
|
||||||
|
file.add_emblem(active["uncommitted_changes"]["emblem"])
|
||||||
|
if wantsUnpushed and info.unpushed:
|
||||||
|
file.add_emblem(active["unpushed_commits"]["emblem"])
|
||||||
|
|
||||||
|
def get_name_and_desc(self):
|
||||||
|
return ["nemo-repo-emblems:::Emblem overlays for git repositories "
|
||||||
|
"and marker directories"]
|
||||||
@@ -1,607 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
nemo-repo-emblems
|
|
||||||
==================
|
|
||||||
|
|
||||||
A single nemo-python extension that gives Nemo (Cinnamon's file manager)
|
|
||||||
real awareness of git repositories:
|
|
||||||
|
|
||||||
* Nemo.InfoProvider -> configurable emblem overlays (git repo / .claude
|
|
||||||
dir / uncommitted changes / unpushed commits)
|
|
||||||
* Nemo.ColumnProvider -> "Git Branch" / "Git Status" columns for list view
|
|
||||||
* Nemo.PropertyPageProvider -> a "Git" tab in the folder's Properties
|
|
||||||
dialog with branch, ahead/behind, dirty counts,
|
|
||||||
untracked count and last commit
|
|
||||||
* Nemo.MenuProvider -> right-click actions: open terminal here, open in
|
|
||||||
editor, git status / git log / git pull in a
|
|
||||||
terminal
|
|
||||||
|
|
||||||
Config file: ~/.config/nemo-repo-emblems.json (created from
|
|
||||||
config.default.json next to this file on first run if missing). Edits are
|
|
||||||
picked up live via a file monitor -- no Nemo restart needed.
|
|
||||||
|
|
||||||
Performance: every git-derived fact for a repo root is cached with a TTL
|
|
||||||
(default 5s, see "cache_ttl_seconds") AND eagerly invalidated by watching
|
|
||||||
.git/HEAD, .git/index and .git/refs/heads for changes, so emblems update
|
|
||||||
promptly after a commit/checkout/pull without shelling out to git on every
|
|
||||||
Nemo redraw.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import weakref
|
|
||||||
|
|
||||||
import gi
|
|
||||||
gi.require_version("Nemo", "3.0")
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import GObject, Gio, GLib, Gtk, Nemo
|
|
||||||
|
|
||||||
logger = logging.getLogger("nemo-repo-emblems")
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
|
||||||
|
|
||||||
CONFIG_PATH = os.path.expanduser("~/.config/nemo-repo-emblems.json")
|
|
||||||
DEFAULT_CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.default.json")
|
|
||||||
|
|
||||||
GIT_BIN = shutil.which("git")
|
|
||||||
CODE_BIN = shutil.which("code")
|
|
||||||
TERMINAL_CANDIDATES = ["gnome-terminal", "x-terminal-emulator", "tilix", "konsole",
|
|
||||||
"xfce4-terminal", "mate-terminal", "xterm"]
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# Config
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
|
||||||
"cache_ttl_seconds": 5,
|
|
||||||
"indicators": {
|
|
||||||
"git_repo": {"enabled": True, "emblem": "emblem-repo-git"},
|
|
||||||
"claude_dir": {"enabled": True, "emblem": "emblem-repo-claude"},
|
|
||||||
"uncommitted_changes": {"enabled": True, "emblem": "emblem-repo-dirty"},
|
|
||||||
"unpushed_commits": {"enabled": True, "emblem": "emblem-repo-unpushed"},
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"open_terminal": True,
|
|
||||||
"open_editor": True,
|
|
||||||
"git_status": True,
|
|
||||||
"git_log": True,
|
|
||||||
"git_pull": True,
|
|
||||||
},
|
|
||||||
"editor_command": "code",
|
|
||||||
"terminal_command": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""Loads ~/.config/nemo-repo-emblems.json and reloads it live on change."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.data = dict(DEFAULT_CONFIG)
|
|
||||||
self._ensure_file_exists()
|
|
||||||
self._load()
|
|
||||||
self._monitor = None
|
|
||||||
self._watch()
|
|
||||||
|
|
||||||
def _ensure_file_exists(self):
|
|
||||||
if os.path.exists(CONFIG_PATH):
|
|
||||||
return
|
|
||||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
|
||||||
try:
|
|
||||||
if os.path.exists(DEFAULT_CONFIG_PATH):
|
|
||||||
shutil.copyfile(DEFAULT_CONFIG_PATH, CONFIG_PATH)
|
|
||||||
else:
|
|
||||||
with open(CONFIG_PATH, "w") as f:
|
|
||||||
json.dump(DEFAULT_CONFIG, f, indent=2)
|
|
||||||
except OSError as e:
|
|
||||||
logger.warning("Could not create default config at %s: %s", CONFIG_PATH, e)
|
|
||||||
|
|
||||||
def _load(self):
|
|
||||||
try:
|
|
||||||
with open(CONFIG_PATH) as f:
|
|
||||||
loaded = json.load(f)
|
|
||||||
merged = dict(DEFAULT_CONFIG)
|
|
||||||
merged["indicators"] = {**DEFAULT_CONFIG["indicators"], **loaded.get("indicators", {})}
|
|
||||||
merged["actions"] = {**DEFAULT_CONFIG["actions"], **loaded.get("actions", {})}
|
|
||||||
merged["cache_ttl_seconds"] = loaded.get("cache_ttl_seconds", DEFAULT_CONFIG["cache_ttl_seconds"])
|
|
||||||
merged["editor_command"] = loaded.get("editor_command", DEFAULT_CONFIG["editor_command"])
|
|
||||||
merged["terminal_command"] = loaded.get("terminal_command", DEFAULT_CONFIG["terminal_command"])
|
|
||||||
self.data = merged
|
|
||||||
logger.info("Loaded config from %s", CONFIG_PATH)
|
|
||||||
except (OSError, ValueError) as e:
|
|
||||||
logger.warning("Failed to load %s (%s), using defaults", CONFIG_PATH, e)
|
|
||||||
self.data = dict(DEFAULT_CONFIG)
|
|
||||||
|
|
||||||
def _watch(self):
|
|
||||||
try:
|
|
||||||
gfile = Gio.File.new_for_path(CONFIG_PATH)
|
|
||||||
self._monitor = gfile.monitor_file(Gio.FileMonitorFlags.NONE, None)
|
|
||||||
self._monitor.connect("changed", self._on_changed)
|
|
||||||
except GLib.Error as e:
|
|
||||||
logger.warning("Could not watch config file: %s", e)
|
|
||||||
|
|
||||||
def _on_changed(self, monitor, file, other_file, event_type):
|
|
||||||
if event_type in (Gio.FileMonitorEvent.CHANGES_DONE_HINT, Gio.FileMonitorEvent.CREATED):
|
|
||||||
self._load()
|
|
||||||
|
|
||||||
def indicator(self, name):
|
|
||||||
return self.data["indicators"].get(name, {"enabled": False, "emblem": ""})
|
|
||||||
|
|
||||||
def indicator_enabled(self, name):
|
|
||||||
return bool(self.indicator(name).get("enabled"))
|
|
||||||
|
|
||||||
def indicator_emblem(self, name):
|
|
||||||
return self.indicator(name).get("emblem", "")
|
|
||||||
|
|
||||||
def action_enabled(self, name):
|
|
||||||
return bool(self.data["actions"].get(name, False))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ttl(self):
|
|
||||||
try:
|
|
||||||
return float(self.data.get("cache_ttl_seconds", 5))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return 5.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def editor_command(self):
|
|
||||||
return self.data.get("editor_command") or "code"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def terminal_command(self):
|
|
||||||
return self.data.get("terminal_command")
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG = Config()
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# Git status gathering + cache
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class RepoInfo:
|
|
||||||
__slots__ = ("branch", "upstream", "ahead", "behind", "has_upstream",
|
|
||||||
"staged", "unstaged", "untracked", "last_commit", "error")
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.branch = None
|
|
||||||
self.upstream = None
|
|
||||||
self.ahead = 0
|
|
||||||
self.behind = 0
|
|
||||||
self.has_upstream = False
|
|
||||||
self.staged = 0
|
|
||||||
self.unstaged = 0
|
|
||||||
self.untracked = 0
|
|
||||||
self.last_commit = None # (hash, author, relative_date, subject)
|
|
||||||
self.error = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dirty(self):
|
|
||||||
return (self.staged + self.unstaged + self.untracked) > 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unpushed(self):
|
|
||||||
# No upstream at all is also surfaced as "unpushed" (nothing to push TO)
|
|
||||||
return self.ahead > 0 or not self.has_upstream
|
|
||||||
|
|
||||||
|
|
||||||
def _run_git(args, cwd):
|
|
||||||
env = dict(os.environ)
|
|
||||||
env["GIT_OPTIONAL_LOCKS"] = "0"
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[GIT_BIN] + args,
|
|
||||||
cwd=cwd,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
env=env,
|
|
||||||
timeout=3,
|
|
||||||
)
|
|
||||||
return result.stdout.decode("utf-8", "replace")
|
|
||||||
except (subprocess.TimeoutExpired, OSError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def compute_repo_info(repo_root):
|
|
||||||
info = RepoInfo()
|
|
||||||
if not GIT_BIN:
|
|
||||||
info.error = "git not found in PATH"
|
|
||||||
return info
|
|
||||||
|
|
||||||
out = _run_git(["status", "--porcelain=2", "--branch"], repo_root)
|
|
||||||
if out is None:
|
|
||||||
info.error = "git status failed"
|
|
||||||
return info
|
|
||||||
|
|
||||||
for line in out.splitlines():
|
|
||||||
if line.startswith("# branch.head "):
|
|
||||||
info.branch = line.split(" ", 2)[2]
|
|
||||||
elif line.startswith("# branch.upstream "):
|
|
||||||
info.upstream = line.split(" ", 2)[2]
|
|
||||||
info.has_upstream = True
|
|
||||||
elif line.startswith("# branch.ab "):
|
|
||||||
parts = line.split(" ")
|
|
||||||
for p in parts[2:]:
|
|
||||||
if p.startswith("+"):
|
|
||||||
info.ahead = int(p[1:])
|
|
||||||
elif p.startswith("-"):
|
|
||||||
info.behind = int(p[1:])
|
|
||||||
elif line.startswith("1 ") or line.startswith("2 "):
|
|
||||||
# ordinary / renamed-copied change: XY <sub> ...
|
|
||||||
xy = line.split(" ", 2)[1]
|
|
||||||
x, y = xy[0], xy[1]
|
|
||||||
if x != ".":
|
|
||||||
info.staged += 1
|
|
||||||
if y != ".":
|
|
||||||
info.unstaged += 1
|
|
||||||
elif line.startswith("u "):
|
|
||||||
info.staged += 1 # unmerged conflict, counts as needing attention
|
|
||||||
elif line.startswith("? "):
|
|
||||||
info.untracked += 1
|
|
||||||
|
|
||||||
log_out = _run_git(["log", "-1", "--format=%h%x09%an%x09%ar%x09%s"], repo_root)
|
|
||||||
if log_out:
|
|
||||||
parts = log_out.strip("\n").split("\t")
|
|
||||||
if len(parts) == 4:
|
|
||||||
info.last_commit = tuple(parts)
|
|
||||||
|
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
class RepoCache:
|
|
||||||
"""TTL cache + git-directory file-watch invalidation, keyed by repo root path."""
|
|
||||||
|
|
||||||
def __init__(self, ttl_fn):
|
|
||||||
self._ttl_fn = ttl_fn
|
|
||||||
self._entries = {} # repo_root -> (RepoInfo, timestamp)
|
|
||||||
self._monitors = {} # repo_root -> [Gio.FileMonitor, ...]
|
|
||||||
self._watchers = {} # repo_root -> weakref.WeakSet[Nemo.FileInfo]
|
|
||||||
|
|
||||||
def get(self, repo_root):
|
|
||||||
now = time.monotonic()
|
|
||||||
entry = self._entries.get(repo_root)
|
|
||||||
if entry is not None:
|
|
||||||
info, ts = entry
|
|
||||||
if (now - ts) < self._ttl_fn():
|
|
||||||
return info
|
|
||||||
info = compute_repo_info(repo_root)
|
|
||||||
self._entries[repo_root] = (info, now)
|
|
||||||
self._ensure_watch(repo_root)
|
|
||||||
return info
|
|
||||||
|
|
||||||
def register_file_object(self, repo_root, nemo_file):
|
|
||||||
"""Track a live Nemo.FileInfo so we can force a re-query on git changes."""
|
|
||||||
watchers = self._watchers.setdefault(repo_root, weakref.WeakSet())
|
|
||||||
try:
|
|
||||||
watchers.add(nemo_file)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _ensure_watch(self, repo_root):
|
|
||||||
if repo_root in self._monitors:
|
|
||||||
return
|
|
||||||
git_dir = os.path.join(repo_root, ".git")
|
|
||||||
# .git can be a file (worktrees / submodules) instead of a directory
|
|
||||||
if os.path.isfile(git_dir):
|
|
||||||
return
|
|
||||||
if not os.path.isdir(git_dir):
|
|
||||||
return
|
|
||||||
|
|
||||||
monitors = []
|
|
||||||
watch_paths = [
|
|
||||||
git_dir, # HEAD, index, MERGE_HEAD, ...
|
|
||||||
os.path.join(git_dir, "refs", "heads"), # local branch tip updates
|
|
||||||
]
|
|
||||||
for path in watch_paths:
|
|
||||||
if not os.path.exists(path):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
gfile = Gio.File.new_for_path(path)
|
|
||||||
monitor = gfile.monitor_directory(Gio.FileMonitorFlags.NONE, None)
|
|
||||||
monitor.connect("changed", self._on_git_dir_changed, repo_root)
|
|
||||||
monitors.append(monitor)
|
|
||||||
except GLib.Error as e:
|
|
||||||
logger.debug("Could not watch %s: %s", path, e)
|
|
||||||
if monitors:
|
|
||||||
self._monitors[repo_root] = monitors
|
|
||||||
|
|
||||||
def _on_git_dir_changed(self, monitor, gfile, other_file, event_type, repo_root):
|
|
||||||
# Invalidate immediately rather than waiting for TTL expiry.
|
|
||||||
self._entries.pop(repo_root, None)
|
|
||||||
for nemo_file in list(self._watchers.get(repo_root, ())):
|
|
||||||
try:
|
|
||||||
nemo_file.invalidate_extension_info()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
CACHE = RepoCache(lambda: CONFIG.ttl)
|
|
||||||
|
|
||||||
|
|
||||||
def find_repo_root(path):
|
|
||||||
"""A directory is a repo root, for our purposes, if it directly contains .git."""
|
|
||||||
if os.path.isdir(os.path.join(path, ".git")) or os.path.isfile(os.path.join(path, ".git")):
|
|
||||||
return path
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def has_claude_dir(path):
|
|
||||||
return os.path.isdir(os.path.join(path, ".claude"))
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# InfoProvider: emblems
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class RepoEmblemProvider(GObject.GObject, Nemo.InfoProvider, Nemo.NameAndDescProvider):
|
|
||||||
|
|
||||||
def update_file_info(self, file):
|
|
||||||
if file.get_uri_scheme() != "file":
|
|
||||||
return
|
|
||||||
if not file.is_directory():
|
|
||||||
return
|
|
||||||
|
|
||||||
path = GLib.filename_from_uri(file.get_uri())[0]
|
|
||||||
|
|
||||||
if CONFIG.indicator_enabled("claude_dir") and has_claude_dir(path):
|
|
||||||
emblem = CONFIG.indicator_emblem("claude_dir")
|
|
||||||
if emblem:
|
|
||||||
file.add_emblem(emblem)
|
|
||||||
|
|
||||||
repo_root = find_repo_root(path)
|
|
||||||
if repo_root is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if CONFIG.indicator_enabled("git_repo"):
|
|
||||||
emblem = CONFIG.indicator_emblem("git_repo")
|
|
||||||
if emblem:
|
|
||||||
file.add_emblem(emblem)
|
|
||||||
|
|
||||||
# Only shell out / consult the cache if we actually need dirty/unpushed state.
|
|
||||||
if CONFIG.indicator_enabled("uncommitted_changes") or CONFIG.indicator_enabled("unpushed_commits"):
|
|
||||||
CACHE.register_file_object(repo_root, file)
|
|
||||||
info = CACHE.get(repo_root)
|
|
||||||
|
|
||||||
if info.error is None:
|
|
||||||
if CONFIG.indicator_enabled("uncommitted_changes") and info.dirty:
|
|
||||||
emblem = CONFIG.indicator_emblem("uncommitted_changes")
|
|
||||||
if emblem:
|
|
||||||
file.add_emblem(emblem)
|
|
||||||
if CONFIG.indicator_enabled("unpushed_commits") and info.unpushed:
|
|
||||||
emblem = CONFIG.indicator_emblem("unpushed_commits")
|
|
||||||
if emblem:
|
|
||||||
file.add_emblem(emblem)
|
|
||||||
|
|
||||||
def get_name_and_desc(self):
|
|
||||||
return [("nemo-repo-emblems:::Configurable git-status emblems (.git / .claude / dirty / unpushed)")]
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# ColumnProvider: extra list-view columns
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class RepoColumnProvider(GObject.GObject, Nemo.ColumnProvider, Nemo.InfoProvider, Nemo.NameAndDescProvider):
|
|
||||||
|
|
||||||
def get_columns(self):
|
|
||||||
return [
|
|
||||||
Nemo.Column(name="NemoRepoEmblems::branch_column",
|
|
||||||
attribute="repo_branch",
|
|
||||||
label="Git Branch",
|
|
||||||
description="Current branch of the git repository"),
|
|
||||||
Nemo.Column(name="NemoRepoEmblems::status_column",
|
|
||||||
attribute="repo_status",
|
|
||||||
label="Git Status",
|
|
||||||
description="Ahead/behind + working tree status"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def update_file_info(self, file):
|
|
||||||
if file.get_uri_scheme() != "file" or not file.is_directory():
|
|
||||||
return
|
|
||||||
|
|
||||||
path = GLib.filename_from_uri(file.get_uri())[0]
|
|
||||||
repo_root = find_repo_root(path)
|
|
||||||
if repo_root is None:
|
|
||||||
file.add_string_attribute("repo_branch", "")
|
|
||||||
file.add_string_attribute("repo_status", "")
|
|
||||||
return
|
|
||||||
|
|
||||||
CACHE.register_file_object(repo_root, file)
|
|
||||||
info = CACHE.get(repo_root)
|
|
||||||
|
|
||||||
if info.error:
|
|
||||||
file.add_string_attribute("repo_branch", "?")
|
|
||||||
file.add_string_attribute("repo_status", info.error)
|
|
||||||
return
|
|
||||||
|
|
||||||
file.add_string_attribute("repo_branch", info.branch or "(detached)")
|
|
||||||
|
|
||||||
bits = []
|
|
||||||
if info.ahead:
|
|
||||||
bits.append(f"↑{info.ahead}")
|
|
||||||
if info.behind:
|
|
||||||
bits.append(f"↓{info.behind}")
|
|
||||||
if not info.has_upstream:
|
|
||||||
bits.append("no upstream")
|
|
||||||
dirty_bits = []
|
|
||||||
if info.staged or info.unstaged:
|
|
||||||
dirty_bits.append(f"{info.staged + info.unstaged} changed")
|
|
||||||
if info.untracked:
|
|
||||||
dirty_bits.append(f"{info.untracked} untracked")
|
|
||||||
if dirty_bits:
|
|
||||||
bits.append("+".join(dirty_bits))
|
|
||||||
file.add_string_attribute("repo_status", ", ".join(bits) if bits else "clean")
|
|
||||||
|
|
||||||
def get_name_and_desc(self):
|
|
||||||
return [("nemo-repo-emblems-columns:::Git Branch / Git Status list-view columns")]
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# PropertyPageProvider: "Git" tab in Properties dialog
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class RepoPropertyPageProvider(GObject.GObject, Nemo.PropertyPageProvider, Nemo.NameAndDescProvider):
|
|
||||||
|
|
||||||
def get_property_pages(self, files):
|
|
||||||
if len(files) != 1:
|
|
||||||
return []
|
|
||||||
file = files[0]
|
|
||||||
if file.get_uri_scheme() != "file" or not file.is_directory():
|
|
||||||
return []
|
|
||||||
|
|
||||||
path = GLib.filename_from_uri(file.get_uri())[0]
|
|
||||||
repo_root = find_repo_root(path)
|
|
||||||
if repo_root is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
info = CACHE.get(repo_root)
|
|
||||||
|
|
||||||
grid = Gtk.Grid()
|
|
||||||
grid.set_row_spacing(6)
|
|
||||||
grid.set_column_spacing(12)
|
|
||||||
grid.set_margin_start(12)
|
|
||||||
grid.set_margin_end(12)
|
|
||||||
grid.set_margin_top(12)
|
|
||||||
grid.set_margin_bottom(12)
|
|
||||||
|
|
||||||
def add_row(row, label_text, value_text):
|
|
||||||
lbl = Gtk.Label(label=label_text)
|
|
||||||
lbl.set_xalign(0)
|
|
||||||
lbl.get_style_context().add_class("dim-label")
|
|
||||||
val = Gtk.Label(label=value_text)
|
|
||||||
val.set_xalign(0)
|
|
||||||
val.set_selectable(True)
|
|
||||||
val.set_line_wrap(True)
|
|
||||||
grid.attach(lbl, 0, row, 1, 1)
|
|
||||||
grid.attach(val, 1, row, 1, 1)
|
|
||||||
|
|
||||||
if info.error:
|
|
||||||
add_row(0, "Status", info.error)
|
|
||||||
else:
|
|
||||||
row = 0
|
|
||||||
add_row(row, "Repository", repo_root); row += 1
|
|
||||||
add_row(row, "Branch", info.branch or "(detached HEAD)"); row += 1
|
|
||||||
add_row(row, "Upstream", info.upstream if info.has_upstream else "(none)"); row += 1
|
|
||||||
add_row(row, "Ahead / Behind", f"{info.ahead} ahead, {info.behind} behind"); row += 1
|
|
||||||
add_row(row, "Staged changes", str(info.staged)); row += 1
|
|
||||||
add_row(row, "Unstaged changes", str(info.unstaged)); row += 1
|
|
||||||
add_row(row, "Untracked files", str(info.untracked)); row += 1
|
|
||||||
if info.last_commit:
|
|
||||||
h, author, when, subject = info.last_commit
|
|
||||||
add_row(row, "Last commit", f"{h} — {subject}"); row += 1
|
|
||||||
add_row(row, "", f"by {author}, {when}"); row += 1
|
|
||||||
|
|
||||||
grid.show_all()
|
|
||||||
label = Gtk.Label(label="Git")
|
|
||||||
return [Nemo.PropertyPage(name="NemoRepoEmblems::git_page", label=label, page=grid)]
|
|
||||||
|
|
||||||
def get_name_and_desc(self):
|
|
||||||
return [("nemo-repo-emblems-properties:::\"Git\" tab in the folder Properties dialog")]
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# MenuProvider: right-click actions
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _resolve_terminal():
|
|
||||||
configured = CONFIG.terminal_command
|
|
||||||
if configured and shutil.which(configured.split(" ")[0]):
|
|
||||||
return configured
|
|
||||||
for candidate in TERMINAL_CANDIDATES:
|
|
||||||
if shutil.which(candidate):
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _launch_terminal(path, shell_command=None):
|
|
||||||
term = _resolve_terminal()
|
|
||||||
if not term:
|
|
||||||
logger.warning("No terminal emulator found")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
if shell_command:
|
|
||||||
inner = f"cd {GLib.shell_quote(path)} && {shell_command}; exec bash"
|
|
||||||
subprocess.Popen([term, "--", "bash", "-c", inner], cwd=path)
|
|
||||||
else:
|
|
||||||
subprocess.Popen([term], cwd=path)
|
|
||||||
except OSError as e:
|
|
||||||
logger.warning("Failed to launch terminal (%s): %s", term, e)
|
|
||||||
|
|
||||||
|
|
||||||
def _launch_editor(path):
|
|
||||||
editor = CONFIG.editor_command
|
|
||||||
binary = shutil.which(editor.split(" ")[0]) if editor else None
|
|
||||||
if not binary:
|
|
||||||
logger.warning("Editor command %r not found in PATH", editor)
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
subprocess.Popen(editor.split(" ") + [path])
|
|
||||||
except OSError as e:
|
|
||||||
logger.warning("Failed to launch editor: %s", e)
|
|
||||||
|
|
||||||
|
|
||||||
class RepoMenuProvider(GObject.GObject, Nemo.MenuProvider, Nemo.NameAndDescProvider):
|
|
||||||
|
|
||||||
def _build_items(self, path, repo_root):
|
|
||||||
items = []
|
|
||||||
is_repo = repo_root is not None
|
|
||||||
|
|
||||||
if CONFIG.action_enabled("open_terminal"):
|
|
||||||
item = Nemo.MenuItem(name="NemoRepoEmblems::open_terminal",
|
|
||||||
label="Open Terminal Here",
|
|
||||||
tip=f"Open a terminal in {path}")
|
|
||||||
item.connect("activate", lambda *_: _launch_terminal(path))
|
|
||||||
items.append(item)
|
|
||||||
|
|
||||||
if CONFIG.action_enabled("open_editor") and CODE_BIN:
|
|
||||||
item = Nemo.MenuItem(name="NemoRepoEmblems::open_editor",
|
|
||||||
label="Open in Editor",
|
|
||||||
tip=f"Open {path} in {CONFIG.editor_command}")
|
|
||||||
item.connect("activate", lambda *_: _launch_editor(path))
|
|
||||||
items.append(item)
|
|
||||||
|
|
||||||
if is_repo and GIT_BIN:
|
|
||||||
if CONFIG.action_enabled("git_status"):
|
|
||||||
item = Nemo.MenuItem(name="NemoRepoEmblems::git_status",
|
|
||||||
label="Git Status (terminal)",
|
|
||||||
tip="Run git status in a terminal")
|
|
||||||
item.connect("activate", lambda *_: _launch_terminal(repo_root, "git status"))
|
|
||||||
items.append(item)
|
|
||||||
|
|
||||||
if CONFIG.action_enabled("git_log"):
|
|
||||||
item = Nemo.MenuItem(name="NemoRepoEmblems::git_log",
|
|
||||||
label="Git Log (terminal)",
|
|
||||||
tip="Run git log in a terminal")
|
|
||||||
item.connect("activate", lambda *_: _launch_terminal(
|
|
||||||
repo_root, "git log --oneline --graph --decorate -30"))
|
|
||||||
items.append(item)
|
|
||||||
|
|
||||||
if CONFIG.action_enabled("git_pull"):
|
|
||||||
item = Nemo.MenuItem(name="NemoRepoEmblems::git_pull",
|
|
||||||
label="Git Pull (terminal)",
|
|
||||||
tip="Run git pull in a terminal")
|
|
||||||
item.connect("activate", lambda *_: _launch_terminal(repo_root, "git pull"))
|
|
||||||
items.append(item)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
def get_file_items(self, window, files):
|
|
||||||
if len(files) != 1 or not files[0].is_directory():
|
|
||||||
return []
|
|
||||||
file = files[0]
|
|
||||||
if file.get_uri_scheme() != "file":
|
|
||||||
return []
|
|
||||||
path = GLib.filename_from_uri(file.get_uri())[0]
|
|
||||||
return self._build_items(path, find_repo_root(path))
|
|
||||||
|
|
||||||
def get_background_items(self, window, current_folder):
|
|
||||||
if current_folder.get_uri_scheme() != "file":
|
|
||||||
return []
|
|
||||||
path = GLib.filename_from_uri(current_folder.get_uri())[0]
|
|
||||||
return self._build_items(path, find_repo_root(path))
|
|
||||||
|
|
||||||
def get_name_and_desc(self):
|
|
||||||
return [("nemo-repo-emblems-menu:::Repo-aware right-click actions (terminal / editor / git status-log-pull)")]
|
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Removes this plugin from Nemo. The core, the other plugins and the config
|
||||||
|
# file are left alone.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
dataHome="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||||
|
extensionsDir="$dataHome/nemo-python/extensions"
|
||||||
|
target="$extensionsDir/nemoRepoEmblems.py"
|
||||||
|
|
||||||
|
if [ -e "$target" ]; then
|
||||||
|
rm -f "$target"
|
||||||
|
echo "Removed $target"
|
||||||
|
else
|
||||||
|
echo "Not installed: $target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$extensionsDir/__pycache__/nemoRepoEmblems".*.pyc
|
||||||
|
|
||||||
|
iconDir="$dataHome/icons/hicolor/scalable/emblems"
|
||||||
|
for name in emblem-repo-git emblem-repo-claude emblem-repo-dirty emblem-repo-unpushed; do
|
||||||
|
if [ -e "$iconDir/$name.svg" ]; then
|
||||||
|
rm -f "$iconDir/$name.svg"
|
||||||
|
echo "Removed $iconDir/$name.svg"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
||||||
|
gtk-update-icon-cache -f -t "$dataHome/icons/hicolor" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Restart Nemo to unload it: nemo -q"
|
||||||
Reference in new issue
Block a user