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
5 changes: 4 additions & 1 deletion commando/core/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ def search_command(
all_known = state_manager.get_all_known_commands()

if not headless:
session_history[base_command] = session_history.get(base_command, 0) + 1
val = session_history.get(base_command, 0) + 1
if base_command in session_history:
session_history.pop(base_command)
session_history[base_command] = val
state_manager.save_history()

if base_command in all_known:
Expand Down
68 changes: 57 additions & 11 deletions commando/ui/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@
clear_screen,
pause,
save_json,
get_category_color,
format_badge,
)


def print_dashboard(session_history, pending_imports):
clear_screen()
print(f"{CYAN}{BOLD}=============================================={RESET}")
print(f"{CYAN}{BOLD} Terminal Command Explorer & Tutor {RESET}")
print(f"{CYAN}{BOLD}=============================================={RESET}")
print(f"{CYAN}{BOLD}╔════════════════════════════════════════════════════╗{RESET}")
print(f"{CYAN}{BOLD} Terminal Command Explorer & Tutor {RESET}")
print(f"{CYAN}{BOLD}╚════════════════════════════════════════════════════╝{RESET}\n")

if session_history:
print(
f"{MAGENTA}Recent: {', '.join(list(session_history.keys())[-5:])}{RESET}\n"
)
recent = list(session_history.keys())[-5:]
print(f"{MAGENTA}{BOLD}Recent:{RESET} {', '.join(recent)}\n")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

print(f" {GREEN}[1]{RESET} Explore Random Command")
print(f" {GREEN}[2]{RESET} Explore by Category")
Expand All @@ -44,8 +45,9 @@ def print_dashboard(session_history, pending_imports):
f" {CYAN}[7]{RESET} Review Pending Imports ({len(pending_imports)} waiting)"
)
print(f" {MAGENTA}[8]{RESET} Install Bash Hook")
print(f" {GREEN}[9]{RESET} View Stats & Mastery")
print(f" {RED}[0]{RESET} Exit & Quiz Mode")
print(f"{YELLOW}Or type a command to search (e.g., ls, nano){RESET}\n")
print(f"\n{YELLOW}Or type a command to search (e.g., ls, nano){RESET}\n")


def view_debug_log():
Expand All @@ -64,6 +66,41 @@ def view_debug_log():
pause()



def view_stats_and_mastery(state_manager):
clear_screen()
session_history = state_manager.session_history
if not session_history:
print(f"{YELLOW}No stats available yet. Start exploring commands!{RESET}")
pause()
return

print(f"{MAGENTA}★ User Stats & Mastery ★{RESET}\n")
print(f"Total commands explored: {len(session_history)}")

# Sort history by mastery level (score) descending, then by name
sorted_history = sorted(session_history.items(), key=lambda x: (-x[1], x[0]))

print(f"\n{CYAN}{BOLD}Command Mastery:{RESET}")
for cmd, score in sorted_history:
# Mastery goes up to 5
display_score = min(5, max(0, score))
filled = "█" * display_score
empty = "░" * (5 - display_score)
bar = f"{filled}{empty}"
Comment on lines +81 to +90

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If session_history contains non-integer values (e.g., if the history file was modified or corrupted), sorting with key=lambda x: (-x[1], x[0]) will raise a TypeError due to the unary negation operator - on a non-numeric type. Additionally, multiplying strings by a non-integer score will raise a TypeError. We should safely parse the score to an integer.

    # Sort history by mastery level (score) descending, then by name
    def get_sort_key(item):
        cmd, score = item
        try:
            return -int(score), cmd
        except (TypeError, ValueError):
            return 0, cmd

    sorted_history = sorted(session_history.items(), key=get_sort_key)

    print(f'\n{CYAN}{BOLD}Command Mastery:{RESET}')
    for cmd, raw_score in sorted_history:
        try:
            score = int(raw_score)
        except (TypeError, ValueError):
            score = 0
        # Mastery goes up to 5
        score = min(5, max(0, score))
        filled = '█' * score
        empty = '░' * (5 - score)
        bar = f'{filled}{empty}'


# Color coding the progress bar
if display_score >= 4:
bar_color = GREEN
if 2 <= display_score < 4:
bar_color = YELLOW
if display_score < 2:
bar_color = RED

print(f" {bar_color}[{bar}]{RESET} {BOLD}{cmd}{RESET}")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pause()

