Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions alix/porter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import yaml
from pathlib import Path
from datetime import datetime
from typing import List, Dict, Any
from typing import List, Dict, Any, Tuple

from alix.models import Alias
from alix.storage import AliasStorage
Expand All @@ -18,7 +18,7 @@ def export_to_dict(self, aliases: List[Alias] = None, tag_filter: str = None) ->
"""Export aliases to a dictionary format"""
if aliases is None:
aliases = self.storage.list_all()

# Apply tag filter if specified
if tag_filter:
aliases = [alias for alias in aliases if tag_filter in alias.tags]
Expand All @@ -29,13 +29,13 @@ def export_to_dict(self, aliases: List[Alias] = None, tag_filter: str = None) ->
"count": len(aliases),
"aliases": [alias.to_dict() for alias in aliases],
}

if tag_filter:
export_data["tag_filter"] = tag_filter

return export_data

def export_to_file(self, filepath: Path, format: str = "json", tag_filter: str = None) -> tuple[bool, str]:
def export_to_file(self, filepath: Path, format: str = "json", tag_filter: str = None) -> Tuple[bool, str]:
"""Export aliases to a file"""
data = self.export_to_dict(tag_filter=tag_filter)

Expand All @@ -54,7 +54,7 @@ def export_to_file(self, filepath: Path, format: str = "json", tag_filter: str =
except Exception as e:
return False, f"Export failed: {str(e)}"

def import_from_file(self, filepath: Path, merge: bool = False, tag_filter: str = None) -> tuple[bool, str]:
def import_from_file(self, filepath: Path, merge: bool = False, tag_filter: str = None) -> Tuple[bool, str]:
"""Import aliases from a file"""
if not filepath.exists():
return False, f"File not found: {filepath}"
Expand All @@ -75,12 +75,12 @@ def import_from_file(self, filepath: Path, merge: bool = False, tag_filter: str

for alias_data in data["aliases"]:
alias = Alias.from_dict(alias_data)

# Apply tag filter if specified
if tag_filter and tag_filter not in alias.tags:
tag_filtered += 1
continue

if merge or alias.name not in self.storage.aliases:
self.storage.aliases[alias.name] = alias
imported += 1
Expand All @@ -100,20 +100,20 @@ def import_from_file(self, filepath: Path, merge: bool = False, tag_filter: str
except Exception as e:
return False, f"Import failed: {str(e)}"

def export_by_tags(self, tags: List[str], filepath: Path, format: str = "json", match_all: bool = False) -> tuple[bool, str]:
def export_by_tags(self, tags: List[str], filepath: Path, format: str = "json", match_all: bool = False) -> Tuple[bool, str]:
"""Export aliases that match any (or all) of the specified tags"""
aliases = self.storage.list_all()

if match_all:
# Match aliases that have ALL specified tags
filtered_aliases = [alias for alias in aliases if all(tag in alias.tags for tag in tags)]
else:
# Match aliases that have ANY of the specified tags
filtered_aliases = [alias for alias in aliases if any(tag in alias.tags for tag in tags)]

if not filtered_aliases:
return False, f"No aliases found matching tags: {', '.join(tags)}"

export_data = {
"version": "1.0",
"exported_at": datetime.now().isoformat(),
Expand All @@ -122,7 +122,7 @@ def export_by_tags(self, tags: List[str], filepath: Path, format: str = "json",
"count": len(filtered_aliases),
"aliases": [alias.to_dict() for alias in filtered_aliases]
}

try:
if format == "yaml":
with open(filepath, "w") as f:
Expand All @@ -142,19 +142,19 @@ def get_tag_statistics(self) -> Dict[str, Any]:
aliases = self.storage.list_all()
tag_counts = {}
tag_combinations = {}

for alias in aliases:
# Count individual tags
for tag in alias.tags:
tag_counts[tag] = tag_counts.get(tag, 0) + 1

# Count tag combinations (pairs)
if len(alias.tags) >= 2:
for i, tag1 in enumerate(alias.tags):
for tag2 in alias.tags[i+1:]:
combo = tuple(sorted([tag1, tag2]))
tag_combinations[combo] = tag_combinations.get(combo, 0) + 1

return {
"total_tags": len(tag_counts),
"total_aliases": len(aliases),
Expand Down
8 changes: 4 additions & 4 deletions alix/render.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import re
from difflib import SequenceMatcher

from typing import Any, Literal
from typing import Any, List, Literal, Union

from rich.table import Table
from rich.console import Console
from rich.text import Text
from rich import box

class Render:
def _split_keep_ws(self, s: str) -> list[str | Any]:
def _split_keep_ws(self, s: str) -> List[Union[str, Any]]:
return re.split(r'(\s+)', s)

def _word_level_text(self, left: str, right: str, side: Literal["left", "right"]) -> Text:
"""
Give color-coded output for additions/deletions using rich.Text,
with token highlight base on the left vs right comparison.
with token highlight base on the left vs right comparison.
"""
if side not in ("left", "right"):
raise ValueError("side must be 'left' or 'right'")

left_tokens = self._split_keep_ws(left)
right_tokens = self._split_keep_ws(right)
sm = SequenceMatcher(None, left_tokens, right_tokens)
Expand Down
16 changes: 8 additions & 8 deletions alix/shell_integrator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import shutil
from pathlib import Path
from datetime import datetime
from typing import Optional
from typing import Optional, Tuple

from alix.shell_detector import ShellDetector, ShellType
from alix.storage import AliasStorage
Expand Down Expand Up @@ -58,8 +58,8 @@ def export_aliases(self, shell_type: ShellType) -> str:

return "\n".join(lines)

def preview_aliases(self, target_file: Optional[Path] = None) -> tuple[str, str]:
"""Get old and new config for dry-run"""
def preview_aliases(self, target_file: Optional[Path] = None) -> Tuple[str, str]:
"""Get old and new config for dry-run"""
# Read current config
if target_file == None:
content = ""
Expand All @@ -80,10 +80,10 @@ def preview_aliases(self, target_file: Optional[Path] = None) -> tuple[str, str]
aliases_section += f"# Generated by alix on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
aliases_section += self.export_aliases(self.shell_type)
aliases_section += f"\n{self.ALIX_MARKER_END}\n"

return (old_alix, aliases_section)
def apply_aliases(self, target_file: Optional[Path] = None) -> tuple[bool, str]:

def apply_aliases(self, target_file: Optional[Path] = None) -> Tuple[bool, str]:
"""Apply aliases to shell configuration file"""
if not target_file:
target_file = self.get_target_file()
Expand Down Expand Up @@ -126,7 +126,7 @@ def apply_aliases(self, target_file: Optional[Path] = None) -> tuple[bool, str]:
# NEW METHOD: apply_single_alias
def apply_single_alias(
self, alias: Alias, auto_reload: bool = True
) -> tuple[bool, str]:
) -> Tuple[bool, str]:
"""Apply a single alias immediately to the current shell session"""
target_file = self.get_target_file()

Expand Down Expand Up @@ -192,7 +192,7 @@ def reload_shell_config(self) -> bool:


# NEW METHOD: install_completions
def install_completions(self, script_content: str, shell_type: Optional[ShellType] = None) -> tuple[bool, str]:
def install_completions(self, script_content: str, shell_type: Optional[ShellType] = None) -> Tuple[bool, str]:
"""Install shell completion script and source it from the user's shell config.

For bash and zsh, writes a script under ~/.config/alix/completions and ensures
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dev = [
"black>=23.0.0",
"flake8>=6.0.0",
"freezegun>=1.5.5",
"pytest-asyncio>=0.24.0",
]

[project.scripts]
Expand Down
Loading
Loading