diff --git a/cmds/system/setup/run.sh b/cmds/system/setup/run.sh index f9d2c492..f467847f 100755 --- a/cmds/system/setup/run.sh +++ b/cmds/system/setup/run.sh @@ -158,6 +158,11 @@ EOF _write_shell_pinned_apps + if [[ -n $root_device ]]; then + sudo timeshift --delete-all 2>/dev/null || true + sudo timeshift --create --comments "Initial system backup" --tags "O" 2>/dev/null || true + fi + sudo rm -f /etc/sudoers.d/retro-post-install rm "$HOME/.retro_install" diff --git a/cmds/tools/settings/core/shell_config.py b/cmds/tools/settings/core/shell_config.py index 8a2a2d90..ae9f6f0b 100644 --- a/cmds/tools/settings/core/shell_config.py +++ b/cmds/tools/settings/core/shell_config.py @@ -883,3 +883,24 @@ def load_dashboard() -> dict: def save_dashboard(data: dict) -> None: save_shell_json("dashboard", data) + + +# ── notifications.json ───────────────────────────────────────────────── + +NOTIFICATIONS_DEFAULTS: dict = { + "soundEnabled": True, + "soundFile": "retro-default.mp3", + "soundVolume": 40, +} + + +def notifications_path() -> Path: + return shell_config_dir() / "notifications.json" + + +def load_notifications() -> dict: + return load_shell_json("notifications", NOTIFICATIONS_DEFAULTS) + + +def save_notifications(data: dict) -> None: + save_shell_json("notifications", data) diff --git a/cmds/tools/settings/pages/fan_control.py b/cmds/tools/settings/pages/fan_control.py index 3cdccaa6..0b93b468 100644 --- a/cmds/tools/settings/pages/fan_control.py +++ b/cmds/tools/settings/pages/fan_control.py @@ -24,6 +24,7 @@ _REFRESH_MS = 2000 _sudoers_done = False _profile_change_pending = False +_acpi_profiles = [] def _ensure_fan_sudoers() -> None: @@ -77,6 +78,107 @@ def _run_core_pkexec(*args: str) -> bool: return False +# User-friendly profile names and their possible system equivalents +_PROFILE_MAP: dict[str, list[str]] = { + "Quiet": ["quiet", "low-power", "powersave"], + "Cool": ["cool"], + "Balanced": ["balanced", "normal"], + "Performance": ["performance", "high-performance"], +} + + +def _read_hw_temps() -> dict[str, str]: + """Read temperatures from available hardware sensors.""" + temps: dict[str, str] = {} + + # CPU temperature + try: + for z in sorted(os.listdir("/sys/class/thermal/")): + if not z.startswith("thermal_zone"): + continue + path = f"/sys/class/thermal/{z}" + try: + ttype = open(f"{path}/type").read().strip() + if ttype in ("x86_pkg_temp", "cpu-thermal", "coretemp"): + temp = int(open(f"{path}/temp").read().strip()) / 1000 + temps["CPU"] = f"{temp:.0f}°C" + break + except (OSError, ValueError): + continue + except OSError: + pass + + # GPU temperature (AMD/NVIDIA via hwmon) + try: + for d in sorted(os.listdir("/sys/class/drm/")): + if not d.startswith("card"): + continue + hwmon = f"/sys/class/drm/{d}/device/hwmon" + if not os.path.isdir(hwmon): + continue + for hm in os.listdir(hwmon): + p = f"{hwmon}/{hm}/temp1_input" + if os.path.isfile(p): + temp = int(open(p).read().strip()) / 1000 + temps["GPU"] = f"{temp:.0f}°C" + break + if "GPU" in temps: + break + except OSError: + pass + + # NVMe SSD temperature + try: + for name in sorted(os.listdir("/sys/class/hwmon/")): + path = f"/sys/class/hwmon/{name}" + try: + label_file = f"{path}/name" + if os.path.isfile(label_file): + label = open(label_file).read().strip().lower() + if "nvme" in label or "nvme" in name: + temp_file = f"{path}/temp1_input" + if os.path.isfile(temp_file): + temp = int(open(temp_file).read().strip()) / 1000 + temps["SSD"] = f"{temp:.0f}°C" + break + except (OSError, ValueError): + continue + except OSError: + pass + + # Battery temperature (if available) + try: + bat_path = "/sys/class/power_supply/BAT*" + import glob as glob_mod + for bat in sorted(glob_mod.glob(bat_path)): + temp_file = f"{bat}/temp" + if os.path.isfile(temp_file): + temp = int(open(temp_file).read().strip()) / 1000 + temps["Battery"] = f"{temp:.0f}°C" + break + except (OSError, ValueError): + pass + + return temps + + +def _load_acpi_profiles() -> list[str]: + """Load available ACPI platform profile choices from the system. + + Returns user-friendly profile names that are actually available. + """ + try: + with open("/sys/firmware/acpi/platform_profile_choices", "r") as f: + available = [line.strip().lower() for line in f.read().splitlines() if line.strip()] + result = [] + for name, choices in _PROFILE_MAP.items(): + if any(c in available for c in choices): + result.append(name) + return result if result else ["Quiet", "Balanced", "Performance"] + except (FileNotFoundError, PermissionError): + return ["Quiet", "Balanced", "Performance"] + + class _FanRow: def __init__(self, fan: dict, on_change): self._fan = fan @@ -197,7 +299,8 @@ def __init__(self, window: "RetroSettingsWindow"): self._engine_lbl: Gtk.Label | None = None self._temp_lbl: Gtk.Label | None = None self._profile_dd: Gtk.DropDown | None = None - self._acpi_lbl: Gtk.Label | None = None + self._temps_lbl: Gtk.Label | None = None + self._refreshing = False def build(self, header: Adw.HeaderBar) -> Adw.ToolbarView: _ensure_fan_sudoers() @@ -208,43 +311,29 @@ def build(self, header: Adw.HeaderBar) -> Adw.ToolbarView: refresh_btn.connect("clicked", lambda _: self._refresh()) header.pack_start(refresh_btn) - # Master control - master_group = Adw.PreferencesGroup(title="Fan Control") - - self._master_switch = Gtk.Switch() - self._master_switch.set_valign(Gtk.Align.CENTER) - self._master_switch.connect("notify::active", self._on_master_toggled) - master_row = Adw.ActionRow(title="Master Control", subtitle="Enable manual fan curve control") - master_row.add_prefix(Gtk.Image.new_from_icon_name("system-run-symbolic")) - master_row.add_suffix(self._master_switch) - master_group.add(master_row) - - self._engine_lbl = Gtk.Label(label="—", halign=Gtk.Align.START) - self._engine_lbl.set_valign(Gtk.Align.CENTER) - eng_row = Adw.ActionRow(title="Engine") - eng_row.add_suffix(self._engine_lbl) - master_group.add(eng_row) - - self._temp_lbl = Gtk.Label(label="—", halign=Gtk.Align.START) - self._temp_lbl.set_valign(Gtk.Align.CENTER) - temp_row = Adw.ActionRow(title="CPU Temperature") - temp_row.add_suffix(self._temp_lbl) - master_group.add(temp_row) - - self._profile_dd = Gtk.DropDown(model=Gtk.StringList.new(["Quiet", "Balanced", "Performance"])) + # Profile group — ACPI platform profiles + Hardware temps + profile_group = Adw.PreferencesGroup(title="Profile") + + # Load available ACPI profiles from the system + global _acpi_profiles + _acpi_profiles = _load_acpi_profiles() + profile_model = Gtk.StringList.new(_acpi_profiles) + self._profile_dd = Gtk.DropDown(model=profile_model) self._profile_dd.set_valign(Gtk.Align.CENTER) self._profile_dd.connect("notify::selected", self._on_profile_changed) - prof_row = Adw.ActionRow(title="Profile") + prof_row = Adw.ActionRow(title="Profile", subtitle="Power profile") prof_row.add_suffix(self._profile_dd) - master_group.add(prof_row) + profile_group.add(prof_row) - self._acpi_lbl = Gtk.Label(label="", halign=Gtk.Align.START) - self._acpi_lbl.set_valign(Gtk.Align.CENTER) - acpi_row = Adw.ActionRow(title="ACPI Platform Profile") - acpi_row.add_suffix(self._acpi_lbl) - master_group.add(acpi_row) + # Hardware temperatures + self._temps_lbl = Gtk.Label(label="", halign=Gtk.Align.START) + self._temps_lbl.set_valign(Gtk.Align.CENTER) + self._temps_lbl.set_selectable(True) + temps_row = Adw.ActionRow(title="Temperatures", subtitle="CPU · GPU · SSD · Battery") + temps_row.add_suffix(self._temps_lbl) + profile_group.add(temps_row) - self._content_box.append(master_group) + self._content_box.append(profile_group) # Fan list self._fans_group = Adw.PreferencesGroup(title="Fans") @@ -254,6 +343,13 @@ def build(self, header: Adw.HeaderBar) -> Adw.ToolbarView: return toolbar_view def _refresh(self) -> None: + self._refreshing = True + try: + self._refresh_impl() + finally: + self._refreshing = False + + def _refresh_impl(self) -> None: data = _run_core("--json") if not data: return @@ -262,27 +358,6 @@ def _refresh(self) -> None: except (json.JSONDecodeError, ValueError): return - # Overview - if self._master_switch is not None: - self._master_switch.handler_block_by_func(self._on_master_toggled) - self._master_switch.set_active(info.get("master", False)) - self._master_switch.handler_unblock_by_func(self._on_master_toggled) - - if self._engine_lbl: - self._engine_lbl.set_text(info.get("engine", "—")) - if self._temp_lbl: - self._temp_lbl.set_text(f"{info.get('cpu_temp', 0)}°C") - - profile_names = {"quiet": 0, "balanced": 1, "performance": 2} - if self._profile_dd and not _profile_change_pending: - self._profile_dd.handler_block_by_func(self._on_profile_changed) - self._profile_dd.set_selected(profile_names.get(info.get("profile", "balanced"), 1)) - self._profile_dd.handler_unblock_by_func(self._on_profile_changed) - - if self._acpi_lbl: - acpi = info.get("acpi_choices", "") - self._acpi_lbl.set_text(acpi if acpi else "Not available") - # Fans fans = info.get("fans", []) existing_ids = {r.fan_id for r in self._fan_rows} @@ -305,18 +380,43 @@ def _refresh(self) -> None: self._fan_rows.append(row) self._fans_group.add(row.widget) - def _on_master_toggled(self, switch, _pspec) -> None: - val = "on" if switch.get_active() else "off" - threading.Thread(target=_run_core_pkexec, args=("--set-master", val), daemon=True).start() - self._mark_dirty() + # Profile — use ACPI profiles from the system + global _acpi_profiles + _acpi_profiles = _load_acpi_profiles() + if self._profile_dd: + profile_model = Gtk.StringList.new(_acpi_profiles) + self._profile_dd.set_model(profile_model) + # Select the first available profile + if _acpi_profiles: + self._profile_dd.set_selected(0) + + # Update hardware temperatures + if self._temps_lbl: + temps = _read_hw_temps() + if temps: + parts = [f"{k}: {v}" for k, v in temps.items()] + self._temps_lbl.set_text(" · ".join(parts)) + else: + self._temps_lbl.set_text("No sensors detected") def _on_profile_changed(self, _dd, _pspec) -> None: - names = {0: "quiet", 1: "balanced", 2: "performance"} - profile = names.get(self._profile_dd.get_selected(), "balanced") + # Skip if this change is from _refresh() resetting the model + if self._refreshing: + return + global _profile_change_pending _profile_change_pending = True # Schedule the flag to be cleared after 3 seconds - GLib.timeout_add(3000, lambda: (_profile_change_pending.__class__.__setattr__("_profile_change_pending", False) if hasattr(_profile_change_pending, "__class__") else False) or True) - threading.Thread(target=_run_core_pkexec, args=("--set-profile", profile), daemon=True).start() + def _clear_flag(): + global _profile_change_pending + _profile_change_pending = False + return False # Don't repeat + GLib.timeout_add(3000, _clear_flag) + profile = self._profile_dd.get_selected() + if profile is not None and profile < len(_acpi_profiles): + profile_name = _acpi_profiles[profile] + # Map user-friendly name to first available system profile name + system_name = _PROFILE_MAP.get(profile_name, [profile_name.lower()])[0] + threading.Thread(target=_run_core_pkexec, args=("--set-profile", system_name), daemon=True).start() self._mark_dirty() def _mark_dirty(self) -> None: @@ -358,3 +458,8 @@ def get_search_entries(self) -> list[dict]: "_group_label": "Fan Control", "_section_label": "System", }] + + def destroy(self) -> None: + if self._timer: + GLib.source_remove(self._timer) + self._timer = 0 \ No newline at end of file diff --git a/cmds/tools/settings/pages/home.py b/cmds/tools/settings/pages/home.py index 34415cd2..51b75268 100644 --- a/cmds/tools/settings/pages/home.py +++ b/cmds/tools/settings/pages/home.py @@ -18,6 +18,7 @@ from settings.ui import make_page_layout from settings.ui.icons import ( ABOUT_ICON, + NOTIFICATION_ICON, APPS_ICON, AUDIO_ICON, AUTOSTART_ICON, @@ -51,6 +52,7 @@ POWER_ICON, PRESETS_ICON, QUICKSHARE_ICON, + SERVICE_CONTROL_ICON, SETTINGS_ICON, SHELL_THEME_ICON, SIDEBAR_ICON, @@ -87,6 +89,7 @@ ("shell_dock", "Dock", "Dock applications", DOCK_ICON), ("shell_desktop", "Desktop", "Desktop widgets", DESKTOP_ICON), ("shell_lock", "Lockscreen", "Lock screen styling", LOCK_ICON), + ("shell_notifications", "Notifications", "Notification sound settings", NOTIFICATION_ICON), ("shell_presets", "Presets", "Shell presets", PRESETS_ICON), # Input ("binds", "Keybinds", "Keyboard shortcuts and bindings", BINDS_ICON), @@ -120,6 +123,7 @@ ("grub", "Bootloader", "Boot entries and kernel options", GRUB_ICON), ("driver", "Drivers", "Hardware drivers", DRIVER_ICON), ("daemon", "Daemon", "Retro background services", DAEMON_ICON), + ("service_control", "Services", "Manage systemd services", SERVICE_CONTROL_ICON), ("xdg", "Default Apps", "Default apps, MIME types and directories", APPS_ICON), ("backups", "Backups", "Timeshift system snapshots", TIMESHIFT_ICON), # Advanced diff --git a/cmds/tools/settings/pages/pending.py b/cmds/tools/settings/pages/pending.py index 5af19f21..becd02a7 100644 --- a/cmds/tools/settings/pages/pending.py +++ b/cmds/tools/settings/pages/pending.py @@ -49,6 +49,7 @@ "Window Rules", "Layer Rules", "Fan Control", + "Notifications", ) # Visual label and CSS class for each kind of change. diff --git a/cmds/tools/settings/pages/power.py b/cmds/tools/settings/pages/power.py index d3709071..34d300e6 100644 --- a/cmds/tools/settings/pages/power.py +++ b/cmds/tools/settings/pages/power.py @@ -272,6 +272,9 @@ def __init__(self, window: "RetroSettingsWindow"): self._pwr_btn_long_row: Adw.ComboRow | None = None self._lid_row: Adw.ComboRow | None = None self._has_hibernate = False + self._caffeine_on_charge_switch: Adw.SwitchRow | None = None + self._caffeine_on_charge_original = False + self._check_hibernate() # ── Hypridle state ── @@ -318,6 +321,16 @@ def build(self, header: Adw.HeaderBar) -> Adw.ToolbarView: self._profile_row = cr group.add(cr) + # Caffeine on Charging + self._caffeine_on_charge_switch = Adw.SwitchRow( + title="Caffeine on Charging", + subtitle="Automatically enable caffeine mode when plugged in to prevent sleep", + ) + self._caffeine_on_charge_original = self._orig.get("caffeine_on_charge", "false") == "true" + self._caffeine_on_charge_switch.set_active(self._caffeine_on_charge_original) + self._caffeine_on_charge_switch.connect("notify::active", self._on_caffeine_on_charge_changed) + group.add(self._caffeine_on_charge_switch) + for label, desc, _var, key in [ ("AC Saver", "AC power limit for power-saver mode", "PWR_AC_SAVER", "ac_saver"), ("AC Balanced", "AC power limit for balanced mode", "PWR_AC_BALANCED", "ac_balanced"), @@ -495,6 +508,8 @@ def _load_power_data(self) -> None: ]: self._orig[key] = get_var(var) or "" + self._orig["caffeine_on_charge"] = get_var("PWR_CAFFEINE_ON_CHARGE", "false") or "false" + try: r = subprocess.run(["bash", _PWR_CORE, "--get"], capture_output=True, text=True, timeout=5, stdin=subprocess.DEVNULL) self._orig["profile"] = r.stdout.strip() or "balanced" @@ -640,6 +655,9 @@ def _on_logind_changed(self, row: Adw.ComboRow, _pspec, var: str) -> None: self._logind_changed = True self._check_dirty() + def _on_caffeine_on_charge_changed(self, sw: Adw.SwitchRow, _pspec) -> None: + self._check_dirty() + def _on_logout_cmd_changed(self, entry: Gtk.Entry) -> None: from lib.python.variable import set_var set_var("RETRO_LOGOUT_CMD", entry.get_text().strip()) @@ -807,6 +825,12 @@ def _check_dirty(self) -> None: if get_var("RETRO_LOGOUT_CMD", "") != self._orig.get("logout_cmd", ""): self._dirty = True + # Caffeine on charge + if self._caffeine_on_charge_switch is not None: + caffeine_current = "true" if self._caffeine_on_charge_switch.get_active() else "false" + if caffeine_current != self._orig.get("caffeine_on_charge", "false"): + self._dirty = True + # Hypridle side self._hypridle_changed = self._enable_idle_value != self._enable_idle_original or any( getattr(self._general, f.name) != getattr(self._original_general, f.name) @@ -846,6 +870,12 @@ def mark_saved(self) -> None: self._orig[key] = get_var(var_name, "suspend") self._orig["logout_cmd"] = get_var("RETRO_LOGOUT_CMD", "") + # Caffeine on charge + if self._caffeine_on_charge_switch is not None: + caffeine_val = "true" if self._caffeine_on_charge_switch.get_active() else "false" + set_var("PWR_CAFFEINE_ON_CHARGE", caffeine_val) + self._orig["caffeine_on_charge"] = caffeine_val + # Hypridle side write_hypridle(general=self._general, listeners=self._listeners) set_var("HYPRIDLE_ENABLE", "true" if self._enable_idle_value else "false") @@ -879,6 +909,10 @@ def discard(self) -> None: if hasattr(self, "_logout_entry"): self._logout_entry.set_text(self._orig.get("logout_cmd") or _DEFAULT_LOGOUT_CMD) + # Caffeine on charge + if self._caffeine_on_charge_switch is not None: + self._caffeine_on_charge_switch.set_active(self._orig.get("caffeine_on_charge", "false") == "true") + # Hypridle side self._general = IdleGeneral( **{f.name: getattr(self._original_general, f.name) for f in IdleGeneral.__dataclass_fields__.values()} diff --git a/cmds/tools/settings/pages/service_control.py b/cmds/tools/settings/pages/service_control.py new file mode 100644 index 00000000..cfd7a3c9 --- /dev/null +++ b/cmds/tools/settings/pages/service_control.py @@ -0,0 +1,384 @@ +"""Service control page — manage systemd services with start/stop/restart/enable/disable.""" + +import subprocess +import threading +import time +from collections.abc import Iterable +from typing import TYPE_CHECKING + +from gi.repository import Adw, GLib, Gtk, Pango + +from settings.core.pending import PendingChange +from settings.ui import make_page_layout + +if TYPE_CHECKING: + from settings.window import RetroSettingsWindow + +_SERVICE_CORE = "/opt/retrolinux/scripts/service_core.sh" +_SERVICE_ICON = "system-service-symbolic" +_REFRESH_MS = 10000 + + +def _run_core(*args: str) -> str: + try: + r = subprocess.run( + ["bash", _SERVICE_CORE, *args], + capture_output=True, text=True, timeout=10, stdin=subprocess.DEVNULL, + ) + return r.stdout.strip() + except Exception: + return "" + + +def _list_services(filter_type: str = "all") -> list[dict]: + result = _run_core("--list", filter_type) + if not result or "result=none" in result: + return [] + services = [] + for line in result.splitlines(): + parts = line.split("|") + if len(parts) >= 6 and parts[0] == "service": + services.append({ + "name": parts[1], + "load": parts[2], + "active": parts[3], + "sub": parts[4], + "description": parts[5], + }) + return services + + +class _ServiceRow(Gtk.ListBoxRow): + """A single service row with status and action buttons.""" + + def __init__(self, svc: dict, on_action): + super().__init__() + self._name = svc["name"] + self._on_action = on_action + + active = svc.get("active", "inactive") + sub = svc.get("sub", "") + desc = svc.get("description", "") + is_active = active in ("active", "running") + is_failed = active == "failed" + + box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + box.set_margin_top(6) + box.set_margin_bottom(6) + box.set_margin_start(8) + box.set_margin_end(8) + + # Status icon + if is_failed: + icon_name = "dialog-error-symbolic" + icon_css = "error" + elif is_active: + icon_name = "emblem-ok-symbolic" + icon_css = "success" + else: + icon_name = "process-stop-symbolic" + icon_css = "dim-label" + + icon_img = Gtk.Image.new_from_icon_name(icon_name) + icon_img.set_valign(Gtk.Align.CENTER) + icon_img.set_pixel_size(20) + icon_img.add_css_class(icon_css) + box.append(icon_img) + + # Text + text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=1) + text_box.set_hexpand(True) + + name_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + name_lbl = Gtk.Label(label=self._name) + name_lbl.set_halign(Gtk.Align.START) + name_lbl.set_ellipsize(Pango.EllipsizeMode.END) + name_box.append(name_lbl) + + if is_active: + state_lbl = Gtk.Label(label=sub.upper()) + state_lbl.add_css_class("badge") + state_lbl.add_css_class("success") + name_box.append(state_lbl) + elif is_failed: + state_lbl = Gtk.Label(label="FAILED") + state_lbl.add_css_class("badge") + state_lbl.add_css_class("error") + name_box.append(state_lbl) + + text_box.append(name_box) + + if desc: + desc_lbl = Gtk.Label(label=desc) + desc_lbl.set_halign(Gtk.Align.START) + desc_lbl.set_ellipsize(Pango.EllipsizeMode.END) + desc_lbl.add_css_class("dim-label") + desc_lbl.add_css_class("caption") + text_box.append(desc_lbl) + + box.append(text_box) + + # Action buttons + btn_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=2) + btn_box.set_valign(Gtk.Align.CENTER) + + # Log button + log_btn = Gtk.Button(icon_name="utilities-terminal-symbolic") + log_btn.set_tooltip_text("View logs") + log_btn.set_size_request(32, 32) + log_btn.add_css_class("flat") + log_btn.connect("clicked", lambda _b: self._open_logs()) + btn_box.append(log_btn) + + if is_active: + stop = Gtk.Button(icon_name="media-playback-stop-symbolic") + stop.set_tooltip_text("Stop") + stop.set_size_request(32, 32) + stop.connect("clicked", lambda _b: on_action("stop", self._name)) + btn_box.append(stop) + + restart = Gtk.Button(icon_name="view-refresh-symbolic") + restart.set_tooltip_text("Restart") + restart.set_size_request(32, 32) + restart.connect("clicked", lambda _b: on_action("restart", self._name)) + btn_box.append(restart) + else: + start = Gtk.Button(icon_name="media-playback-start-symbolic") + start.set_tooltip_text("Start") + start.set_size_request(32, 32) + start.connect("clicked", lambda _b: on_action("start", self._name)) + btn_box.append(start) + + box.append(btn_box) + self.set_child(box) + + # CSS class based on state + if is_active: + self.add_css_class("option-managed") + else: + self.add_css_class("option-default") + + def _open_logs(self) -> None: + """Open journalctl logs for this service in a dialog.""" + dialog = Adw.Dialog() + dialog.set_title(f"Logs — {self._name}") + dialog.set_content_width(700) + dialog.set_content_height(500) + + toolbar = Adw.ToolbarView() + header = Adw.HeaderBar() + toolbar.add_top_bar(header) + + # Log text view + scrolled = Gtk.ScrolledWindow() + scrolled.set_vexpand(True) + + text_view = Gtk.TextView() + text_view.set_editable(False) + text_view.set_monospace(True) + text_view.set_wrap_mode(Gtk.WrapMode.NONE) + text_view.add_css_class("view") + + # Load logs in background + def load_logs(): + try: + r = subprocess.run( + ["journalctl", "-u", self._name, "-n", "200", "--no-pager", "-o", "short-iso"], + capture_output=True, text=True, timeout=10, + ) + logs = r.stdout or "No logs available" + except Exception as e: + logs = f"Error loading logs: {e}" + + def apply_logs(): + text_view.get_buffer().set_text(logs) + # Scroll to bottom + adj = scrolled.get_vadjustment() + adj.set_value(adj.get_upper() - adj.get_page_size()) + + GLib.idle_add(apply_logs) + + threading.Thread(target=load_logs, daemon=True).start() + + scrolled.set_child(text_view) + toolbar.set_content(scrolled) + dialog.set_child(toolbar) + dialog.present(self.get_root()) + + +class ServiceControlPage: + def __init__(self, window: "RetroSettingsWindow"): + self._window = window + self._on_dirty_changed = None + self._content_box: Gtk.Box | None = None + self._timer = 0 + self._services: list[dict] = [] + self._filter = "all" + self._search = "" + self._listbox: Gtk.ListBox | None = None + self._count_label: Gtk.Label | None = None + + def build(self, header: Adw.HeaderBar) -> Adw.ToolbarView: + toolbar_view, _, self._content_box, _ = make_page_layout(header=header) + + # Refresh button + refresh_btn = Gtk.Button(icon_name="view-refresh-symbolic") + refresh_btn.set_tooltip_text("Refresh services") + refresh_btn.connect("clicked", lambda _: self._refresh()) + header.pack_start(refresh_btn) + + # ── Status section ── + status_group = Adw.PreferencesGroup(title="Service Status", description="Overview of system services managed by systemd.") + + self._count_label = Gtk.Label(label="Loading\u2026") + self._count_label.set_valign(Gtk.Align.CENTER) + self._count_label.add_css_class("dim-label") + count_row = Adw.ActionRow(title="Total Services", subtitle="Number of services matching the current filter") + count_row.add_suffix(self._count_label) + status_group.add(count_row) + + clean_btn = Gtk.Button(label="Reset Failed State") + clean_btn.set_valign(Gtk.Align.CENTER) + clean_btn.add_css_class("destructive-action") + clean_btn.set_tooltip_text("Reset all failed services") + clean_btn.connect("clicked", lambda _: self._clean_failed()) + clean_row = Adw.ActionRow(title="Reset Failed", subtitle="Clear the failed state of all services") + clean_row.add_suffix(clean_btn) + status_group.add(clean_row) + + self._content_box.append(status_group) + + # ── Search and Filter row ── + search = Gtk.SearchEntry() + search.set_placeholder_text("Search services\u2026") + search.set_hexpand(True) + search.connect("search-changed", self._on_search_changed) + self._search_entry = search + + self._filter_dd = Gtk.DropDown(model=Gtk.StringList.new([ + "All", "Running", "Failed", "Enabled", + ])) + self._filter_dd.set_valign(Gtk.Align.CENTER) + self._filter_dd.connect("notify::selected", self._on_filter_changed) + + search_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) + search_bar.set_margin_start(12) + search_bar.set_margin_end(12) + search_bar.set_margin_top(8) + search_bar.set_margin_bottom(8) + search_bar.append(search) + search_bar.append(self._filter_dd) + self._content_box.append(search_bar) + + # ── Service list ── + list_group = Adw.PreferencesGroup(title="Services", description="Click a service to start, stop, or restart it. Active services are shown in green.") + self._listbox = Gtk.ListBox() + self._listbox.set_selection_mode(Gtk.SelectionMode.NONE) + self._listbox.add_css_class("boxed-list") + list_group.add(self._listbox) + self._content_box.append(list_group) + + self._refresh() + return toolbar_view + + def _on_filter_changed(self, _dd, _pspec) -> None: + idx = self._filter_dd.get_selected() + self._filter = ["all", "running", "failed", "enabled"][idx] + self._refresh() + + # ── Actions ── + + def _on_search_changed(self, _entry: Gtk.SearchEntry) -> None: + self._search = self._search_entry.get_text().lower().strip() + self._rebuild_list() + + def _refresh(self) -> None: + def load(): + svcs = _list_services(self._filter) + GLib.idle_add(self._apply, svcs) + threading.Thread(target=load, daemon=True).start() + + def _apply(self, svcs: list[dict]) -> None: + self._services = svcs + self._rebuild_list() + n = len(svcs) + self._count_label.set_text(f"{n} service{'s' if n != 1 else ''}") + + def _rebuild_list(self) -> None: + if not self._listbox: + return + while child := self._listbox.get_first_child(): + self._listbox.remove(child) + + q = self._search + filtered = [s for s in self._services + if not q or q in s["name"].lower() or q in s.get("description", "").lower()] + + if not filtered: + empty = Gtk.Label(label="No services found") + empty.set_margin_top(24) + empty.set_margin_bottom(24) + empty.set_halign(Gtk.Align.CENTER) + empty.add_css_class("dim-label") + row = Gtk.ListBoxRow() + row.set_child(empty) + row.set_activatable(False) + self._listbox.append(row) + return + + for svc in filtered: + row = _ServiceRow(svc, self._do_action) + self._listbox.append(row) + + def _do_action(self, action: str, name: str) -> None: + def run(): + _run_core(f"--{action}", name) + time.sleep(0.3) + GLib.idle_add(self._refresh) + threading.Thread(target=run, daemon=True).start() + + def _clean_failed(self) -> None: + def run(): + _run_core("--clean-failed") + time.sleep(0.3) + GLib.idle_add(self._refresh) + threading.Thread(target=run, daemon=True).start() + + # ── Lifecycle ── + + def on_shown(self) -> None: + self._timer = GLib.timeout_add(_REFRESH_MS, self._on_tick) + + def on_hidden(self) -> None: + if self._timer: + GLib.source_remove(self._timer) + self._timer = 0 + + def _on_tick(self) -> bool: + self._refresh() + return True + + def is_dirty(self) -> bool: + return False + + def mark_saved(self) -> None: + pass + + def discard(self) -> None: + pass + + def iter_pending_changes(self) -> Iterable[PendingChange]: + return [] + + def get_search_entries(self) -> list[dict]: + return [{ + "key": "service:control", + "label": "Services", + "description": "Manage systemd services, start/stop/restart, enable/disable", + "_group_id": "service_control", + "_group_label": "Services", + "_section_label": "System", + }] + + +__all__ = ["ServiceControlPage"] diff --git a/cmds/tools/settings/pages/shell_notifications.py b/cmds/tools/settings/pages/shell_notifications.py new file mode 100644 index 00000000..ec9da8b2 --- /dev/null +++ b/cmds/tools/settings/pages/shell_notifications.py @@ -0,0 +1,264 @@ +"""Shell Notifications page — configure notification sounds. + +Values are written to ``~/.config/retro/shell/notifications.json``; the shell's +``FileView`` watches that file with ``watchChanges`` and reloads on external +writes, so changes apply live without a shell restart. +""" + +import os +import subprocess +from collections.abc import Iterable +from typing import TYPE_CHECKING + +from gi.repository import Adw, Gtk + +from settings.core.pending import PendingChange +from settings.core.shell_config import NOTIFICATIONS_DEFAULTS, load_notifications, save_notifications +from settings.ui import make_page_layout +from settings.ui.icons import NOTIFICATION_ICON +from settings.ui.managed_row import ManagedRow, make_combo_row, make_spin_int_row + +if TYPE_CHECKING: + from settings.window import RetroSettingsWindow + +_SND_DIR = "/opt/retrolinux/modules/retroshell/files/assets/sound" + +_SOUND_OPTIONS = [ + ("retro-default.mp3", "Retro Default"), + ("gentle-chime.wav", "Gentle Chime"), + ("correct-answer.wav", "Correct Answer"), + ("double-beep.wav", "Double Beep"), + ("dry-pop.wav", "Dry Pop"), + ("long-pop.wav", "Long Pop"), + ("message-pop.mp3", "Message Pop"), + ("dragon-chime.mp3", "Dragon Chime"), + ("bright-chime.mp3", "Bright Chime"), + ("new-chime.mp3", "New Chime"), +] + + +class ShellNotificationsPage: + """Shell notification settings — writes ``notifications.json`` on save.""" + + def __init__(self, window: "RetroSettingsWindow"): + self._window = window + self._on_dirty_changed = None + self._data = load_notifications() + self._saved = dict(self._data) + self._rows: dict[str, ManagedRow] = {} + + def build(self, header: Adw.HeaderBar) -> Adw.ToolbarView: + toolbar, _page_box, content_box, _scrolled = make_page_layout(header=header) + + # Test notification button in header + test_btn = Gtk.Button(icon_name="feather-bell-symbolic") + test_btn.set_tooltip_text("Send test notification") + test_btn.add_css_class("flat") + test_btn.connect("clicked", self._on_test_notification) + header.pack_start(test_btn) + + sounds_group = Adw.PreferencesGroup( + title="Sounds", + description="Sound, volume, and enable/disable.", + ) + self._add_switch(sounds_group, "soundEnabled", "Enable Sounds", + subtitle="Play sounds when notifications arrive") + sound_mrow = self._add_combo(sounds_group, "soundFile", "Notification Sound", _SOUND_OPTIONS, + subtitle="Which sound plays on notification") + preview_btn = Gtk.Button(icon_name="media-playback-start-symbolic") + preview_btn.set_valign(Gtk.Align.CENTER) + preview_btn.add_css_class("flat") + preview_btn.set_tooltip_text("Preview sound") + preview_btn.connect("clicked", self._on_preview) + sound_mrow.row.add_suffix(preview_btn) + self._add_spin(sounds_group, "soundVolume", "Volume", lower=0, upper=100, suffix="%", + subtitle="Notification sound volume percentage") + content_box.append(sounds_group) + + return toolbar + + # ── Row builders ── + + def _add_switch(self, group: Adw.PreferencesGroup, key: str, label: str, + *, subtitle: str = "") -> ManagedRow: + row = Adw.SwitchRow(title=label, subtitle=subtitle) + row.set_active(bool(self._data.get(key, NOTIFICATIONS_DEFAULTS[key]))) + group.add(row) + + def get_value(): + return row.get_active() + def set_silent(value): + row.set_active(bool(value)) + + mrow = ManagedRow(row, default=NOTIFICATIONS_DEFAULTS[key], + baseline=self._saved.get(key, NOTIFICATIONS_DEFAULTS[key]), + get_value=get_value, set_value_silent=set_silent, + on_value_set=lambda v, k=key: self._on_change(k, v)) + self._rows[key] = mrow + row.connect("notify::active", lambda *a, k=key, m=mrow: (setattr(self, '_data', {**self._data, k: m.value}), m.refresh(), self._notify_dirty())) + return mrow + + def _add_combo(self, group: Adw.PreferencesGroup, key: str, label: str, + options: list[tuple[str, str]], *, subtitle: str = "") -> ManagedRow: + ids = [o[0] for o in options] + labels = [o[1] for o in options] + current = self._data.get(key, NOTIFICATIONS_DEFAULTS[key]) + try: + selected = ids.index(current) + except ValueError: + selected = 0 + row = make_combo_row(label, model=Gtk.StringList.new(labels), selected=selected, subtitle=subtitle) + group.add(row) + + def get_value(): + return ids[row.get_selected()] if 0 <= row.get_selected() < len(ids) else ids[0] + def set_silent(value): + try: row.set_selected(ids.index(value)) + except ValueError: row.set_selected(0) + + mrow = ManagedRow(row, default=NOTIFICATIONS_DEFAULTS[key], + baseline=self._saved.get(key, NOTIFICATIONS_DEFAULTS[key]), + get_value=get_value, set_value_silent=set_silent, + on_value_set=lambda v, k=key: self._on_change(k, v)) + self._rows[key] = mrow + row.connect("notify::selected", lambda *a, k=key, m=mrow: (setattr(self, '_data', {**self._data, k: m.value}), m.refresh(), self._notify_dirty())) + return mrow + + def _add_spin(self, group: Adw.PreferencesGroup, key: str, label: str, + *, lower: int, upper: int, suffix: str, subtitle: str = "") -> ManagedRow: + row, spin = make_spin_int_row(label, value=int(self._data.get(key, NOTIFICATIONS_DEFAULTS[key])), + lower=lower, upper=upper, step=5, page_step=10, subtitle=subtitle) + group.add(row) + if suffix: + l = Gtk.Label(label=suffix); l.add_css_class("dim-label"); l.set_valign(Gtk.Align.CENTER); row.add_suffix(l) + + def get_value(): return int(spin.get_value()) + def set_silent(value): spin.set_value(int(value)) + + mrow = ManagedRow(row, default=NOTIFICATIONS_DEFAULTS[key], + baseline=self._saved.get(key, NOTIFICATIONS_DEFAULTS[key]), + get_value=get_value, set_value_silent=set_silent, + on_value_set=lambda v, k=key: self._on_change(k, v)) + self._rows[key] = mrow + spin.connect("value-changed", lambda *a, k=key, m=mrow: (setattr(self, '_data', {**self._data, k: m.value}), m.refresh(), self._notify_dirty())) + return mrow + + # ── Sound playback ── + + def _play_sound(self) -> None: + """Play the currently configured notification sound.""" + sound_file = self._data.get("soundFile", NOTIFICATIONS_DEFAULTS["soundFile"]) + volume = int(self._data.get("soundVolume", NOTIFICATIONS_DEFAULTS["soundVolume"])) + enabled = self._data.get("soundEnabled", NOTIFICATIONS_DEFAULTS["soundEnabled"]) + if not enabled: + return + path = os.path.join(_SND_DIR, sound_file) + if not os.path.exists(path): + return + pa_vol = int(volume * 65536 / 100) + try: + subprocess.Popen( + ["paplay", "--volume", str(pa_vol), path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + try: + subprocess.Popen( + ["mpg123", "-q", "--gain", str(volume), path], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + pass + + # ── Preview ── + + def _on_test_notification(self, _btn) -> None: + """Send a test notification.""" + try: + subprocess.Popen( + [ + "notify-send", + "-a", "RetroLinux Settings", + "-i", "feather-bell-symbolic", + "-u", "normal", + "Test Notification", + "This is a test notification from RetroLinux Settings.", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except Exception: + pass + + def _on_preview(self, _btn) -> None: + self._play_sound() + + # ── Change plumbing ── + + def _on_change(self, key: str, value) -> None: + self._data[key] = value + self._notify_dirty() + + def _write_live(self) -> None: + save_notifications(self._data) + + def _notify_dirty(self) -> None: + self._write_live() + if self._on_dirty_changed is not None: + self._on_dirty_changed() + + # ── Lifecycle ── + + def is_dirty(self) -> bool: + return self._data != self._saved + + def mark_saved(self) -> None: + if not self.is_dirty(): + return + save_notifications(self._data) + self._saved = dict(self._data) + for key, mrow in self._rows.items(): + mrow.set_baseline(self._data.get(key, NOTIFICATIONS_DEFAULTS[key])) + + def discard(self) -> None: + self._data = dict(self._saved) + for mrow in self._rows.values(): + mrow.discard() + self._write_live() + + def reload_from_disk(self) -> None: + self._data = load_notifications() + self._saved = dict(self._data) + for key, mrow in self._rows.items(): + value = self._data.get(key, NOTIFICATIONS_DEFAULTS[key]) + mrow.apply_value(value) + mrow.set_baseline(value) + + # ── Pending changes ── + + def iter_pending_changes(self) -> Iterable[PendingChange]: + if not self.is_dirty(): + return + changed = [] + for key in self._rows: + if self._data.get(key) != self._saved.get(key): + changed.append({"soundEnabled": "Enable", "soundFile": "Sound", + "soundVolume": "Volume"}.get(key, key)) + yield PendingChange(category="Notifications", title="Notifications", + subtitle=", ".join(changed[:3]), navigate_to="shell_notifications", + icon=NOTIFICATION_ICON, kind="modified", revert=self.discard) + + # ── Search ── + + def get_search_entries(self) -> list[dict]: + return [ + {"key": "shell_notifications:sounds", "label": "Notifications", + "description": "Sound, volume, and enable/disable", + "_group_id": "shell_notifications", "_group_label": "Notifications", + "_section_label": "Notifications"}, + ] + + +__all__ = ["ShellNotificationsPage"] diff --git a/cmds/tools/settings/ui/fan_curve_canvas.py b/cmds/tools/settings/ui/fan_curve_canvas.py index 8e668765..27336495 100644 --- a/cmds/tools/settings/ui/fan_curve_canvas.py +++ b/cmds/tools/settings/ui/fan_curve_canvas.py @@ -174,7 +174,7 @@ def _on_drag_begin(self, gesture, x, y) -> None: self._drag_origin_x = x self._drag_origin_y = y gesture.set_state(Gtk.EventSequenceState.CLAIMED) - self.set_cursor(*get_cursor_grab()) + self.set_cursor(get_cursor_grab()) else: gesture.set_state(Gtk.EventSequenceState.DENIED) @@ -199,7 +199,7 @@ def _on_drag_update(self, gesture, offset_x, offset_y) -> None: def _on_drag_end(self, _gesture, _x, _y) -> None: self._dragging = None - self.set_cursor(*get_cursor_none()) + self.set_cursor(None) # Reset to default cursor if self._drag_end_cb: self._drag_end_cb(self.points) @@ -209,6 +209,6 @@ def _on_motion(self, _ctrl, x, y) -> None: w, h = self.get_width(), self.get_height() idx = self._hit_test(x, y, w, h) if idx is not None: - self.set_cursor(*get_cursor_grab()) + self.set_cursor(get_cursor_grab()) else: - self.set_cursor(*get_cursor_none()) + self.set_cursor(None) # Reset to default cursor diff --git a/cmds/tools/settings/ui/fan_curve_editor.py b/cmds/tools/settings/ui/fan_curve_editor.py index d6fc7a78..bb1171e7 100644 --- a/cmds/tools/settings/ui/fan_curve_editor.py +++ b/cmds/tools/settings/ui/fan_curve_editor.py @@ -155,7 +155,7 @@ def _on_remove_point(self, _btn) -> None: points.pop() self._canvas.set_points(points) - def _on_user_curve_selected(self, _dd) -> None: + def _on_user_curve_selected(self, _dd, _pspec) -> None: # Read the selected curve name from the dropdown model, not from the entry model = self._user_curve_dd.get_model() if model is None: diff --git a/cmds/tools/settings/ui/icons.py b/cmds/tools/settings/ui/icons.py index daaec416..093834f9 100644 --- a/cmds/tools/settings/ui/icons.py +++ b/cmds/tools/settings/ui/icons.py @@ -50,9 +50,11 @@ POWER_ICON = "preferences-system-power-symbolic" BATTERY_ICON = "battery-symbolic" AUDIO_ICON = "audio-volume-high-symbolic" +NOTIFICATION_ICON = "feather-bell-symbolic" BLUETOOTH_ICON = "bluetooth-active-symbolic" NETWORK_ICON = "network-wireless-symbolic" DAEMON_ICON = "system-run-symbolic" +SERVICE_CONTROL_ICON = "utilities-system-monitor-symbolic" DRIVER_ICON = "preferences-other-symbolic" SLEEP_ICON = "weather-clear-night-symbolic" KEYRING_ICON = "dialog-password-symbolic" diff --git a/cmds/tools/settings/ui/sidebar.py b/cmds/tools/settings/ui/sidebar.py index ccf080bb..cd9dc694 100644 --- a/cmds/tools/settings/ui/sidebar.py +++ b/cmds/tools/settings/ui/sidebar.py @@ -8,6 +8,7 @@ from settings.ui.icons import ( ABOUT_ICON, + NOTIFICATION_ICON, APPS_ICON, AUDIO_ICON, AUTOSTART_ICON, @@ -41,6 +42,7 @@ POWER_ICON, PRESETS_ICON, QUICKSHARE_ICON, + SERVICE_CONTROL_ICON, SETTINGS_ICON, SHELL_THEME_ICON, SIDEBAR_ICON, @@ -260,6 +262,7 @@ def add_schema_row(listbox: Gtk.ListBox, group_id: str) -> None: add_row(shell, "shell_dock", "Dock", DOCK_ICON) add_row(shell, "shell_desktop", "Desktop", DESKTOP_ICON) add_row(shell, "shell_lock", "Lockscreen", LOCK_ICON) + add_row(shell, "shell_notifications", "Notifications", NOTIFICATION_ICON) add_row(shell, "shell_presets", "Presets", PRESETS_ICON) input_cat = new_category("Input") @@ -305,6 +308,7 @@ def add_schema_row(listbox: Gtk.ListBox, group_id: str) -> None: add_row(system, "grub", "Bootloader", GRUB_ICON) add_row(system, "driver", "Drivers", DRIVER_ICON) add_row(system, "daemon", "Daemon", DAEMON_ICON) + add_row(system, "service_control", "Services", SERVICE_CONTROL_ICON) add_row(system, "xdg", "Default Apps", APPS_ICON) add_row(system, "backups", "Backups", TIMESHIFT_ICON) diff --git a/cmds/tools/settings/window.py b/cmds/tools/settings/window.py index e6633f56..72e60f47 100644 --- a/cmds/tools/settings/window.py +++ b/cmds/tools/settings/window.py @@ -40,6 +40,7 @@ def _dbg(msg: str) -> None: from settings.pages.bluetooth import BluetoothPage from settings.pages.changelog import ChangelogPage from settings.pages.daemon import DaemonPage + from settings.pages.service_control import ServiceControlPage from settings.pages.shell_dashboard import ShellDashboardPage from settings.pages.disk import DiskPage from settings.pages.env_vars import EnvVarsPage @@ -61,6 +62,7 @@ def _dbg(msg: str) -> None: from settings.pages.shell_dock import ShellDockPage from settings.pages.shell_frame import ShellFramePage from settings.pages.shell_lock import ShellLockPage + from settings.pages.shell_notifications import ShellNotificationsPage from settings.pages.shell_notch import ShellNotchPage from settings.pages.shell_overview import ShellOverviewPage from settings.pages.shell_presets import ShellPresetsPage @@ -551,6 +553,7 @@ def _build_pages(self) -> tuple[list[dict], dict[str, dict]]: ("settings.pages.bluetooth", "BluetoothPage", "_bluetooth_page", "bluetooth", "Bluetooth"), ("settings.pages.network", "NetworkPage", "_network_page", "network", "Network"), ("settings.pages.daemon", "DaemonPage", "_daemon_page", "daemon", "Daemon"), + ("settings.pages.service_control", "ServiceControlPage", "_service_control_page", "service_control", "Services"), ("settings.pages.shell_dashboard", "ShellDashboardPage", "_shell_dashboard_page", "shell_dashboard", "Dashboard"), ("settings.pages.disk", "DiskPage", "_disk_page", "disks", "Disks"), ("settings.pages.driver", "DriverPage", "_driver_page", "driver", "Drivers"), @@ -569,6 +572,7 @@ def _build_pages(self) -> tuple[list[dict], dict[str, dict]]: ("settings.pages.shell_sidebar", "ShellSidebarPage", "_shell_sidebar_page", "shell_sidebar", "Sidebar"), ("settings.pages.shell_frame", "ShellFramePage", "_shell_frame_page", "shell_frame", "Frame"), ("settings.pages.shell_lock", "ShellLockPage", "_shell_lock_page", "shell_lock", "Lockscreen"), + ("settings.pages.shell_notifications", "ShellNotificationsPage", "_shell_notifications_page", "shell_notifications", "Notifications"), ("settings.pages.shell_workspaces", "ShellWorkspacesPage", "_shell_workspaces_page", "shell_workspaces", "Workspaces"), ("settings.pages.shell_overview", "ShellOverviewPage", "_shell_overview_page", "shell_overview", "Overview"), ("settings.pages.misc", "MiscPage", "_misc_page", "misc", "Miscellaneous"), @@ -991,6 +995,9 @@ def _update_sidebar_badges(self): if shell_frame_page is not None and shell_frame_page.is_dirty(): counts["shell_frame"] = 1 shell_lock_page = getattr(self, "_shell_lock_page", None) + shell_notifications_page = getattr(self, "_shell_notifications_page", None) + if shell_notifications_page is not None and shell_notifications_page.is_dirty(): + counts["shell_notifications"] = 1 if shell_lock_page is not None and shell_lock_page.is_dirty(): counts["shell_lock"] = 1 shell_workspaces_page = getattr(self, "_shell_workspaces_page", None) @@ -1354,6 +1361,10 @@ def _build_lazy_standalone_page(self, slug: str): page._on_dirty_changed = self._on_section_dirty # type: ignore[attr-defined] self._section_pages.append(page) # type: ignore[attr-defined] self._search_page_builder.add_entries(page.get_search_entries()) + elif cls_name == "ShellNotificationsPage": + page._on_dirty_changed = self._on_section_dirty # type: ignore[attr-defined] + self._section_pages.append(page) # type: ignore[attr-defined] + self._search_page_builder.add_entries(page.get_search_entries()) elif cls_name == "AboutPage": page._on_dirty_changed = self._on_section_dirty # type: ignore[attr-defined] self._section_pages.append(page) # type: ignore[attr-defined] @@ -1374,6 +1385,10 @@ def _build_lazy_standalone_page(self, slug: str): page._on_dirty_changed = self._on_section_dirty # type: ignore[attr-defined] self._section_pages.append(page) # type: ignore[attr-defined] self._search_page_builder.add_entries(page.get_search_entries()) + elif cls_name == "ServiceControlPage": + page._on_dirty_changed = self._on_section_dirty # type: ignore[attr-defined] + self._section_pages.append(page) # type: ignore[attr-defined] + self._search_page_builder.add_entries(page.get_search_entries()) elif cls_name == "XdgPage": page._on_dirty_changed = self._on_section_dirty # type: ignore[attr-defined] self._section_pages.append(page) # type: ignore[attr-defined] diff --git a/daemon/events/power.lua b/daemon/events/power.lua index 366f1666..d6b626d0 100644 --- a/daemon/events/power.lua +++ b/daemon/events/power.lua @@ -5,6 +5,12 @@ local Battery = require("battery") local Events = {} function Events.on_power_disconnect(cap) + -- Auto-disable caffeine when unplugging + if Watcher.get_var("PWR_CAFFEINE_ON_CHARGE") == "true" then + Watcher.set_var("HYPRIDLE_CAFFEINE_ENABLE", "false") + Watcher.log("power", "Caffeine disabled (unplugged)", "info") + end + if Watcher.get_var("BAT_SAVER_ON_PWR_DIS") == "true" and Watcher.get_var("BAT_SAVER_ACTIVE") ~= "true" then Power.set_profile("saver") end @@ -15,6 +21,12 @@ end function Events.on_power_connect(cap) Power.restore_previous() + -- Auto-enable caffeine when charging + if Watcher.get_var("PWR_CAFFEINE_ON_CHARGE") == "true" then + Watcher.set_var("HYPRIDLE_CAFFEINE_ENABLE", "true") + Watcher.log("power", "Caffeine enabled (charging)", "info") + end + if Watcher.get_var("BAT_SAVER_ON_PWR_DIS") == "true" and Watcher.get_var("BAT_SAVER_ACTIVE") == "true" then Battery.set_saver("false") end diff --git a/lib/module.sh b/lib/module.sh index 88a79668..d5e2109e 100755 --- a/lib/module.sh +++ b/lib/module.sh @@ -87,6 +87,28 @@ get_module_check() { rx_get_json "$json_file" "check" "" 2>/dev/null } +get_module_files() { + local name="$1" + local mod_path="$RETRO_DIR/modules/$name" + local json_file="$mod_path/properties.json" + + local raw + raw=$(rx_get_json "$json_file" "files" "" 2>/dev/null) + [[ -z $raw ]] && return 0 + echo "$raw" | jq -r '.[]? // empty' 2>/dev/null +} + +get_module_dependencies() { + local name="$1" + local mod_path="$RETRO_DIR/modules/$name" + local json_file="$mod_path/properties.json" + + local raw + raw=$(rx_get_json "$json_file" "dependencies" "" 2>/dev/null) + [[ -z $raw ]] && return 0 + echo "$raw" | jq -r '.[]? // empty' 2>/dev/null +} + get_module_uninstall_pkgs() { local name="$1" local mod_path="$RETRO_DIR/modules/$name" @@ -203,6 +225,20 @@ execute_logic() { [[ ! -d $mod_path ]] && rx_log "error" "Module '$name' not found." && return 1 + local dep + while IFS= read -r dep; do + [[ -z $dep ]] && continue + if [[ $type == "install" ]]; then + local dep_path="$RETRO_DIR/modules/$dep" + if [[ -d $dep_path ]]; then + if ! rx_module_status "$dep" >/dev/null 2>&1; then + rx_log "info" "Installing dependency: ${PINK}$dep${RESET}..." + execute_logic "install" "$dep" + fi + fi + fi + done <<< "$(get_module_dependencies "$name")" + local mod_access=$(get_module_access "$name") if [[ $mod_access == "root" && $EUID -ne 0 ]]; then rx_log "warn" "Module '${name}' requires root permissions, running with sudo..." @@ -295,18 +331,47 @@ rx_default_install() { local name="$1" IFS='|' read -r src dest <<<"$(get_module_paths "$name")" - [[ -d $src ]] && rx_link "$src" "$dest" + local tracked_files + tracked_files=$(get_module_files "$name") + + if [[ -n $tracked_files ]]; then + rx_log "info" "Linking specific tracked files for ${PINK}$name${RESET}..." + while IFS= read -r file; do + [[ -z $file ]] && continue + local repo_file="$src/$file" + local system_file="$dest/$file" + [[ -e $repo_file ]] && rx_link "$repo_file" "$system_file" + done <<< "$tracked_files" + else + [[ -d $src ]] && rx_link "$src" "$dest" + fi } rx_default_pull() { local name="$1" IFS='|' read -r src dest <<<"$(get_module_paths "$name")" - if [[ -d $dest ]]; then - if [[ -L $dest ]]; then - rx_log "success" "Module ${PINK}$name${RESET} is already linked, skipping" - else - rx_mirror_pull "$dest" "$src" + local tracked_files + tracked_files=$(get_module_files "$name") + + if [[ -n $tracked_files ]]; then + rx_log "info" "Pulling specific tracked files for ${PINK}$name${RESET}..." + while IFS= read -r file; do + [[ -z $file ]] && continue + local system_file="$dest/$file" + local repo_file="$src/$file" + if [[ -e $system_file ]]; then + mkdir -p "$(dirname "$repo_file")" + cp "$system_file" "$repo_file" + fi + done <<< "$tracked_files" + else + if [[ -d $dest ]]; then + if [[ -L $dest ]]; then + rx_log "success" "Module ${PINK}$name${RESET} is already linked, skipping" + else + rx_mirror_pull "$dest" "$src" + fi fi fi } diff --git a/modules/retro/files/variables.sh b/modules/retro/files/variables.sh index 521d8d85..ff9ccdb4 100755 --- a/modules/retro/files/variables.sh +++ b/modules/retro/files/variables.sh @@ -106,6 +106,7 @@ export HYPRIDLE_ENABLE="true" export HYPRIDLE_CAFFEINE_ENABLE="false" export CAFFEINE_UNTIL="0" export CAFFEINE_INITIAL="0" +export PWR_CAFFEINE_ON_CHARGE="false" export RETRO_FILEMANAGER_CMD="nemo" export PKG_HELPER="yay" export RETRO_THEME="retro" diff --git a/modules/retroshell/files/assets/sound/bright-chime.mp3 b/modules/retroshell/files/assets/sound/bright-chime.mp3 new file mode 100644 index 00000000..40d5df37 Binary files /dev/null and b/modules/retroshell/files/assets/sound/bright-chime.mp3 differ diff --git a/modules/retroshell/files/assets/sound/correct-answer.wav b/modules/retroshell/files/assets/sound/correct-answer.wav new file mode 100644 index 00000000..88a18e15 Binary files /dev/null and b/modules/retroshell/files/assets/sound/correct-answer.wav differ diff --git a/modules/retroshell/files/assets/sound/double-beep.wav b/modules/retroshell/files/assets/sound/double-beep.wav new file mode 100644 index 00000000..ef2645e7 Binary files /dev/null and b/modules/retroshell/files/assets/sound/double-beep.wav differ diff --git a/modules/retroshell/files/assets/sound/dragon-chime.mp3 b/modules/retroshell/files/assets/sound/dragon-chime.mp3 new file mode 100644 index 00000000..e6e23360 Binary files /dev/null and b/modules/retroshell/files/assets/sound/dragon-chime.mp3 differ diff --git a/modules/retroshell/files/assets/sound/dry-pop.wav b/modules/retroshell/files/assets/sound/dry-pop.wav new file mode 100644 index 00000000..2841ba53 Binary files /dev/null and b/modules/retroshell/files/assets/sound/dry-pop.wav differ diff --git a/modules/retroshell/files/assets/sound/gentle-chime.wav b/modules/retroshell/files/assets/sound/gentle-chime.wav new file mode 100644 index 00000000..aad0814d Binary files /dev/null and b/modules/retroshell/files/assets/sound/gentle-chime.wav differ diff --git a/modules/retroshell/files/assets/sound/long-pop.wav b/modules/retroshell/files/assets/sound/long-pop.wav new file mode 100644 index 00000000..4bed79ac Binary files /dev/null and b/modules/retroshell/files/assets/sound/long-pop.wav differ diff --git a/modules/retroshell/files/assets/sound/message-pop.mp3 b/modules/retroshell/files/assets/sound/message-pop.mp3 new file mode 100644 index 00000000..a1ad34ac Binary files /dev/null and b/modules/retroshell/files/assets/sound/message-pop.mp3 differ diff --git a/modules/retroshell/files/assets/sound/new-chime.mp3 b/modules/retroshell/files/assets/sound/new-chime.mp3 new file mode 100644 index 00000000..727b77bd Binary files /dev/null and b/modules/retroshell/files/assets/sound/new-chime.mp3 differ diff --git a/modules/retroshell/files/assets/sound/retro-default.mp3 b/modules/retroshell/files/assets/sound/retro-default.mp3 new file mode 100644 index 00000000..8bfa8747 Binary files /dev/null and b/modules/retroshell/files/assets/sound/retro-default.mp3 differ diff --git a/modules/retroshell/files/config/Config.qml b/modules/retroshell/files/config/Config.qml index 923e9d59..061e5f0c 100644 --- a/modules/retroshell/files/config/Config.qml +++ b/modules/retroshell/files/config/Config.qml @@ -23,6 +23,7 @@ import "defaults/tools.js" as ToolsDefaults import "defaults/dock.js" as DockDefaults import "defaults/ai.js" as AiDefaults import "defaults/dashboard.js" as DashboardDefaults +import "defaults/notifications.js" as NotificationsDefaults import "ConfigValidator.js" as ConfigValidator Singleton { @@ -57,11 +58,12 @@ Singleton { property bool systemReady: false property bool dockReady: false property bool dashboardReady: false + property bool notificationsReady: false property bool aiReady: false property bool toolsReady: false property bool keybindsInitialLoadComplete: false - property bool initialLoadComplete: themeReady && barReady && workspacesReady && overviewReady && notchReady && compositorReady && performanceReady && weatherReady && desktopReady && lockscreenReady && prefixReady && systemReady && dockReady && dashboardReady && aiReady && toolsReady + property bool initialLoadComplete: themeReady && barReady && workspacesReady && overviewReady && notchReady && compositorReady && performanceReady && weatherReady && desktopReady && lockscreenReady && prefixReady && systemReady && dockReady && dashboardReady && notificationsReady && aiReady && toolsReady // Compatibility aliases property alias loader: themeLoader @@ -1261,6 +1263,47 @@ Singleton { } } + // ============================================ + // NOTIFICATIONS MODULE + // ============================================ + FileView { + id: notificationsLoader + path: root.configDir + "/notifications.json" + atomicWrites: true + watchChanges: true + onLoaded: { + if (!root.notificationsReady) { + validateModule("notifications", notificationsLoader, NotificationsDefaults.data, () => { + root.notificationsReady = true; + }); + } + } + onLoadFailed: { + if (error.toString().includes("FileNotFound") && !root.notificationsReady) { + handleMissingConfig("notifications", notificationsLoader, NotificationsDefaults.data, () => { + root.notificationsReady = true; + }); + } + } + onFileChanged: { + root.pauseAutoSave = true; + reload(); + root.pauseAutoSave = false; + } + onPathChanged: reload() + onAdapterUpdated: { + if (root.notificationsReady && !root.pauseAutoSave) { + notificationsLoader.writeAdapter(); + } + } + + adapter: JsonAdapter { + property bool soundEnabled: true + property string soundFile: "retro-default.mp3" + property int soundVolume: 40 + } + } + // ============================================ // AI MODULE // ============================================ @@ -3583,6 +3626,9 @@ Singleton { // Dashboard configuration property QtObject dashboard: dashboardLoader.adapter + // Notifications configuration + property QtObject notifications: notificationsLoader.adapter + // Pinned apps configuration (stored in dataPath) property QtObject pinnedApps: pinnedAppsLoader.adapter @@ -3626,6 +3672,9 @@ Singleton { function saveDock() { dockLoader.writeAdapter(); } + function saveNotifications() { + notificationsLoader.writeAdapter(); + } function savePinnedApps() { pinnedAppsLoader.writeAdapter(); } diff --git a/modules/retroshell/files/config/defaults/notifications.js b/modules/retroshell/files/config/defaults/notifications.js new file mode 100644 index 00000000..d07caa97 --- /dev/null +++ b/modules/retroshell/files/config/defaults/notifications.js @@ -0,0 +1,7 @@ +.pragma library + +var data = { + "soundEnabled": true, + "soundFile": "retro-default.mp3", + "soundVolume": 40 +} diff --git a/modules/retroshell/files/modules/globals/GlobalStates.qml b/modules/retroshell/files/modules/globals/GlobalStates.qml index ea0176e4..7880d36a 100644 --- a/modules/retroshell/files/modules/globals/GlobalStates.qml +++ b/modules/retroshell/files/modules/globals/GlobalStates.qml @@ -12,6 +12,29 @@ Singleton { property var wallpaperManager: null property string avatarCacheBuster: "" + // Theme color mode ("dark"/"light") — single source of truth for the shell + property string themeMode: "dark" + + function setThemeMode(mode) { + if (mode !== "dark" && mode !== "light") + return; + if (root.themeMode === mode) + return; + root.themeMode = mode; + var rd = Quickshell.env("RETRO_DIR"); + if (!rd) + return; + var proc = Qt.createQmlObject(' + import Quickshell + import Quickshell.Io + Process { + running: true + command: ["bash", "' + rd + '/scripts/theme_core.sh", "--mode", "' + mode + '"] + onExited: function () { destroy(); } + } + ', root); + } + function pickUserAvatar() { filePickerProcess.running = true; } @@ -95,12 +118,36 @@ Singleton { setCompositorLayout(availableLayouts[nextIndex]); } + // Read the persisted theme mode at startup so the shell stays in sync + function loadInitialThemeMode() { + var cfg = Quickshell.env("RETRO_CONFIG") || Quickshell.env("HOME") + "/.config/retro"; + var proc = Qt.createQmlObject(' + import Quickshell + import Quickshell.Io + Process { + running: false + stdout: StdioCollector { + onStreamFinished: { + var val = text.trim(); + if (val === "dark" || val === "light") { + root.themeMode = val; + } + destroy(); + } + } + } + ', root); + proc.command = ["bash", "-c", "source '" + cfg + "/variables.sh' 2>/dev/null; echo $RETRO_THEME_MODE"]; + proc.running = true; + } + // Ensure LockscreenService singleton is loaded Component.onCompleted: { LockscreenService.toString(); getLayoutProcess.running = true; screenshotTimedMode = Config.tools.screenshotTimerEnabled; + root.loadInitialThemeMode(); } // Persistent launcher state across monitors diff --git a/modules/retroshell/files/modules/services/GlobalShortcuts.qml b/modules/retroshell/files/modules/services/GlobalShortcuts.qml index 30360733..d7039dd9 100644 --- a/modules/retroshell/files/modules/services/GlobalShortcuts.qml +++ b/modules/retroshell/files/modules/services/GlobalShortcuts.qml @@ -48,6 +48,14 @@ QtObject { return; } + if (command.indexOf("theme-mode ") === 0) { + var mode = command.substring("theme-mode ".length).trim(); + if (mode === "dark" || mode === "light") { + GlobalStates.setThemeMode(mode); + } + return; + } + switch (command) { // Launcher (Standalone Notch Module) case "launcher": toggleLauncher(); break; diff --git a/modules/retroshell/files/modules/services/Notifications.qml b/modules/retroshell/files/modules/services/Notifications.qml index a5e2714b..c7c63c7b 100644 --- a/modules/retroshell/files/modules/services/Notifications.qml +++ b/modules/retroshell/files/modules/services/Notifications.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Notifications +import qs.config Singleton { id: root @@ -141,7 +142,21 @@ Singleton { property var popupList: list.filter(notif => notif.popup) property bool popupInhibited: silent property var latestTimeForApp: ({}) - property var totalCounts: ({}) // Conteo total independiente del almacenamiento: {appName: {summary: count}} + property var totalCounts: ({}) + + function playNotifSound() { + if (!Config.notifications?.soundEnabled || root.silent) return; + const file = Config.notifications?.soundFile ?? "retro-default.mp3"; + const vol = Config.notifications?.soundVolume ?? 40; + const paVol = Math.round(vol * 65536 / 100); + const path = Quickshell.shellDir + "/assets/sound/" + file; + notifSoundProc.command = ["paplay", "--volume", String(paVol), path]; + notifSoundProc.running = true; + } + + Process { + id: notifSoundProc + } Component { id: notifComponent @@ -340,6 +355,8 @@ Singleton { } root.notify(newNotifObject); + + try { root.playNotifSound(); } catch(e) { console.log("NotificationSound error:", e); } } } diff --git a/modules/retroshell/files/modules/widgets/dashboard/widgets/QuickControls.qml b/modules/retroshell/files/modules/widgets/dashboard/widgets/QuickControls.qml index 032ce1cb..fcf38ffb 100644 --- a/modules/retroshell/files/modules/widgets/dashboard/widgets/QuickControls.qml +++ b/modules/retroshell/files/modules/widgets/dashboard/widgets/QuickControls.qml @@ -6,6 +6,7 @@ import Quickshell.Io import qs.modules.theme import qs.modules.components import qs.modules.services +import qs.modules.globals import qs.config import "../controls" @@ -18,8 +19,7 @@ StyledRect { radius: Styling.radius(4) property int expandedPanel: -1 // -1: none, 0: wifi, 1: bluetooth, 2: quickshare, 3: caffeine - property bool darkMode: true - property bool dlLocked: false + property bool darkMode: GlobalStates.themeMode === "dark" property var controlOrder: (Config.dashboard && Config.dashboard.controlOrder) ? Config.dashboard.controlOrder @@ -37,21 +37,11 @@ StyledRect { return undefined; } - Process { id: dlProc; running: false; stdout: SplitParser {} } - Timer { id: dlUnlock; interval: 3000; repeat: false; onTriggered: dlLocked = false } - - Process { id: modeReadProc; running: false - stdout: StdioCollector { - onStreamFinished: { darkMode = text.trim() !== "light"; } - } + function toggleThemeMode() { + var newMode = root.darkMode ? "light" : "dark"; + GlobalStates.setThemeMode(newMode); } - Component.onCompleted: { - var cfg = Quickshell.env("RETRO_CONFIG") || Quickshell.env("HOME") + "/.config/retro"; - modeReadProc.command = ["bash", "-c", "source '" + cfg + "/variables.sh' 2>/dev/null; echo $RETRO_THEME_MODE"]; - modeReadProc.running = true; - } - onVisibleChanged: { if (!visible) { root.expandedPanel = -1; @@ -372,13 +362,7 @@ StyledRect { isActive: root.darkMode tooltipText: root.darkMode ? "Dark Mode" : "Light Mode" onClicked: { - if (root.dlLocked) return; - root.dlLocked = true; - root.darkMode = !root.darkMode; - var rd = Quickshell.env("RETRO_DIR"); - dlProc.command = ["bash", rd + "/scripts/theme_core.sh", "--mode", root.darkMode ? "dark" : "light"]; - dlProc.running = true; - root.dlUnlock.start(); + root.toggleThemeMode(); } } } diff --git a/modules/retroshell/packages.sh b/modules/retroshell/packages.sh index 49f03a17..5585c59d 100755 --- a/modules/retroshell/packages.sh +++ b/modules/retroshell/packages.sh @@ -7,5 +7,6 @@ ttf-phosphor-icons ttf-league-gothic unzip curl brightnessctl networkmanager syntax-highlighting wl-clipboard slurp hyprpicker upower gpu-screen-recorder +pulseaudio-utils mpg123 go diff --git a/modules/spotify/install.sh b/modules/spotify/install.sh index 7102963c..30e5f828 100755 --- a/modules/spotify/install.sh +++ b/modules/spotify/install.sh @@ -1,51 +1,51 @@ #!/bin/bash -if [[ ! -d /opt/spotify ]]; then - rx_log "error" "Spotify not found at /opt/spotify" - return 1 -fi - -spicetify_config="${XDG_CONFIG_HOME:-$HOME/.config}/spicetify" -if [[ -d $spicetify_config ]]; then - rx_log "info" "Spicetify already configured — skipping Spotify launch and re-initialization" - return 0 -fi - -rx_log "info" "Launching Spotify — please log in to generate your profile..." -rx_log "info" "Waiting for ~/.config/spotify/prefs to appear..." - -spotify & - -local attempts=0 -while [[ ! -f "$HOME/.config/spotify/prefs" && $attempts -lt 30 ]]; do - sleep 2 - ((attempts++)) -done - -if [[ -f "$HOME/.config/spotify/prefs" ]]; then - rx_log "success" "Spotify preferences detected" -else - rx_log "warn" "Spotify preferences not detected after $((attempts * 2))s — continuing anyway" -fi - -rx_log "info" "Granting write permissions to Spotify directory..." -sudo chmod a+wr /opt/spotify -sudo chmod a+wr /opt/spotify/Apps -R -rx_log "success" "Spotify directory permissions set" - -rx_log "info" "Patching Spotify desktop entry..." -local desktop_file="/usr/share/applications/spotify.desktop" -if ! grep -q "^Comment=" "$desktop_file" 2>/dev/null; then - sudo sed -i '/^Icon=/a Comment=Stream music and podcasts on Spotify' "$desktop_file" - rx_log "success" "Desktop entry updated with description" -else - rx_log "info" "Desktop entry already has a description" -fi - -rx_log "info" "Initializing Spicetify..." -spicetify apply -n -rx_log "success" "Spicetify initialized" - -rx_log "info" "Installing Spicetify Marketplace..." -curl -fsSL https://raw.githubusercontent.com/spicetify/marketplace/main/resources/install.sh | sh -rx_log "success" "Spicetify Marketplace installed" +source "$RETRO_DIR/lib/log.sh" + +install_spotify() { + if [[ ! -d /opt/spotify ]]; then + rx_log "error" "Spotify not found at /opt/spotify" + return 1 + fi + + rx_log "info" "Launching Spotify — please log in to generate your profile..." + rx_log "info" "Waiting for ~/.config/spotify/prefs to appear..." + + spotify & + + local attempts=0 + while [[ ! -f "$HOME/.config/spotify/prefs" && $attempts -lt 30 ]]; do + sleep 2 + ((attempts++)) + done + + if [[ -f "$HOME/.config/spotify/prefs" ]]; then + rx_log "success" "Spotify preferences detected" + else + rx_log "warn" "Spotify preferences not detected after $((attempts * 2))s — continuing anyway" + fi + + rx_log "info" "Granting write permissions to Spotify directory..." + sudo chmod a+wr /opt/spotify + sudo chmod a+wr /opt/spotify/Apps -R + rx_log "success" "Spotify directory permissions set" + + rx_log "info" "Patching Spotify desktop entry..." + local desktop_file="/usr/share/applications/spotify.desktop" + if ! grep -q "^Comment=" "$desktop_file" 2>/dev/null; then + sudo sed -i '/^Icon=/a Comment=Stream music and podcasts on Spotify' "$desktop_file" + rx_log "success" "Desktop entry updated with description" + else + rx_log "info" "Desktop entry already has a description" + fi + + rx_log "info" "Initializing Spicetify..." + spicetify backup apply -n + rx_log "success" "Spicetify initialized" + + rx_log "info" "Installing Spicetify Marketplace..." + curl -fsSL https://raw.githubusercontent.com/spicetify/marketplace/main/resources/install.sh | sh + rx_log "success" "Spicetify Marketplace installed" +} + +install_spotify diff --git a/modules/spotify/properties.json b/modules/spotify/properties.json index a119d8a6..74a1c5f3 100644 --- a/modules/spotify/properties.json +++ b/modules/spotify/properties.json @@ -5,8 +5,5 @@ "access": "user", "defaults": true, "mode": "mirror", - "config": "./files", - "install": "~/.config/spicetify", - "overwrite": false, "gui": true } diff --git a/modules/tmux/files/scripts/battery.sh b/modules/tmux/files/scripts/battery.sh new file mode 100755 index 00000000..6aaf2bdf --- /dev/null +++ b/modules/tmux/files/scripts/battery.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +if sys_bat=$(ls -d /sys/class/power_supply/BAT* 2>/dev/null | head -n 1) && [ -n "$sys_bat" ]; then + capacity=$(cat "$sys_bat/capacity" 2>/dev/null) + status=$(cat "$sys_bat/status" 2>/dev/null) + + if [ -n "$capacity" ]; then + [ "$status" = "Charging" ] && icon="󰂄" || icon="󰁹" + echo "#[bg=brightblack,fg=green] ${icon} ${capacity}% #[bg=default] " + fi +fi diff --git a/modules/tmux/files/scripts/cpu.sh b/modules/tmux/files/scripts/cpu.sh new file mode 100755 index 00000000..34c515e6 --- /dev/null +++ b/modules/tmux/files/scripts/cpu.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +read_cpu() { + awk '/^cpu / {print $2+$3+$4+$5+$6+$7+$8, $5}' /proc/stat +} + +read -r total1 idle1 < <(read_cpu) +sleep 0.5 +read -r total2 idle2 < <(read_cpu) + +diff_idle=$((idle2 - idle1)) +diff_total=$((total2 - total1)) + +[[ $diff_total -eq 0 ]] && exit 0 + +usage=$(((diff_total - diff_idle) * 100 / diff_total)) +echo "#[bg=brightblack,fg=yellow] ${usage}% #[bg=default] " diff --git a/modules/tmux/files/scripts/num_to_icon.sh b/modules/tmux/files/scripts/num_to_icon.sh new file mode 100755 index 00000000..40d8255d --- /dev/null +++ b/modules/tmux/files/scripts/num_to_icon.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash + +category="$1" +num="$2" + +case "$category" in + win_active) + case "$num" in + 1) echo "󰼏" ;; + 2) echo "󰼐" ;; + 3) echo "󰼑" ;; + 4) echo "󰼒" ;; + 5) echo "󰼓" ;; + 6) echo "󰼔" ;; + 7) echo "󰼕" ;; + 8) echo "󰼖" ;; + 9) echo "󰼗" ;; + 10) echo "󰿪" ;; + *) echo "$num" ;; + esac + ;; + win_inactive) + case "$num" in + 1) echo "󰎥" ;; + 2) echo "󰎨" ;; + 3) echo "󰎫" ;; + 4) echo "󰎲" ;; + 5) echo "󰎯" ;; + 6) echo "󰎴" ;; + 7) echo "󰎷" ;; + 8) echo "󰎺" ;; + 9) echo "󰎽" ;; + 10) echo "󰿫" ;; + *) echo "$num" ;; + esac + ;; + pane_active) + case "$num" in + 1) echo "󰎤" ;; + 2) echo "󰎧" ;; + 3) echo "󰎪" ;; + 4) echo "󰎭" ;; + 5) echo "󰎱" ;; + 6) echo "󰎳" ;; + 7) echo "󰎶" ;; + 8) echo "󰎹" ;; + 9) echo "󰎼" ;; + 10) echo "󰽽" ;; + *) echo "$num" ;; + esac + ;; + pane_inactive) + case "$num" in + 1) echo "󰎦" ;; + 2) echo "󰎩" ;; + 3) echo "󰎬" ;; + 4) echo "󰎮" ;; + 5) echo "󰎰" ;; + 6) echo "󰎵" ;; + 7) echo "󰎸" ;; + 8) echo "󰎻" ;; + 9) echo "󰎾" ;; + 10) echo "󰽾" ;; + *) echo "$num" ;; + esac + ;; + *) + echo "$num" + ;; +esac diff --git a/modules/tmux/files/tmux.conf b/modules/tmux/files/tmux.conf new file mode 100644 index 00000000..3acab95d --- /dev/null +++ b/modules/tmux/files/tmux.conf @@ -0,0 +1,126 @@ +# Style +set-option -g default-terminal 'screen-256color' +set-option -g terminal-overrides ',xterm-256color:RGB' + +set -g pane-active-border-style 'fg=magenta,bg=default' +set -g pane-border-style 'fg=brightblack,bg=default' + +# Settings +set -g detach-on-destroy off # don't exit from tmux when closing a session +set -g escape-time 0 # zero-out escape time delay +set -g history-limit 1000000 # increase history size (from 2,000) +set -g set-clipboard on # use system clipboard +set -g status-position top # macOS / darwin style +set -g default-terminal "${TERM}" + +# Index +set -g base-index 1 +set -g pane-base-index 1 +set-window-option -g pane-base-index 1 +set-option -g renumber-windows on + +# Prefix +unbind C-b +set -g prefix C-x +bind C-x send-prefix + +# Keybinds +set-window-option -g mode-keys vi + +unbind r +bind r source-file ~/.config/tmux/tmux.conf + +bind-key x kill-pane +bind-key & kill-window + +bind-key -T copy-mode-vi v send-keys -X begin-selection +bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle +bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel + +bind -n M-H previous-window +bind -n M-L next-window + +bind 'v' split-window -v -c "#{pane_current_path}" +bind h split-window -h -c "#{pane_current_path}" + +# Plugins +set -g @plugin 'tmux-plugins/tpm' +set -g @plugin 'sainnhe/tmux-fzf' +set -g @plugin 'wfxr/tmux-fzf-url' +set -g @plugin 'omerxx/tmux-floax' +set -g @plugin 'omerxx/tmux-sessionx' +set -g @plugin 'fcsonline/tmux-thumbs' +set -g @plugin 'tmux-plugins/tmux-yank' +set -g @plugin 'tmux-plugins/tmux-sensible' +set -g @plugin 'tmux-plugins/tmux-resurrect' +set -g @plugin 'tmux-plugins/tmux-continuum' +set -g @plugin 'alexwforsythe/tmux-which-key' +set -g @plugin 'christoomey/vim-tmux-navigator' + +set -g @floax-width '80%' +set -g @floax-height '80%' +set -g @floax-border-color 'magenta' +set -g @floax-text-color 'blue' +set -g @floax-bind 'p' +set -g @floax-change-path 'true' + +set -g @sessionx-bind-zo-new-window 'ctrl-y' +set -g @sessionx-auto-accept 'off' +set -g @sessionx-custom-paths '~/.config' +set -g @sessionx-bind 'o' +set -g @sessionx-x-path '~/.config' +set -g @sessionx-window-height '85%' +set -g @sessionx-window-width '75%' +set -g @sessionx-zoxide-mode 'on' +set -g @sessionx-custom-paths-subdirectories 'false' +set -g @sessionx-filter-current 'false' + +set -g @continuum-restore 'on' +set -g @continuum-save-interval '5' +set -g @resurrect-strategy-nvim 'session' + +# FZF Integration +set -g @fzf-url-fzf-options '-p 60%,30% --prompt=" " --border-label=" Open URL "' +set -g @fzf-url-history-limit '2000' + +# Custom menu +bind m display-menu -T "#[fg=blue,bold] 󱐋 Quick Actions " -x C -y C \ + "#[fg=green]󰐕 New Window" n "new-window -c '#{pane_current_path}'" \ + "#[fg=green]󰓩 Split Right" h "split-window -h -c '#{pane_current_path}'" \ + "#[fg=green]󰓩 Split Down" v "split-window -v -c '#{pane_current_path}'" \ + "#[fg=brightblack]--------------------" "" "" \ + "#[fg=yellow]󰁌 Toggle Pane Zoom" z "resize-pane -Z" \ + "#[fg=yellow]󰓦 Toggle Pane Sync" y "set-option synchronize-panes" \ + "#[fg=yellow]󰐃 Swap with Master" s "swap-pane -U" \ + "#[fg=brightblack]--------------------" "" "" \ + "#[fg=cyan]󰉋 Rename Window" r "command-prompt -I '#W' 'rename-window %%'" \ + "#[fg=cyan]󰑐 Reload Config" R "source-file ~/.config/tmux/tmux.conf; display 'Config Reloaded!'" \ + "#[fg=brightblack]--------------------" "" "" \ + "#[fg=red]󰆴 Kill Current Pane" x "kill-pane" \ + "#[fg=red]󰆴 Kill Current Window" k "kill-window" + +# Status bar base style +set -g monitor-activity on +set -g visual-activity off +set -g status-style "bg=default,fg=default" +set -w -g window-status-style "bg=default,fg=default" +set -w -g window-status-current-style "bg=default,fg=default" +set -w -g window-status-activity-style "none" +set -w -g window-status-bell-style "none" + +set -g status-position top +set -g status-justify left +set -g status-interval 5 + +set -g status-left-length 50 +set -g status-left "#[bg=blue,fg=black,bold]  #S #[bg=default,fg=default] " + +set -w -g window-status-separator " " +set -w -g window-status-format "#[bg=default,fg=brightblack]  #(~/.config/tmux/scripts/num_to_icon.sh win_inactive #I) #W #(~/.config/tmux/scripts/num_to_icon.sh pane_inactive #P)#{?window_zoomed_flag, #[fg=yellow]󱉶,}#{?window_activity_flag, #[fg=yellow]󰑐,} " +set -w -g window-status-current-format "#[bg=brightblack,fg=green]  #(~/.config/tmux/scripts/num_to_icon.sh win_active #I) #[fg=white]#W #[fg=white]#(~/.config/tmux/scripts/num_to_icon.sh pane_active #P)#{?window_zoomed_flag, #[fg=yellow]󱉶,} #[bg=default]" + +set -g status-right-length 150 +set -g status-right "#{?SSH_CLIENT,#[bg=red,fg=white,bold] 󰒋 #H #[bg=default] ,}#[bg=brightblack,fg=yellow] #(~/.config/tmux/scripts/cpu.sh)#(~/.config/tmux/scripts/battery.sh)#[bg=brightblack,fg=blue] 󰉋 #[fg=white]#{b:pane_current_path} #[bg=default] #[bg=brightblack,fg=cyan] 󰥔 #[fg=white]%H:%M #[bg=default]" + +# Initialize TMUX plugin manager (keep this line at the very bottom of tmux.conf) +run '~/.tmux/plugins/tpm/tpm' diff --git a/modules/tmux/install.sh b/modules/tmux/install.sh new file mode 100755 index 00000000..cd9172c8 --- /dev/null +++ b/modules/tmux/install.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +source "$RETRO_DIR/lib/log.sh" + +install_tmux() { + local tpm_dir="${XDG_CONFIG_HOME:-$HOME/.config}/tmux/plugins/tpm" + + if [[ -d $tpm_dir ]]; then + rx_log "info" "tpm already installed — skipping clone" + else + rx_log "info" "Cloning tpm (tmux plugin manager)..." + git clone https://github.com/tmux-plugins/tpm "$tpm_dir" + rx_log "success" "tpm installed" + fi + + rx_log "info" "Installing tmux plugins..." + "$tpm_dir/bin/install_plugins" + rx_log "success" "tmux plugins installed" +} + +install_tmux diff --git a/modules/tmux/packages.sh b/modules/tmux/packages.sh new file mode 100644 index 00000000..e9db5870 --- /dev/null +++ b/modules/tmux/packages.sh @@ -0,0 +1,4 @@ +git +fzf +tmux +zoxide diff --git a/modules/tmux/properties.json b/modules/tmux/properties.json new file mode 100644 index 00000000..37e6dd79 --- /dev/null +++ b/modules/tmux/properties.json @@ -0,0 +1,13 @@ +{ + "title": "Tmux", + "description": "Tmux configuration with tpm, neovim integration, and productivity plugins", + "type": "extra", + "access": "user", + "defaults": true, + "mode": "install", + "config": "./files", + "install": "~/.config/tmux", + "overwrite": true, + "check": "tmux", + "files": ["tmux.conf", "scripts/battery.sh", "scripts/cpu.sh", "scripts/num_to_icon.sh"] +} diff --git a/modules/zsh/files/.p10k.zsh b/modules/zsh/files/.p10k.zsh new file mode 100644 index 00000000..8a02b7fe --- /dev/null +++ b/modules/zsh/files/.p10k.zsh @@ -0,0 +1,1713 @@ +# Generated by Powerlevel10k configuration wizard on 2026-08-23 at 12:31 EEST. +# Based on romkatv/powerlevel10k/config/p10k-lean-8colors.zsh, checksum 23564. +# Wizard options: nerdfont-v3 + powerline, small icons, unicode, lean_8colors, 24h time, +# 1 line, sparse, many icons, fluent, transient_prompt, instant_prompt=verbose. +# Type `p10k configure` to generate another config. +# +# Config for Powerlevel10k with 8-color lean prompt style. Type `p10k configure` to generate +# your own config based on it. +# +# Tip: Looking for a nice color? Here's a one-liner to print colormap. +# +# for i in {0..255}; do print -Pn "%K{$i} %k%F{$i}${(l:3::0:)i}%f " ${${(M)$((i%6)):#3}:+$'\n'}; done + +# Temporarily change options. +'builtin' 'local' '-a' 'p10k_config_opts' +[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') +[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') +[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') +'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' + +() { + emulate -L zsh -o extended_glob + + # Unset all configuration options. This allows you to apply configuration changes without + # restarting zsh. Edit ~/.p10k.zsh and type `source ~/.p10k.zsh`. + unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR' + + # Zsh >= 5.1 is required. + [[ $ZSH_VERSION == (5.<1->*|<6->.*) ]] || return + + # The list of segments shown on the left. Fill it with the most important segments. + typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( + #os_icon # os identifier + dir # current directory + vcs # git status + prompt_char # prompt symbol + ) + + # The list of segments shown on the right. Fill it with less important segments. + # Right prompt on the last prompt line (where you are typing your commands) gets + # automatically hidden when the input line reaches it. Right prompt above the + # last prompt line gets hidden if it would overlap with left prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( + status # exit code of the last command + command_execution_time # duration of the last command + background_jobs # presence of background jobs + direnv # direnv status (https://direnv.net/) + asdf # asdf version manager (https://github.com/asdf-vm/asdf) + virtualenv # python virtual environment (https://docs.python.org/3/library/venv.html) + anaconda # conda environment (https://conda.io/) + pyenv # python environment (https://github.com/pyenv/pyenv) + goenv # go environment (https://github.com/syndbg/goenv) + nodenv # node.js version from nodenv (https://github.com/nodenv/nodenv) + nvm # node.js version from nvm (https://github.com/nvm-sh/nvm) + nodeenv # node.js environment (https://github.com/ekalinin/nodeenv) + node_version # node.js version + go_version # go version (https://golang.org) + rust_version # rustc version (https://www.rust-lang.org) + dotnet_version # .NET version (https://dotnet.microsoft.com) + php_version # php version (https://www.php.net/) + laravel_version # laravel php framework version (https://laravel.com/) + java_version # java version (https://www.java.com/) + package # name@version from package.json (https://docs.npmjs.com/files/package.json) + rbenv # ruby version from rbenv (https://github.com/rbenv/rbenv) + rvm # ruby version from rvm (https://rvm.io) + fvm # flutter version management (https://github.com/leoafarias/fvm) + luaenv # lua version from luaenv (https://github.com/cehoffman/luaenv) + jenv # java version from jenv (https://github.com/jenv/jenv) + plenv # perl version from plenv (https://github.com/tokuhirom/plenv) + perlbrew # perl version from perlbrew (https://github.com/gugod/App-perlbrew) + phpenv # php version from phpenv (https://github.com/phpenv/phpenv) + scalaenv # scala version from scalaenv (https://github.com/scalaenv/scalaenv) + haskell_stack # haskell version from stack (https://haskellstack.org/) + kubecontext # current kubernetes context (https://kubernetes.io/) + terraform # terraform workspace (https://www.terraform.io) + # terraform_version # terraform version (https://www.terraform.io) + aws # aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) + aws_eb_env # aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) + azure # azure account name (https://docs.microsoft.com/en-us/cli/azure) + gcloud # google cloud cli account and project (https://cloud.google.com/) + google_app_cred # google application credentials (https://cloud.google.com/docs/authentication/production) + toolbox # toolbox name (https://github.com/containers/toolbox) + context # user@hostname + nordvpn # nordvpn connection status, linux only (https://nordvpn.com/) + ranger # ranger shell (https://github.com/ranger/ranger) + yazi # yazi shell (https://github.com/sxyazi/yazi) + nnn # nnn shell (https://github.com/jarun/nnn) + lf # lf shell (https://github.com/gokcehan/lf) + xplr # xplr shell (https://github.com/sayanarijit/xplr) + vim_shell # vim shell indicator (:sh) + midnight_commander # midnight commander shell (https://midnight-commander.org/) + nix_shell # nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) + chezmoi_shell # chezmoi shell (https://www.chezmoi.io/) + # vpn_ip # virtual private network indicator + # load # CPU load + # disk_usage # disk usage + # ram # free RAM + # swap # used swap + todo # todo items (https://github.com/todotxt/todo.txt-cli) + timewarrior # timewarrior tracking status (https://timewarrior.net/) + taskwarrior # taskwarrior task count (https://taskwarrior.org/) + per_directory_history # Oh My Zsh per-directory-history local/global indicator + # cpu_arch # CPU architecture + time # current time + # ip # ip address and bandwidth usage for a specified network interface + # public_ip # public IP address + # proxy # system-wide http/https/ftp proxy + # battery # internal battery + # wifi # wifi speed + # example # example user-defined segment (see prompt_example function below) + ) + + # Defines character set used by powerlevel10k. It's best to let `p10k configure` set it for you. + typeset -g POWERLEVEL9K_MODE=nerdfont-v3 + # When set to `moderate`, some icons will have an extra space after them. This is meant to avoid + # icon overlap when using non-monospace fonts. When set to `none`, spaces are not added. + typeset -g POWERLEVEL9K_ICON_PADDING=none + + # Basic style options that define the overall look of your prompt. You probably don't want to + # change them. + typeset -g POWERLEVEL9K_BACKGROUND= # transparent background + typeset -g POWERLEVEL9K_{LEFT,RIGHT}_{LEFT,RIGHT}_WHITESPACE= # no surrounding whitespace + typeset -g POWERLEVEL9K_{LEFT,RIGHT}_SUBSEGMENT_SEPARATOR=' ' # separate segments with a space + typeset -g POWERLEVEL9K_{LEFT,RIGHT}_SEGMENT_SEPARATOR= # no end-of-line symbol + + # When set to true, icons appear before content on both sides of the prompt. When set + # to false, icons go after content. If empty or not set, icons go before content in the left + # prompt and after content in the right prompt. + # + # You can also override it for a specific segment: + # + # POWERLEVEL9K_STATUS_ICON_BEFORE_CONTENT=false + # + # Or for a specific segment in specific state: + # + # POWERLEVEL9K_DIR_NOT_WRITABLE_ICON_BEFORE_CONTENT=false + typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT=true + + # Add an empty line before each prompt. + typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true + + # Connect left prompt lines with these symbols. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX= + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX= + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX= + # Connect right prompt lines with these symbols. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX= + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX= + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX= + + # The left end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= + # The right end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL= + + # Ruler, a.k.a. the horizontal line before each prompt. If you set it to true, you'll + # probably want to set POWERLEVEL9K_PROMPT_ADD_NEWLINE=false above and + # POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' ' below. + typeset -g POWERLEVEL9K_SHOW_RULER=false + typeset -g POWERLEVEL9K_RULER_CHAR='─' # reasonable alternative: '·' + typeset -g POWERLEVEL9K_RULER_FOREGROUND=7 + + # Filler between left and right prompt on the first prompt line. You can set it to '·' or '─' + # to make it easier to see the alignment between left and right prompt and to separate prompt + # from command output. It serves the same purpose as ruler (see above) without increasing + # the number of prompt lines. You'll probably want to set POWERLEVEL9K_SHOW_RULER=false + # if using this. You might also like POWERLEVEL9K_PROMPT_ADD_NEWLINE=false for more compact + # prompt. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' ' + if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then + # The color of the filler. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=7 + # Add a space between the end of left prompt and the filler. + typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL=' ' + # Add a space between the filler and the start of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL=' ' + # Start filler from the edge of the screen if there are no left segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}' + # End filler on the edge of the screen if there are no right segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}' + fi + + #################################[ os_icon: os identifier ]################################## + # OS identifier color. + # typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND= + # Custom icon. + # typeset -g POWERLEVEL9K_OS_ICON_CONTENT_EXPANSION='⭐' + + ################################[ prompt_char: prompt symbol ]################################ + # Green prompt symbol if the last command succeeded. + typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=2 + # Red prompt symbol if the last command failed. + typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=1 + # Default prompt symbol. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' + # Prompt symbol in command vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' + # Prompt symbol in visual vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V' + # Prompt symbol in overwrite vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' + typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true + # No line terminator if prompt_char is the last segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='' + # No line introducer if prompt_char is the first segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= + + ##################################[ dir: current directory ]################################## + # Default current directory color. + typeset -g POWERLEVEL9K_DIR_FOREGROUND=4 + # If directory is too long, shorten some of its segments to the shortest possible unique + # prefix. The shortened directory can be tab-completed to the original. + typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique + # Replace removed segment suffixes with this symbol. + typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= + # Color of the shortened directory segments. + typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=4 + # Color of the anchor directory segments. Anchor segments are never shortened. The first + # segment is always an anchor. + typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=4 + # Set to true to display anchor directory segments in bold. + typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=false + # Don't shorten directories that contain any of these files. They are anchors. + local anchor_files=( + .bzr + .citc + .git + .hg + .node-version + .python-version + .go-version + .ruby-version + .lua-version + .java-version + .perl-version + .php-version + .tool-versions + .mise.toml + .shorten_folder_marker + .svn + .terraform + CVS + Cargo.toml + composer.json + go.mod + package.json + stack.yaml + ) + typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})" + # If set to "first" ("last"), remove everything before the first (last) subdirectory that contains + # files matching $POWERLEVEL9K_SHORTEN_FOLDER_MARKER. For example, when the current directory is + # /foo/bar/git_repo/nested_git_repo/baz, prompt will display git_repo/nested_git_repo/baz (first) + # or nested_git_repo/baz (last). This assumes that git_repo and nested_git_repo contain markers + # and other directories don't. + # + # Optionally, "first" and "last" can be followed by ":" where is an integer. + # This moves the truncation point to the right (positive offset) or to the left (negative offset) + # relative to the marker. Plain "first" and "last" are equivalent to "first:0" and "last:0" + # respectively. + typeset -g POWERLEVEL9K_DIR_TRUNCATE_BEFORE_MARKER=false + # Don't shorten this many last directory segments. They are anchors. + typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1 + # Shorten directory if it's longer than this even if there is space for it. The value can + # be either absolute (e.g., '80') or a percentage of terminal width (e.g, '50%'). If empty, + # directory will be shortened only when prompt doesn't fit or when other parameters demand it + # (see POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS and POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT below). + # If set to `0`, directory will always be shortened to its minimum length. + typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least this + # many columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least + # COLUMNS * POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT * 0.01 columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50 + # If set to true, embed a hyperlink into the directory. Useful for quickly + # opening a directory in the file manager simply by clicking the link. + # Can also be handy when the directory is shortened, as it allows you to see + # the full directory that was used in previous commands. + typeset -g POWERLEVEL9K_DIR_HYPERLINK=false + + # Enable special styling for non-writable and non-existent directories. See POWERLEVEL9K_LOCK_ICON + # and POWERLEVEL9K_DIR_CLASSES below. + typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3 + + # The default icon shown next to non-writable and non-existent directories when + # POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3. + # typeset -g POWERLEVEL9K_LOCK_ICON='⭐' + + # POWERLEVEL9K_DIR_CLASSES allows you to specify custom icons and colors for different + # directories. It must be an array with 3 * N elements. Each triplet consists of: + # + # 1. A pattern against which the current directory ($PWD) is matched. Matching is done with + # extended_glob option enabled. + # 2. Directory class for the purpose of styling. + # 3. An empty string. + # + # Triplets are tried in order. The first triplet whose pattern matches $PWD wins. + # + # If POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3, non-writable and non-existent directories + # acquire class suffix _NOT_WRITABLE and NON_EXISTENT respectively. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=( + # '~/work(|/*)' WORK '' + # '~(|/*)' HOME '' + # '*' DEFAULT '') + # + # Whenever the current directory is ~/work or a subdirectory of ~/work, it gets styled with one + # of the following classes depending on its writability and existence: WORK, WORK_NOT_WRITABLE or + # WORK_NON_EXISTENT. + # + # Simply assigning classes to directories doesn't have any visible effects. It merely gives you an + # option to define custom colors and icons for different directory classes. + # + # # Styling for WORK. + # typeset -g POWERLEVEL9K_DIR_WORK_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_FOREGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_SHORTENED_FOREGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_ANCHOR_FOREGROUND=4 + # + # # Styling for WORK_NOT_WRITABLE. + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_SHORTENED_FOREGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_ANCHOR_FOREGROUND=4# + # + # Styling for WORK_NON_EXISTENT. + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_FOREGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_SHORTENED_FOREGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_ANCHOR_FOREGROUND=4 + # + # If a styling parameter isn't explicitly defined for some class, it falls back to the classless + # parameter. For example, if POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND is not set, it falls + # back to POWERLEVEL9K_DIR_FOREGROUND. + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=() + + # Custom prefix. + # typeset -g POWERLEVEL9K_DIR_PREFIX='%fin ' + + #####################################[ vcs: git status ]###################################### + # Branch icon. Set this parameter to '\UE0A0 ' for the popular Powerline branch icon. + typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 ' + + # Untracked files icon. It's really a question mark, your font isn't broken. + # Change the value of this parameter to show a different icon. + typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?' + + # Formatter for Git status. + # + # Example output: master wip ⇣42⇡42 *42 merge ~42 +42 !42 ?42. + # + # You can edit the function to customize how Git status looks. + # + # VCS_STATUS_* parameters are set by gitstatus plugin. See reference: + # https://github.com/romkatv/gitstatus/blob/master/gitstatus.plugin.zsh. + function my_git_formatter() { + emulate -L zsh + + if [[ -n $P9K_CONTENT ]]; then + # If P9K_CONTENT is not empty, use it. It's either "loading" or from vcs_info (not from + # gitstatus plugin). VCS_STATUS_* parameters are not available in this case. + typeset -g my_git_format=$P9K_CONTENT + return + fi + + if (( $1 )); then + # Styling for up-to-date Git status. + local meta='%f' # default foreground + local clean='%2F' # green foreground + local modified='%3F' # yellow foreground + local untracked='%4F' # blue foreground + local conflicted='%1F' # red foreground + else + # Styling for incomplete and stale Git status. + local meta='%f' # default foreground + local clean='%f' # default foreground + local modified='%f' # default foreground + local untracked='%f' # default foreground + local conflicted='%f' # default foreground + fi + + local res + + if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then + local branch=${(V)VCS_STATUS_LOCAL_BRANCH} + # If local branch name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show local branch name in full without truncation, delete the next line. + (( $#branch > 32 )) && branch[13,-13]="…" # <-- this line + res+="${clean}${(g::)POWERLEVEL9K_VCS_BRANCH_ICON}${branch//\%/%%}" + fi + + if [[ -n $VCS_STATUS_TAG + # Show tag only if not on a branch. + # Tip: To always show tag, delete the next line. + && -z $VCS_STATUS_LOCAL_BRANCH # <-- this line + ]]; then + local tag=${(V)VCS_STATUS_TAG} + # If tag name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show tag name in full without truncation, delete the next line. + (( $#tag > 32 )) && tag[13,-13]="…" # <-- this line + res+="${meta}#${clean}${tag//\%/%%}" + fi + + # Display the current Git commit if there is no branch and no tag. + # Tip: To always display the current Git commit, delete the next line. + [[ -z $VCS_STATUS_LOCAL_BRANCH && -z $VCS_STATUS_TAG ]] && # <-- this line + res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}" + + # Show tracking branch name if it differs from local branch. + if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then + res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}" + fi + + # Display "wip" if the latest commit's summary contains "wip" or "WIP". + if [[ $VCS_STATUS_COMMIT_SUMMARY == (|*[^[:alnum:]])(wip|WIP)(|[^[:alnum:]]*) ]]; then + res+=" ${modified}wip" + fi + + if (( VCS_STATUS_COMMITS_AHEAD || VCS_STATUS_COMMITS_BEHIND )); then + # ⇣42 if behind the remote. + (( VCS_STATUS_COMMITS_BEHIND )) && res+=" ${clean}⇣${VCS_STATUS_COMMITS_BEHIND}" + # ⇡42 if ahead of the remote; no leading space if also behind the remote: ⇣42⇡42. + (( VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND )) && res+=" " + (( VCS_STATUS_COMMITS_AHEAD )) && res+="${clean}⇡${VCS_STATUS_COMMITS_AHEAD}" + elif [[ -n $VCS_STATUS_REMOTE_BRANCH ]]; then + # Tip: Uncomment the next line to display '=' if up to date with the remote. + # res+=" ${clean}=" + fi + + # ⇠42 if behind the push remote. + (( VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" ${clean}⇠${VCS_STATUS_PUSH_COMMITS_BEHIND}" + (( VCS_STATUS_PUSH_COMMITS_AHEAD && !VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" " + # ⇢42 if ahead of the push remote; no leading space if also behind: ⇠42⇢42. + (( VCS_STATUS_PUSH_COMMITS_AHEAD )) && res+="${clean}⇢${VCS_STATUS_PUSH_COMMITS_AHEAD}" + # *42 if have stashes. + (( VCS_STATUS_STASHES )) && res+=" ${clean}*${VCS_STATUS_STASHES}" + # 'merge' if the repo is in an unusual state. + [[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}" + # ~42 if have merge conflicts. + (( VCS_STATUS_NUM_CONFLICTED )) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}" + # +42 if have staged changes. + (( VCS_STATUS_NUM_STAGED )) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}" + # !42 if have unstaged changes. + (( VCS_STATUS_NUM_UNSTAGED )) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}" + # ?42 if have untracked files. It's really a question mark, your font isn't broken. + # See POWERLEVEL9K_VCS_UNTRACKED_ICON above if you want to use a different icon. + # Remove the next line if you don't want to see untracked files at all. + (( VCS_STATUS_NUM_UNTRACKED )) && res+=" ${untracked}${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}" + # "─" if the number of unstaged files is unknown. This can happen due to + # POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY (see below) being set to a non-negative number lower + # than the number of files in the Git index, or due to bash.showDirtyState being set to false + # in the repository config. The number of staged and untracked files may also be unknown + # in this case. + (( VCS_STATUS_HAS_UNSTAGED == -1 )) && res+=" ${modified}─" + + typeset -g my_git_format=$res + } + functions -M my_git_formatter 2>/dev/null + + # Don't count the number of unstaged, untracked and conflicted files in Git repositories with + # more than this many files in the index. Negative value means infinity. + # + # If you are working in Git repositories with tens of millions of files and seeing performance + # sagging, try setting POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY to a number lower than the output + # of `git ls-files | wc -l`. Alternatively, add `bash.showDirtyState = false` to the repository's + # config: `git config bash.showDirtyState false`. + typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=-1 + + # Don't show Git status in prompt for repositories whose workdir matches this pattern. + # For example, if set to '~', the Git repository at $HOME/.git will be ignored. + # Multiple patterns can be combined with '|': '~(|/foo)|/bar/baz/*'. + typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' + + # Disable the default Git status formatting. + typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true + # Install our own Git status formatter. + typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter(1)))+${my_git_format}}' + typeset -g POWERLEVEL9K_VCS_LOADING_CONTENT_EXPANSION='${$((my_git_formatter(0)))+${my_git_format}}' + # Enable counters for staged, unstaged, etc. + typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 + + # Icon color. + typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_COLOR=2 + typeset -g POWERLEVEL9K_VCS_LOADING_VISUAL_IDENTIFIER_COLOR= + # Custom icon. + # typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + typeset -g POWERLEVEL9K_VCS_PREFIX='%fon ' + + # Show status of repositories of these types. You can add svn and/or hg if you are + # using them. If you do, your prompt may become slow even when your current directory + # isn't in an svn or hg repository. + typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) + + # These settings are used for repositories other than Git or when gitstatusd fails and + # Powerlevel10k has to fall back to using vcs_info. + typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=2 + typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=2 + typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=3 + + ##########################[ status: exit code of the last command ]########################### + # Enable OK_PIPE, ERROR_PIPE and ERROR_SIGNAL status states to allow us to enable, disable and + # style them independently from the regular OK and ERROR state. + typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true + + # Status on success. No content, just an icon. No need to show it if prompt_char is enabled as + # it will signify success by turning green. + typeset -g POWERLEVEL9K_STATUS_OK=false + typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=2 + typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when some part of a pipe command fails but the overall exit status is zero. It may look + # like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=2 + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when it's just an error code (e.g., '1'). No need to show it if prompt_char is enabled as + # it will signify error by turning red. + typeset -g POWERLEVEL9K_STATUS_ERROR=false + typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=1 + typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘' + + # Status when the last command was terminated by a signal. + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=1 + # Use terse signal names: "INT" instead of "SIGINT(2)". + typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘' + + # Status when some part of a pipe command fails and the overall exit status is also non-zero. + # It may look like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=1 + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘' + + ###################[ command_execution_time: duration of the last command ]################### + # Show duration of the last command if takes at least this many seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 + # Show this many fractional digits. Zero means round to seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 + # Execution time color. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=3 + # Duration format: 1d 2h 3m 4s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' + # Custom icon. + # typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PREFIX='%ftook ' + + #######################[ background_jobs: presence of background jobs ]####################### + # Don't show the number of background jobs. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false + # Background jobs color. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=1 + # Custom icon. + # typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ direnv: direnv status (https://direnv.net/) ]######################## + # Direnv color. + typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_DIRENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ asdf: asdf version manager (https://github.com/asdf-vm/asdf) ]############### + # Default asdf color. Only used to display tools for which there is no color override (see below). + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_FOREGROUND. + typeset -g POWERLEVEL9K_ASDF_FOREGROUND=6 + + # There are four parameters that can be used to hide asdf tools. Each parameter describes + # conditions under which a tool gets hidden. Parameters can hide tools but not unhide them. If at + # least one parameter decides to hide a tool, that tool gets hidden. If no parameter decides to + # hide a tool, it gets shown. + # + # Special note on the difference between POWERLEVEL9K_ASDF_SOURCES and + # POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW. Consider the effect of the following commands: + # + # asdf local python 3.8.1 + # asdf global python 3.8.1 + # + # After running both commands the current python version is 3.8.1 and its source is "local" as + # it takes precedence over "global". If POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW is set to false, + # it'll hide python version in this case because 3.8.1 is the same as the global version. + # POWERLEVEL9K_ASDF_SOURCES will hide python version only if the value of this parameter doesn't + # contain "local". + + # Hide tool versions that don't come from one of these sources. + # + # Available sources: + # + # - shell `asdf current` says "set by ASDF_${TOOL}_VERSION environment variable" + # - local `asdf current` says "set by /some/not/home/directory/file" + # - global `asdf current` says "set by /home/username/file" + # + # Note: If this parameter is set to (shell local global), it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SOURCES. + typeset -g POWERLEVEL9K_ASDF_SOURCES=(shell local global) + + # If set to false, hide tool versions that are the same as global. + # + # Note: The name of this parameter doesn't reflect its meaning at all. + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_PROMPT_ALWAYS_SHOW. + typeset -g POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW=false + + # If set to false, hide tool versions that are equal to "system". + # + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_SYSTEM. + typeset -g POWERLEVEL9K_ASDF_SHOW_SYSTEM=true + + # If set to non-empty value, hide tools unless there is a file matching the specified file pattern + # in the current directory, or its parent directory, or its grandparent directory, and so on. + # + # Note: If this parameter is set to empty value, it won't hide tools. + # Note: SHOW_ON_UPGLOB isn't specific to asdf. It works with all prompt segments. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_ON_UPGLOB. + # + # Example: Hide nodejs version when there is no package.json and no *.js files in the current + # directory, in `..`, in `../..` and so on. + # + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.js|package.json' + typeset -g POWERLEVEL9K_ASDF_SHOW_ON_UPGLOB= + + # Ruby version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUBY_FOREGROUND=1 + # typeset -g POWERLEVEL9K_ASDF_RUBY_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUBY_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Python version from asdf. + typeset -g POWERLEVEL9K_ASDF_PYTHON_FOREGROUND=6 + # typeset -g POWERLEVEL9K_ASDF_PYTHON_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PYTHON_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Go version from asdf. + typeset -g POWERLEVEL9K_ASDF_GOLANG_FOREGROUND=6 + # typeset -g POWERLEVEL9K_ASDF_GOLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_GOLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Node.js version from asdf. + typeset -g POWERLEVEL9K_ASDF_NODEJS_FOREGROUND=2 + # typeset -g POWERLEVEL9K_ASDF_NODEJS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Rust version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUST_FOREGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_RUST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUST_SHOW_ON_UPGLOB='*.foo|*.bar' + + # .NET Core version from asdf. + typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_FOREGROUND=5 + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Flutter version from asdf. + typeset -g POWERLEVEL9K_ASDF_FLUTTER_FOREGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Lua version from asdf. + typeset -g POWERLEVEL9K_ASDF_LUA_FOREGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_LUA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_LUA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Java version from asdf. + typeset -g POWERLEVEL9K_ASDF_JAVA_FOREGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_JAVA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JAVA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Perl version from asdf. + typeset -g POWERLEVEL9K_ASDF_PERL_FOREGROUND=6 + # typeset -g POWERLEVEL9K_ASDF_PERL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PERL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Erlang version from asdf. + typeset -g POWERLEVEL9K_ASDF_ERLANG_FOREGROUND=1 + # typeset -g POWERLEVEL9K_ASDF_ERLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ERLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Elixir version from asdf. + typeset -g POWERLEVEL9K_ASDF_ELIXIR_FOREGROUND=5 + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Postgres version from asdf. + typeset -g POWERLEVEL9K_ASDF_POSTGRES_FOREGROUND=6 + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_SHOW_ON_UPGLOB='*.foo|*.bar' + + # PHP version from asdf. + typeset -g POWERLEVEL9K_ASDF_PHP_FOREGROUND=5 + # typeset -g POWERLEVEL9K_ASDF_PHP_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PHP_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Haskell version from asdf. + typeset -g POWERLEVEL9K_ASDF_HASKELL_FOREGROUND=3 + # typeset -g POWERLEVEL9K_ASDF_HASKELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_HASKELL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Julia version from asdf. + typeset -g POWERLEVEL9K_ASDF_JULIA_FOREGROUND=2 + # typeset -g POWERLEVEL9K_ASDF_JULIA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JULIA_SHOW_ON_UPGLOB='*.foo|*.bar' + + ##########[ nordvpn: nordvpn connection status, linux only (https://nordvpn.com/) ]########### + # NordVPN connection indicator color. + typeset -g POWERLEVEL9K_NORDVPN_FOREGROUND=6 + # Hide NordVPN connection indicator when not connected. + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION= + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION= + # Custom icon. + # typeset -g POWERLEVEL9K_NORDVPN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ ranger: ranger shell (https://github.com/ranger/ranger) ]################## + # Ranger shell color. + typeset -g POWERLEVEL9K_RANGER_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ yazi: yazi shell (https://github.com/sxyazi/yazi) ]##################### + # Yazi shell color. + typeset -g POWERLEVEL9K_YAZI_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_YAZI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ nnn: nnn shell (https://github.com/jarun/nnn) ]####################### + # Nnn shell color. + typeset -g POWERLEVEL9K_NNN_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_NNN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ lf: lf shell (https://github.com/gokcehan/lf) ]####################### + # lf shell color. + typeset -g POWERLEVEL9K_LF_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_LF_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ xplr: xplr shell (https://github.com/sayanarijit/xplr) ]################## + # xplr shell color. + typeset -g POWERLEVEL9K_XPLR_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_XPLR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########################[ vim_shell: vim shell indicator (:sh) ]########################### + # Vim shell indicator color. + typeset -g POWERLEVEL9K_VIM_SHELL_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_VIM_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ midnight_commander: midnight commander shell (https://midnight-commander.org/) ]###### + # Midnight Commander shell color. + typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ nix_shell: nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) ]## + # Nix shell color. + typeset -g POWERLEVEL9K_NIX_SHELL_FOREGROUND=4 + + # Display the icon of nix_shell if PATH contains a subdirectory of /nix/store. + # typeset -g POWERLEVEL9K_NIX_SHELL_INFER_FROM_PATH=false + + # Tip: If you want to see just the icon without "pure" and "impure", uncomment the next line. + # typeset -g POWERLEVEL9K_NIX_SHELL_CONTENT_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_NIX_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ chezmoi_shell: chezmoi shell (https://www.chezmoi.io/) ]################## + # chezmoi shell color. + typeset -g POWERLEVEL9K_CHEZMOI_SHELL_FOREGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_CHEZMOI_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ disk_usage: disk usage ]################################## + # Colors for different levels of disk usage. + typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_FOREGROUND=2 + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_FOREGROUND=3 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_FOREGROUND=1 + # Thresholds for different levels of disk usage (percentage points). + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=90 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=95 + # If set to true, hide disk usage when below $POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL percent. + typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=false + # Custom icon. + # typeset -g POWERLEVEL9K_DISK_USAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ ram: free RAM ]####################################### + # RAM color. + typeset -g POWERLEVEL9K_RAM_FOREGROUND=2 + # Custom icon. + # typeset -g POWERLEVEL9K_RAM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################################[ swap: used swap ]###################################### + # Swap color. + typeset -g POWERLEVEL9K_SWAP_FOREGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_SWAP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ load: CPU load ]###################################### + # Show average CPU load over this many last minutes. Valid values are 1, 5 and 15. + typeset -g POWERLEVEL9K_LOAD_WHICH=5 + # Load color when load is under 50%. + typeset -g POWERLEVEL9K_LOAD_NORMAL_FOREGROUND=2 + # Load color when load is between 50% and 70%. + typeset -g POWERLEVEL9K_LOAD_WARNING_FOREGROUND=3 + # Load color when load is over 70%. + typeset -g POWERLEVEL9K_LOAD_CRITICAL_FOREGROUND=1 + # Custom icon. + # typeset -g POWERLEVEL9K_LOAD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ todo: todo items (https://github.com/todotxt/todo.txt-cli) ]################ + # Todo color. + typeset -g POWERLEVEL9K_TODO_FOREGROUND=4 + # Hide todo when the total number of tasks is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true + # Hide todo when the number of tasks after filtering is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false + + # Todo format. The following parameters are available within the expansion. + # + # - P9K_TODO_TOTAL_TASK_COUNT The total number of tasks. + # - P9K_TODO_FILTERED_TASK_COUNT The number of tasks after filtering. + # + # These variables correspond to the last line of the output of `todo.sh -p ls`: + # + # TODO: 24 of 42 tasks shown + # + # Here 24 is P9K_TODO_FILTERED_TASK_COUNT and 42 is P9K_TODO_TOTAL_TASK_COUNT. + # + # typeset -g POWERLEVEL9K_TODO_CONTENT_EXPANSION='$P9K_TODO_FILTERED_TASK_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TODO_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ timewarrior: timewarrior tracking status (https://timewarrior.net/) ]############ + # Timewarrior color. + typeset -g POWERLEVEL9K_TIMEWARRIOR_FOREGROUND=4 + # If the tracked task is longer than 24 characters, truncate and append "…". + # Tip: To always display tasks without truncation, delete the following parameter. + # Tip: To hide task names and display just the icon when time tracking is enabled, set the + # value of the following parameter to "". + typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}' + + # Custom icon. + # typeset -g POWERLEVEL9K_TIMEWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ taskwarrior: taskwarrior task count (https://taskwarrior.org/) ]############## + # Taskwarrior color. + typeset -g POWERLEVEL9K_TASKWARRIOR_FOREGROUND=6 + + # Taskwarrior segment format. The following parameters are available within the expansion. + # + # - P9K_TASKWARRIOR_PENDING_COUNT The number of pending tasks: `task +PENDING count`. + # - P9K_TASKWARRIOR_OVERDUE_COUNT The number of overdue tasks: `task +OVERDUE count`. + # + # Zero values are represented as empty parameters. + # + # The default format: + # + # '${P9K_TASKWARRIOR_OVERDUE_COUNT:+"!$P9K_TASKWARRIOR_OVERDUE_COUNT/"}$P9K_TASKWARRIOR_PENDING_COUNT' + # + # typeset -g POWERLEVEL9K_TASKWARRIOR_CONTENT_EXPANSION='$P9K_TASKWARRIOR_PENDING_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TASKWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ per_directory_history: Oh My Zsh per-directory-history local/global indicator ]####### + # Color when using local/global history. + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_FOREGROUND=5 + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_FOREGROUND=3 + + # Tip: Uncomment the next two lines to hide "local"/"global" text and leave just the icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_CONTENT_EXPANSION='' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_CONTENT_EXPANSION='' + + # Custom icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ cpu_arch: CPU architecture ]################################ + # CPU architecture color. + typeset -g POWERLEVEL9K_CPU_ARCH_FOREGROUND=3 + + # Hide the segment when on a specific CPU architecture. + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_CONTENT_EXPANSION= + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_VISUAL_IDENTIFIER_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CPU_ARCH_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ context: user@hostname ]################################## + # Context color when running with privileges. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=1 + # Context color in SSH without privileges. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=7 + # Default context color (no privileges, no SSH). + typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=7 + + # Context format when running with privileges: bold user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m' + # Context format when in SSH without privileges: user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m' + # Default context format (no privileges, no SSH): user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n@%m' + + # Don't show context unless running with privileges or in SSH. + # Tip: Remove the next line to always show context. + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT,VISUAL_IDENTIFIER}_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CONTEXT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + typeset -g POWERLEVEL9K_CONTEXT_PREFIX='%fwith ' + + ###[ virtualenv: python virtual environment (https://docs.python.org/3/library/venv.html) ]### + # Python virtual environment color. + typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=6 + # Don't show Python version next to the virtual environment name. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false + # If set to "false", won't show virtualenv if pyenv is already shown. + # If set to "if-different", won't show virtualenv if it's the same as pyenv. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_WITH_PYENV=false + # Separate environment name from Python version only with a space. + typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_VIRTUALENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ anaconda: conda environment (https://conda.io/) ]###################### + # Anaconda environment color. + typeset -g POWERLEVEL9K_ANACONDA_FOREGROUND=6 + + # Anaconda segment format. The following parameters are available within the expansion. + # + # - CONDA_PREFIX Absolute path to the active Anaconda/Miniconda environment. + # - CONDA_DEFAULT_ENV Name of the active Anaconda/Miniconda environment. + # - CONDA_PROMPT_MODIFIER Configurable prompt modifier (see below). + # - P9K_ANACONDA_PYTHON_VERSION Current python version (python --version). + # + # CONDA_PROMPT_MODIFIER can be configured with the following command: + # + # conda config --set env_prompt '({default_env}) ' + # + # The last argument is a Python format string that can use the following variables: + # + # - prefix The same as CONDA_PREFIX. + # - default_env The same as CONDA_DEFAULT_ENV. + # - name The last segment of CONDA_PREFIX. + # - stacked_env Comma-separated list of names in the environment stack. The first element is + # always the same as default_env. + # + # Note: '({default_env}) ' is the default value of env_prompt. + # + # The default value of POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION expands to $CONDA_PROMPT_MODIFIER + # without the surrounding parentheses, or to the last path component of CONDA_PREFIX if the former + # is empty. + typeset -g POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION='${${${${CONDA_PROMPT_MODIFIER#\(}% }%\)}:-${CONDA_PREFIX:t}}' + + # Custom icon. + # typeset -g POWERLEVEL9K_ANACONDA_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ pyenv: python environment (https://github.com/pyenv/pyenv) ]################ + # Pyenv color. + typeset -g POWERLEVEL9K_PYENV_FOREGROUND=6 + # Hide python version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global) + # If set to false, hide python version if it's the same as global: + # $(pyenv version-name) == $(pyenv global). + typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide python version if it's equal to "system". + typeset -g POWERLEVEL9K_PYENV_SHOW_SYSTEM=true + + # Pyenv segment format. The following parameters are available within the expansion. + # + # - P9K_CONTENT Current pyenv environment (pyenv version-name). + # - P9K_PYENV_PYTHON_VERSION Current python version (python --version). + # + # The default format has the following logic: + # + # 1. Display just "$P9K_CONTENT" if it's equal to "$P9K_PYENV_PYTHON_VERSION" or + # starts with "$P9K_PYENV_PYTHON_VERSION/". + # 2. Otherwise display "$P9K_CONTENT $P9K_PYENV_PYTHON_VERSION". + typeset -g POWERLEVEL9K_PYENV_CONTENT_EXPANSION='${P9K_CONTENT}${${P9K_CONTENT:#$P9K_PYENV_PYTHON_VERSION(|/*)}:+ $P9K_PYENV_PYTHON_VERSION}' + + # Custom icon. + # typeset -g POWERLEVEL9K_PYENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ goenv: go environment (https://github.com/syndbg/goenv) ]################ + # Goenv color. + typeset -g POWERLEVEL9K_GOENV_FOREGROUND=6 + # Hide go version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global) + # If set to false, hide go version if it's the same as global: + # $(goenv version-name) == $(goenv global). + typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide go version if it's equal to "system". + typeset -g POWERLEVEL9K_GOENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_GOENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ nodenv: node.js version from nodenv (https://github.com/nodenv/nodenv) ]########## + # Nodenv color. + typeset -g POWERLEVEL9K_NODENV_FOREGROUND=2 + # Hide node version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_NODENV_SOURCES=(shell local global) + # If set to false, hide node version if it's the same as global: + # $(nodenv version-name) == $(nodenv global). + typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NODENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ nvm: node.js version from nvm (https://github.com/nvm-sh/nvm) ]############### + # Nvm color. + typeset -g POWERLEVEL9K_NVM_FOREGROUND=2 + # If set to false, hide node version if it's the same as default: + # $(nvm version current) == $(nvm version default). + typeset -g POWERLEVEL9K_NVM_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NVM_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ nodeenv: node.js environment (https://github.com/ekalinin/nodeenv) ]############ + # Nodeenv color. + typeset -g POWERLEVEL9K_NODEENV_FOREGROUND=2 + # Don't show Node version next to the environment name. + typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false + # Separate environment name from Node version only with a space. + typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_NODEENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############################[ node_version: node.js version ]############################### + # Node version color. + typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=2 + # Show node version only when in a directory tree containing package.json. + typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ go_version: go version (https://golang.org) ]######################## + # Go version color. + typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=6 + # Show go version only when in a go project subdirectory. + typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ rust_version: rustc version (https://www.rust-lang.org) ]################## + # Rust version color. + typeset -g POWERLEVEL9K_RUST_VERSION_FOREGROUND=4 + # Show rust version only when in a rust project subdirectory. + typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_RUST_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ dotnet_version: .NET version (https://dotnet.microsoft.com) ]################ + # .NET version color. + typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND=5 + # Show .NET version only when in a .NET project subdirectory. + typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_DOTNET_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ php_version: php version (https://www.php.net/) ]###################### + # PHP version color. + typeset -g POWERLEVEL9K_PHP_VERSION_FOREGROUND=5 + # Show PHP version only when in a PHP project subdirectory. + typeset -g POWERLEVEL9K_PHP_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHP_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ laravel_version: laravel php framework version (https://laravel.com/) ]########### + # Laravel version color. + typeset -g POWERLEVEL9K_LARAVEL_VERSION_FOREGROUND=1 + # Custom icon. + # typeset -g POWERLEVEL9K_LARAVEL_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ java_version: java version (https://www.java.com/) ]#################### + # Java version color. + typeset -g POWERLEVEL9K_JAVA_VERSION_FOREGROUND=4 + # Show java version only when in a java project subdirectory. + typeset -g POWERLEVEL9K_JAVA_VERSION_PROJECT_ONLY=true + # Show brief version. + typeset -g POWERLEVEL9K_JAVA_VERSION_FULL=false + # Custom icon. + # typeset -g POWERLEVEL9K_JAVA_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###[ package: name@version from package.json (https://docs.npmjs.com/files/package.json) ]#### + # Package color. + typeset -g POWERLEVEL9K_PACKAGE_FOREGROUND=6 + # Package format. The following parameters are available within the expansion. + # + # - P9K_PACKAGE_NAME The value of `name` field in package.json. + # - P9K_PACKAGE_VERSION The value of `version` field in package.json. + # + # typeset -g POWERLEVEL9K_PACKAGE_CONTENT_EXPANSION='${(V)P9K_PACKAGE_NAME//\%/%%}@${(V)P9K_PACKAGE_VERSION//\%/%%}' + # Custom icon. + # typeset -g POWERLEVEL9K_PACKAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ rbenv: ruby version from rbenv (https://github.com/rbenv/rbenv) ]############## + # Rbenv color. + typeset -g POWERLEVEL9K_RBENV_FOREGROUND=1 + # Hide ruby version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global) + # If set to false, hide ruby version if it's the same as global: + # $(rbenv version-name) == $(rbenv global). + typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide ruby version if it's equal to "system". + typeset -g POWERLEVEL9K_RBENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_RBENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ rvm: ruby version from rvm (https://rvm.io) ]######################## + # Rvm color. + typeset -g POWERLEVEL9K_RVM_FOREGROUND=1 + # Don't show @gemset at the end. + typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false + # Don't show ruby- at the front. + typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_RVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ fvm: flutter version management (https://github.com/leoafarias/fvm) ]############ + # Fvm color. + typeset -g POWERLEVEL9K_FVM_FOREGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_FVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ luaenv: lua version from luaenv (https://github.com/cehoffman/luaenv) ]########### + # Lua color. + typeset -g POWERLEVEL9K_LUAENV_FOREGROUND=4 + # Hide lua version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global) + # If set to false, hide lua version if it's the same as global: + # $(luaenv version-name) == $(luaenv global). + typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide lua version if it's equal to "system". + typeset -g POWERLEVEL9K_LUAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_LUAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ jenv: java version from jenv (https://github.com/jenv/jenv) ]################ + # Java color. + typeset -g POWERLEVEL9K_JENV_FOREGROUND=4 + # Hide java version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global) + # If set to false, hide java version if it's the same as global: + # $(jenv version-name) == $(jenv global). + typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide java version if it's equal to "system". + typeset -g POWERLEVEL9K_JENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_JENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ plenv: perl version from plenv (https://github.com/tokuhirom/plenv) ]############ + # Perl color. + typeset -g POWERLEVEL9K_PLENV_FOREGROUND=6 + # Hide perl version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global) + # If set to false, hide perl version if it's the same as global: + # $(plenv version-name) == $(plenv global). + typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide perl version if it's equal to "system". + typeset -g POWERLEVEL9K_PLENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PLENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ perlbrew: perl version from perlbrew (https://github.com/gugod/App-perlbrew) ]############ + # Perlbrew color. + typeset -g POWERLEVEL9K_PERLBREW_FOREGROUND=67 + # Show perlbrew version only when in a perl project subdirectory. + typeset -g POWERLEVEL9K_PERLBREW_PROJECT_ONLY=true + # Don't show "perl-" at the front. + typeset -g POWERLEVEL9K_PERLBREW_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_PERLBREW_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ phpenv: php version from phpenv (https://github.com/phpenv/phpenv) ]############ + # PHP color. + typeset -g POWERLEVEL9K_PHPENV_FOREGROUND=5 + # Hide php version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PHPENV_SOURCES=(shell local global) + # If set to false, hide php version if it's the same as global: + # $(phpenv version-name) == $(phpenv global). + typeset -g POWERLEVEL9K_PHPENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide php version if it's equal to "system". + typeset -g POWERLEVEL9K_PHPENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHPENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######[ scalaenv: scala version from scalaenv (https://github.com/scalaenv/scalaenv) ]####### + # Scala color. + typeset -g POWERLEVEL9K_SCALAENV_FOREGROUND=1 + # Hide scala version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_SCALAENV_SOURCES=(shell local global) + # If set to false, hide scala version if it's the same as global: + # $(scalaenv version-name) == $(scalaenv global). + typeset -g POWERLEVEL9K_SCALAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide scala version if it's equal to "system". + typeset -g POWERLEVEL9K_SCALAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_SCALAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ haskell_stack: haskell version from stack (https://haskellstack.org/) ]########### + # Haskell color. + typeset -g POWERLEVEL9K_HASKELL_STACK_FOREGROUND=3 + # Hide haskell version if it doesn't come from one of these sources. + # + # shell: version is set by STACK_YAML + # local: version is set by stack.yaml up the directory tree + # global: version is set by the implicit global project (~/.stack/global-project/stack.yaml) + typeset -g POWERLEVEL9K_HASKELL_STACK_SOURCES=(shell local) + # If set to false, hide haskell version if it's the same as in the implicit global project. + typeset -g POWERLEVEL9K_HASKELL_STACK_ALWAYS_SHOW=true + # Custom icon. + # typeset -g POWERLEVEL9K_HASKELL_STACK_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ kubecontext: current kubernetes context (https://kubernetes.io/) ]############# + # Show kubecontext only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show kubecontext. + typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc|istioctl|kogito|k9s|helmfile|flux|fluxctl|stern|kubeseal|skaffold|kubent|kubecolor|cmctl|sparkctl' + + # Kubernetes context classes for the purpose of using different colors, icons and expansions with + # different contexts. + # + # POWERLEVEL9K_KUBECONTEXT_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current kubernetes context gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_KUBECONTEXT_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_KUBECONTEXT_CLASSES defines the context class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current kubernetes context is "deathray-testing/default", its class is TEST + # because "deathray-testing/default" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_FOREGROUND=3 + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=5 + # typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_KUBECONTEXT_CONTENT_EXPANSION to specify the content displayed by kubecontext + # segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # Within the expansion the following parameters are always available: + # + # - P9K_CONTENT The content that would've been displayed if there was no content + # expansion defined. + # - P9K_KUBECONTEXT_NAME The current context's name. Corresponds to column NAME in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_CLUSTER The current context's cluster. Corresponds to column CLUSTER in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_NAMESPACE The current context's namespace. Corresponds to column NAMESPACE + # in the output of `kubectl config get-contexts`. If there is no + # namespace, the parameter is set to "default". + # - P9K_KUBECONTEXT_USER The current context's user. Corresponds to column AUTHINFO in the + # output of `kubectl config get-contexts`. + # + # If the context points to Google Kubernetes Engine (GKE) or Elastic Kubernetes Service (EKS), + # the following extra parameters are available: + # + # - P9K_KUBECONTEXT_CLOUD_NAME Either "gke" or "eks". + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT Account/project ID. + # - P9K_KUBECONTEXT_CLOUD_ZONE Availability zone. + # - P9K_KUBECONTEXT_CLOUD_CLUSTER Cluster. + # + # P9K_KUBECONTEXT_CLOUD_* parameters are derived from P9K_KUBECONTEXT_CLUSTER. For example, + # if P9K_KUBECONTEXT_CLUSTER is "gke_my-account_us-east1-a_my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=gke + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=my-account + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east1-a + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + # + # If P9K_KUBECONTEXT_CLUSTER is "arn:aws:eks:us-east-1:123456789012:cluster/my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=eks + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=123456789012 + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east-1 + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION= + # Show P9K_KUBECONTEXT_CLOUD_CLUSTER if it's not empty and fall back to P9K_KUBECONTEXT_NAME. + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}' + # Append the current context's namespace if it's not "default". + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}' + + # Custom prefix. + typeset -g POWERLEVEL9K_KUBECONTEXT_PREFIX='%fat ' + + ################[ terraform: terraform workspace (https://www.terraform.io) ]################# + # Don't show terraform workspace if it's literally "default". + typeset -g POWERLEVEL9K_TERRAFORM_SHOW_DEFAULT=false + # POWERLEVEL9K_TERRAFORM_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current terraform workspace gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_TERRAFORM_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_TERRAFORM_CLASSES defines the workspace class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current terraform workspace is "project_test", its class is TEST because "project_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_FOREGROUND=2 + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + typeset -g POWERLEVEL9K_TERRAFORM_OTHER_FOREGROUND=4 + # typeset -g POWERLEVEL9K_TERRAFORM_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ terraform_version: terraform version (https://www.terraform.io) ]############## + # Terraform version color. + typeset -g POWERLEVEL9K_TERRAFORM_VERSION_FOREGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_TERRAFORM_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ aws: aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) ]# + # Show aws only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show aws. + typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|cdk|terraform|tofu|pulumi|terragrunt' + + # POWERLEVEL9K_AWS_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current AWS profile gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AWS_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AWS_CLASSES defines the profile class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current AWS profile is "company_test", its class is TEST + # because "company_test" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AWS_TEST_FOREGROUND=2 + # typeset -g POWERLEVEL9K_AWS_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AWS_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_AWS_DEFAULT_FOREGROUND=3 + # typeset -g POWERLEVEL9K_AWS_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # AWS segment format. The following parameters are available within the expansion. + # + # - P9K_AWS_PROFILE The name of the current AWS profile. + # - P9K_AWS_REGION The region associated with the current AWS profile. + typeset -g POWERLEVEL9K_AWS_CONTENT_EXPANSION='${P9K_AWS_PROFILE//\%/%%}${P9K_AWS_REGION:+ ${P9K_AWS_REGION//\%/%%}}' + + #[ aws_eb_env: aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) ]# + # AWS Elastic Beanstalk environment color. + typeset -g POWERLEVEL9K_AWS_EB_ENV_FOREGROUND=2 + # Custom icon. + # typeset -g POWERLEVEL9K_AWS_EB_ENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ azure: azure account name (https://docs.microsoft.com/en-us/cli/azure) ]########## + # Show azure only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show azure. + typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|tofu|pulumi|terragrunt' + + # POWERLEVEL9K_AZURE_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current azure account name gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AZURE_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AZURE_CLASSES defines the account class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current azure account is "company_test", its class is TEST because "company_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AZURE_TEST_FOREGROUND=2 + # typeset -g POWERLEVEL9K_AZURE_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AZURE_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + + # Azure account name color. + typeset -g POWERLEVEL9K_AZURE_OTHER_FOREGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_AZURE_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ gcloud: google cloud account and project (https://cloud.google.com/) ]########### + # Show gcloud only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show gcloud. + typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs|gsutil' + # Google cloud color. + typeset -g POWERLEVEL9K_GCLOUD_FOREGROUND=4 + + # Google cloud format. Change the value of POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION and/or + # POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION if the default is too verbose or not informative + # enough. You can use the following parameters in the expansions. Each of them corresponds to the + # output of `gcloud` tool. + # + # Parameter | Source + # -------------------------|-------------------------------------------------------------------- + # P9K_GCLOUD_CONFIGURATION | gcloud config configurations list --format='value(name)' + # P9K_GCLOUD_ACCOUNT | gcloud config get-value account + # P9K_GCLOUD_PROJECT_ID | gcloud config get-value project + # P9K_GCLOUD_PROJECT_NAME | gcloud projects describe $P9K_GCLOUD_PROJECT_ID --format='value(name)' + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced with '%%'. + # + # Obtaining project name requires sending a request to Google servers. This can take a long time + # and even fail. When project name is unknown, P9K_GCLOUD_PROJECT_NAME is not set and gcloud + # prompt segment is in state PARTIAL. When project name gets known, P9K_GCLOUD_PROJECT_NAME gets + # set and gcloud prompt segment transitions to state COMPLETE. + # + # You can customize the format, icon and colors of gcloud segment separately for states PARTIAL + # and COMPLETE. You can also hide gcloud in state PARTIAL by setting + # POWERLEVEL9K_GCLOUD_PARTIAL_VISUAL_IDENTIFIER_EXPANSION and + # POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION to empty. + typeset -g POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_ID//\%/%%}' + typeset -g POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_NAME//\%/%%}' + + # Send a request to Google (by means of `gcloud projects describe ...`) to obtain project name + # this often. Negative value disables periodic polling. In this mode project name is retrieved + # only when the current configuration, account or project id changes. + typeset -g POWERLEVEL9K_GCLOUD_REFRESH_PROJECT_NAME_SECONDS=60 + + # Custom icon. + # typeset -g POWERLEVEL9K_GCLOUD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ google_app_cred: google application credentials (https://cloud.google.com/docs/authentication/production) ]# + # Show google_app_cred only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show google_app_cred. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|tofu|pulumi|terragrunt' + + # Google application credentials classes for the purpose of using different colors, icons and + # expansions with different credentials. + # + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES is an array with even number of elements. The first + # element in each pair defines a pattern against which the current kubernetes context gets + # matched. More specifically, it's P9K_CONTENT prior to the application of context expansion + # (see below) that gets matched. If you unset all POWERLEVEL9K_GOOGLE_APP_CRED_*CONTENT_EXPANSION + # parameters, you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES defines the context class. Patterns are tried in order. + # The first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD + # '*:*test*:*' TEST + # '*' DEFAULT) + # + # If your current Google application credentials is "service_account deathray-testing x@y.com", + # its class is TEST because it doesn't match the pattern '* *prod* *' but does match '* *test* *'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_FOREGROUND=3 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_CONTENT_EXPANSION='$P9K_GOOGLE_APP_CRED_PROJECT_ID' + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD # These values are examples that are unlikely + # '*:*test*:*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_FOREGROUND=5 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_GOOGLE_APP_CRED_CONTENT_EXPANSION to specify the content displayed by + # google_app_cred segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # You can use the following parameters in the expansion. Each of them corresponds to one of the + # fields in the JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS. + # + # Parameter | JSON key file field + # ---------------------------------+--------------- + # P9K_GOOGLE_APP_CRED_TYPE | type + # P9K_GOOGLE_APP_CRED_PROJECT_ID | project_id + # P9K_GOOGLE_APP_CRED_CLIENT_EMAIL | client_email + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced by '%%'. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}' + + ##############[ toolbox: toolbox name (https://github.com/containers/toolbox) ]############### + # Toolbox color. + typeset -g POWERLEVEL9K_TOOLBOX_FOREGROUND=3 + # Don't display the name of the toolbox if it matches fedora-toolbox-*. + typeset -g POWERLEVEL9K_TOOLBOX_CONTENT_EXPANSION='${P9K_TOOLBOX_NAME:#fedora-toolbox-*}' + # Custom icon. + # typeset -g POWERLEVEL9K_TOOLBOX_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + typeset -g POWERLEVEL9K_TOOLBOX_PREFIX='%fin ' + + ###############################[ public_ip: public IP address ]############################### + # Public IP color. + typeset -g POWERLEVEL9K_PUBLIC_IP_FOREGROUND=6 + # Custom icon. + # typeset -g POWERLEVEL9K_PUBLIC_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ########################[ vpn_ip: virtual private network indicator ]######################### + # VPN IP color. + typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=3 + # When on VPN, show just an icon without the IP address. + # Tip: To display the private IP address when on VPN, remove the next line. + typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION= + # Regular expression for the VPN network interface. Run `ifconfig` or `ip -4 a show` while on VPN + # to see the name of the interface. + typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(gpd|wg|(.*tun)|tailscale)[0-9]*|(zt.*)' + # If set to true, show one segment per matching network interface. If set to false, show only + # one segment corresponding to the first matching network interface. + # Tip: If you set it to true, you'll probably want to unset POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION. + typeset -g POWERLEVEL9K_VPN_IP_SHOW_ALL=false + # Custom icon. + # typeset -g POWERLEVEL9K_VPN_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ ip: ip address and bandwidth usage for a specified network interface ]########### + # IP color. + typeset -g POWERLEVEL9K_IP_FOREGROUND=4 + # The following parameters are accessible within the expansion: + # + # Parameter | Meaning + # ----------------------+------------------------------------------- + # P9K_IP_IP | IP address + # P9K_IP_INTERFACE | network interface + # P9K_IP_RX_BYTES | total number of bytes received + # P9K_IP_TX_BYTES | total number of bytes sent + # P9K_IP_RX_BYTES_DELTA | number of bytes received since last prompt + # P9K_IP_TX_BYTES_DELTA | number of bytes sent since last prompt + # P9K_IP_RX_RATE | receive rate (since last prompt) + # P9K_IP_TX_RATE | send rate (since last prompt) + typeset -g POWERLEVEL9K_IP_CONTENT_EXPANSION='$P9K_IP_IP${P9K_IP_RX_RATE:+ %2F⇣$P9K_IP_RX_RATE}${P9K_IP_TX_RATE:+ %3F⇡$P9K_IP_TX_RATE}' + # Show information for the first network interface whose name matches this regular expression. + # Run `ifconfig` or `ip -4 a show` to see the names of all network interfaces. + typeset -g POWERLEVEL9K_IP_INTERFACE='[ew].*' + # Custom icon. + # typeset -g POWERLEVEL9K_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #########################[ proxy: system-wide http/https/ftp proxy ]########################## + # Proxy color. + typeset -g POWERLEVEL9K_PROXY_FOREGROUND=2 + # Custom icon. + # typeset -g POWERLEVEL9K_PROXY_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ battery: internal battery ]################################# + # Show battery in red when it's below this level and not connected to power supply. + typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20 + typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=1 + # Show battery in green when it's charging or fully charged. + typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=2 + # Show battery in yellow when it's discharging. + typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=3 + # Battery pictograms going from low to high level of charge. + typeset -g POWERLEVEL9K_BATTERY_STAGES='\UF008E\UF007A\UF007B\UF007C\UF007D\UF007E\UF007F\UF0080\UF0081\UF0082\UF0079' + # Don't show the remaining time to charge/discharge. + typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false + + #####################################[ wifi: wifi speed ]##################################### + # WiFi color. + typeset -g POWERLEVEL9K_WIFI_FOREGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use different colors and icons depending on signal strength ($P9K_WIFI_BARS). + # + # # Wifi colors and icons for different signal strength levels (low to high). + # typeset -g my_wifi_fg=(4 4 4 4 4) # <-- change these values + # typeset -g my_wifi_icon=('WiFi' 'WiFi' 'WiFi' 'WiFi' 'WiFi') # <-- change these values + # + # typeset -g POWERLEVEL9K_WIFI_CONTENT_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}$P9K_WIFI_LAST_TX_RATE Mbps' + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}${my_wifi_icon[P9K_WIFI_BARS+1]}' + # + # The following parameters are accessible within the expansions: + # + # Parameter | Meaning + # ----------------------+--------------- + # P9K_WIFI_SSID | service set identifier, a.k.a. network name + # P9K_WIFI_LINK_AUTH | authentication protocol such as "wpa2-psk" or "none"; empty if unknown + # P9K_WIFI_LAST_TX_RATE | wireless transmit rate in megabits per second + # P9K_WIFI_RSSI | signal strength in dBm, from -120 to 0 + # P9K_WIFI_NOISE | noise in dBm, from -120 to 0 + # P9K_WIFI_BARS | signal strength in bars, from 0 to 4 (derived from P9K_WIFI_RSSI and P9K_WIFI_NOISE) + + ####################################[ time: current time ]#################################### + # Current time color. + typeset -g POWERLEVEL9K_TIME_FOREGROUND=6 + # Format for the current time: 09:51:02. See `man 3 strftime`. + typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}' + # If set to true, time will update when you hit enter. This way prompts for the past + # commands will contain the start times of their commands as opposed to the default + # behavior where they contain the end times of their preceding commands. + typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false + # Custom icon. + # typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + typeset -g POWERLEVEL9K_TIME_PREFIX='%fat ' + + # Example of a user-defined prompt segment. Function prompt_example will be called on every + # prompt if `example` prompt segment is added to POWERLEVEL9K_LEFT_PROMPT_ELEMENTS or + # POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS. It displays an icon and green text greeting the user. + # + # Type `p10k help segment` for documentation and a more sophisticated example. + function prompt_example() { + p10k segment -f 2 -i '⭐' -t 'hello, %n' + } + + # User-defined prompt segments may optionally provide an instant_prompt_* function. Its job + # is to generate the prompt segment for display in instant prompt. See + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # + # Powerlevel10k will call instant_prompt_* at the same time as the regular prompt_* function + # and will record all `p10k segment` calls it makes. When displaying instant prompt, Powerlevel10k + # will replay these calls without actually calling instant_prompt_*. It is imperative that + # instant_prompt_* always makes the same `p10k segment` calls regardless of environment. If this + # rule is not observed, the content of instant prompt will be incorrect. + # + # Usually, you should either not define instant_prompt_* or simply call prompt_* from it. If + # instant_prompt_* is not defined for a segment, the segment won't be shown in instant prompt. + function instant_prompt_example() { + # Since prompt_example always makes the same `p10k segment` calls, we can call it from + # instant_prompt_example. This will give us the same `example` prompt segment in the instant + # and regular prompts. + prompt_example + } + + # User-defined prompt segments can be customized the same way as built-in segments. + # typeset -g POWERLEVEL9K_EXAMPLE_FOREGROUND=208 + # typeset -g POWERLEVEL9K_EXAMPLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt + # when accepting a command line. Supported values: + # + # - off: Don't change prompt when accepting a command line. + # - always: Trim down prompt when accepting a command line. + # - same-dir: Trim down prompt when accepting a command line unless this is the first command + # typed after changing current working directory. + typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always + + # Instant prompt mode. + # + # - off: Disable instant prompt. Choose this if you've tried instant prompt and found + # it incompatible with your zsh configuration files. + # - quiet: Enable instant prompt and don't print warnings when detecting console output + # during zsh initialization. Choose this if you've read and understood + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # - verbose: Enable instant prompt and print a warning when detecting console output during + # zsh initialization. Choose this if you've never tried instant prompt, haven't + # seen the warning, or if you are unsure what this all means. + typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose + + # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. + # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload + # can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you + # really need it. + typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true + + # If p10k is already loaded, reload configuration. + # This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true. + (( ! $+functions[p10k] )) || p10k reload +} + +# Tell `p10k configure` which file it should overwrite. +typeset -g POWERLEVEL9K_CONFIG_FILE=${${(%):-%x}:a} + +(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} +'builtin' 'unset' 'p10k_config_opts' diff --git a/modules/zsh/files/.zshrc b/modules/zsh/files/.zshrc new file mode 100644 index 00000000..e2901ee8 --- /dev/null +++ b/modules/zsh/files/.zshrc @@ -0,0 +1,79 @@ +# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. +# Initialization code that may require console input (password prompts, [y/n] +# confirmations, etc.) must go above this block; everything else may go below. + +if [[ -z "$TMUX" && -n "$PS1" ]]; then + exec tmux new-session -A -s main +fi + +if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then + source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" +fi + +# Set the driectory we want to store zinit and plugins +ZINIT_HOME="${XDG_DATA_HOME:-${HOME}/.local/share}/zinit/zinit.git" + +# Download zinit, if it's not there +if [ ! -d "$ZINIT_HOME" ]; then + mkdir -p "$(dirname $ZINIT_HOME)" + git clone https://github.com/zdharma-continuum/zinit.git $ZINIT_HOME +fi + +# Source zinit +source "${ZINIT_HOME}/zinit.zsh" + +autoload -U compinit && compinit + +# Add in powerlevel10k +zinit ice depth=1; zinit light romkatv/powerlevel10k + +# Add in other plugins +zinit light Aloxaf/fzf-tab + +# Add in zsh plugins +zinit light zsh-users/zsh-syntax-highlighting +zinit light zsh-users/zsh-completions +zinit light zsh-users/zsh-autosuggestions + +# Keybinds +bindkey -v +bindkey '^p' history-search-backward +bindkey '^n' history-search-forward + +# History +HISTSIZE=5000 +HISTFILE=~/.zsh_history +SAVEHIST=$HISTSIZE +HISTDUP=erase + +setopt appendhistory +setopt sharehistory +setopt hist_ignore_space +setopt hist_ignore_all_dups +setopt hist_save_no_dups +setopt hist_ignore_dups +setopt hist_find_no_dups + +# Completion style +zstyle ':completion:*' matcher-list 'm:{a-z}={A-Za-z}' +zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" +zstyle ':completion:*:git-checkout:*' sort false +zstyle ':completion:*:descriptions' format '[%d]' +zstyle ':completion:*' menu no +zstyle ':fzf-tab:complete:cd:*' fzf-preview 'eza -1 --color=always --icons=always $realpath' +zstyle ':fzf-tab:*' fzf-flags --color=fg:1,fg+:2 --bind=tab:accept +zstyle ':fzf-tab:*' use-fzf-default-opts yes +zstyle ':fzf-tab:*' switch-group '<' '>' +zstyle ':fzf-tab:*' fzf-command ftb-tmux-popup + +# Aliases +alias ls='eza --icons=always' +alias ll='eza -lh --icons=always --git' +alias la='eza -lah --icons=always --git' +alias tree='eza --tree --level=2 --icons=always' + +eval "$(fzf --zsh)" +eval "$(zoxide init zsh)" + +# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh. +[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh diff --git a/modules/zsh/install.sh b/modules/zsh/install.sh new file mode 100755 index 00000000..36821838 --- /dev/null +++ b/modules/zsh/install.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +source "$RETRO_DIR/lib/log.sh" + +install_zsh() { + local current_shell + current_shell=$(getent passwd "$USER" | cut -d: -f7) + + rx_log "info" "Clearing p10k instant prompt cache..." + rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-"*.zsh + rx_log "success" "p10k cache cleared" + + if [[ "$current_shell" == *"zsh"* ]]; then + rx_log "info" "Default shell is already zsh — skipping" + return 0 + fi + + rx_log "warn" "Default shell is ${PINK}${current_shell}${RESET}, switching to zsh..." + chsh -s "$(command -v zsh)" + rx_log "success" "Default shell changed to zsh (takes effect on next login)" +} + +install_zsh diff --git a/modules/zsh/packages.sh b/modules/zsh/packages.sh new file mode 100644 index 00000000..04b905c3 --- /dev/null +++ b/modules/zsh/packages.sh @@ -0,0 +1,4 @@ +zsh +fzf +eza +git diff --git a/modules/zsh/properties.json b/modules/zsh/properties.json new file mode 100644 index 00000000..86c764f6 --- /dev/null +++ b/modules/zsh/properties.json @@ -0,0 +1,14 @@ +{ + "title": "Zsh", + "description": "Zsh configuration with Powerlevel10k, zinit, and productivity plugins", + "type": "extra", + "access": "user", + "defaults": true, + "mode": "install", + "config": "./files", + "install": "~", + "overwrite": false, + "check": "zsh", + "files": [".zshrc", ".p10k.zsh"], + "dependencies": ["tmux"] +} diff --git a/scripts/timeshift_core.sh b/scripts/timeshift_core.sh index fa8217d8..47cfaf75 100755 --- a/scripts/timeshift_core.sh +++ b/scripts/timeshift_core.sh @@ -19,11 +19,14 @@ _write_json_field() { local file="$1" local field="$2" local value="$3" - if grep -q "\"${field}\"" "$file" 2>/dev/null; then - sudo sed -i "s|\"${field}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"|\"${field}\": \"${value}\"|" "$file" - else - sudo sed -i "/\"schedule_daily\"/s|$|\n \"${field}\": \"${value}\",|" "$file" - fi + sudo python3 -c " +import json +with open('$file') as f: + cfg = json.load(f) +cfg['$field'] = '$value' +with open('$file', 'w') as f: + json.dump(cfg, f, indent=2) +" } _check_timeshift() { diff --git a/tests/module_structure_test.sh b/tests/module_structure_test.sh index 0d4ab31a..0f64ae9f 100755 --- a/tests/module_structure_test.sh +++ b/tests/module_structure_test.sh @@ -40,7 +40,12 @@ for mod_dir in "${MODULES_DIRS[@]}"; do if .overwrite != null and (.overwrite | type != "boolean") then "invalid overwrite (must be boolean)" else empty end, if .check != null and (.check | type != "string") then "invalid check" else empty end, if .uninstall_pkgs != null and (.uninstall_pkgs | type != "boolean") then "invalid uninstall_pkgs" else empty end, - if .gui != null and (.gui | type != "boolean") then "invalid gui" else empty end + if .gui != null and (.gui | type != "boolean") then "invalid gui" else empty end, + if .files != null and (.files | type != "array") then "invalid files (must be array)" else empty end, + if .files != null and (.files | all(. | type == "string") | not) then "invalid files (must be array of strings)" else empty end, + if .dependency != null and (.dependency | type != "string") then "invalid dependency" else empty end, + if .dependencies != null and (.dependencies | type != "array") then "invalid dependencies (must be array)" else empty end, + if .dependencies != null and (.dependencies | all(. | type == "string") | not) then "invalid dependencies (must be array of strings)" else empty end ] | join(", ") ' "$json_file")