def factory_reset(state_manager):
clear_screen()
print(f"{RED}★ Factory Reset ★{RESET}\n")
Expand Down Expand Up @@ -122,7 +159,7 @@ def explore_category(state_manager):
if 0 <= idx < len(categories):
selected_cat = categories[idx]
clear_screen()
print(f"{YELLOW}--- {selected_cat} Commands ---{RESET}\n")
print(f"{YELLOW}--- {format_badge(selected_cat)} Commands ---{RESET}\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The format_badge function ends with a {RESET} ANSI escape sequence. This will reset the terminal color formatting, causing the subsequent text ' Commands ---' to lose its {YELLOW} color and render in the default terminal color. To maintain the yellow color for the entire header, re-apply {YELLOW} immediately after the badge.

Suggested change
print(f"{YELLOW}--- {format_badge(selected_cat)} Commands ---{RESET}\n")
print(f'{YELLOW}--- {format_badge(selected_cat)}{YELLOW} Commands ---{RESET}\n')

for cmd, data in all_known.items():
if data["category"] == selected_cat:
print(f" • {CYAN}{BOLD}{cmd}{RESET}: {data['desc']}")
Expand All @@ -147,7 +184,8 @@ def manage_imports(state_manager):
short_desc = (
(data["desc"][:60] + "...") if len(data["desc"]) > 60 else data["desc"]
)
print(f" • {CYAN}{BOLD}{cmd}{RESET}: {short_desc}")
badge = format_badge(data.get("category", "Custom"))
print(f" • {badge} {CYAN}{BOLD}{cmd}{RESET}: {short_desc}")

print(f"\n{YELLOW}Options:{RESET}")
print(f" - Type a command name to {RED}DELETE{RESET} it from the database.")
Expand Down Expand Up @@ -222,10 +260,15 @@ def run_quiz(state_manager):

if ans == q_cmd:
print(f"{GREEN}Correct!{RESET}\n")
session_history[q_cmd] = min(5, session_history.get(q_cmd, 0) + 1)
val = min(5, session_history.get(q_cmd, 0) + 1)
if q_cmd in session_history:
session_history.pop(q_cmd)
session_history[q_cmd] = val
score += 1
else:
print(f"{RED}Incorrect.{RESET} The answer was '{BOLD}{q_cmd}{RESET}'.\n")
if q_cmd in session_history:
session_history.pop(q_cmd)
session_history[q_cmd] = 0

save_json(HISTORY_FILE, session_history)
Expand Down Expand Up @@ -261,7 +304,7 @@ def review_pending_imports(state_manager):
clear_screen()
data = pending_imports[cmd]
print(f"{CYAN}Command:{RESET} {cmd}")
print(f"Category: [{MAGENTA}{data['category']}{RESET}]")
print(f"Category: {format_badge(data['category'])}")
print(f"Explanation: {data['desc']}")
print(f"Example: {data['example']}")
print(
Expand Down Expand Up @@ -407,6 +450,9 @@ def main_loop(state_manager, search_command_fn, auto_scan_fn):
elif raw_input == "8":
install_bash_hook()
continue
elif raw_input == "9":
view_stats_and_mastery(state_manager)
continue
elif raw_input == "0" or raw_input.lower() == "done":
choice = (
input(
Expand Down
19 changes: 19 additions & 0 deletions commando/utils/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,22 @@ def pause():
input(f"\n{YELLOW}Press Enter to continue...{RESET}")
except (EOFError, KeyboardInterrupt):
print()


def get_category_color(category):
cat_lower = category.lower()
Comment on lines +86 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If category is None or not a string, calling category.lower() will raise an AttributeError. We should add a defensive check to handle non-string or None values gracefully.

def get_category_color(category):
    if not isinstance(category, str):
        return BOLD
    cat_lower = category.lower()

if "file" in cat_lower or "disk" in cat_lower:
return CYAN
if "network" in cat_lower or "web" in cat_lower:
return MAGENTA
if "process" in cat_lower or "system" in cat_lower:
return RED
if "navig" in cat_lower or "search" in cat_lower:
return GREEN
if "text" in cat_lower or "edit" in cat_lower:
return YELLOW
return BOLD

def format_badge(category):
color = get_category_color(category)
return f"[{color}{category}{RESET}]"
Comment on lines +100 to +102

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If category is None, format_badge will display [None]. We should handle None or empty values by defaulting to a fallback string like 'Custom'.

Suggested change
def format_badge(category):
color = get_category_color(category)
return f"[{color}{category}{RESET}]"
def format_badge(category):
category_str = str(category) if category else 'Custom'
color = get_category_color(category_str)
return f'[{color}{category_str}{RESET}]'

Loading