From 0a7f61d73dc93e0736f7cac82704dbd7df79e5db Mon Sep 17 00:00:00 2001 From: JoseVelazcoH Date: Sun, 28 Jun 2026 16:00:07 -0600 Subject: [PATCH 1/6] chore: remove the python prototype --- prototype/python/pyproject.toml | 19 --- prototype/python/tests/__init__.py | 0 prototype/python/tests/conftest.py | 10 -- prototype/python/tests/test_cli.py | 115 ------------- prototype/python/tests/test_db.py | 192 --------------------- prototype/python/tests/test_logging.py | 60 ------- prototype/python/tests/test_paths.py | 55 ------ prototype/python/tests/test_project.py | 178 -------------------- prototype/python/tests/test_recorder.py | 211 ------------------------ prototype/python/tests/test_session.py | 40 ----- prototype/python/tth/__init__.py | 1 - prototype/python/tth/cli.py | 44 ----- prototype/python/tth/database.py | 111 ------------- prototype/python/tth/logging_config.py | 25 --- prototype/python/tth/project.py | 116 ------------- prototype/python/tth/recorder.py | 106 ------------ prototype/python/tth/schema.py | 49 ------ prototype/python/tth/session.py | 34 ---- 18 files changed, 1366 deletions(-) delete mode 100644 prototype/python/pyproject.toml delete mode 100644 prototype/python/tests/__init__.py delete mode 100644 prototype/python/tests/conftest.py delete mode 100644 prototype/python/tests/test_cli.py delete mode 100644 prototype/python/tests/test_db.py delete mode 100644 prototype/python/tests/test_logging.py delete mode 100644 prototype/python/tests/test_paths.py delete mode 100644 prototype/python/tests/test_project.py delete mode 100644 prototype/python/tests/test_recorder.py delete mode 100644 prototype/python/tests/test_session.py delete mode 100644 prototype/python/tth/__init__.py delete mode 100644 prototype/python/tth/cli.py delete mode 100644 prototype/python/tth/database.py delete mode 100644 prototype/python/tth/logging_config.py delete mode 100644 prototype/python/tth/project.py delete mode 100644 prototype/python/tth/recorder.py delete mode 100644 prototype/python/tth/schema.py delete mode 100644 prototype/python/tth/session.py diff --git a/prototype/python/pyproject.toml b/prototype/python/pyproject.toml deleted file mode 100644 index cd1261e..0000000 --- a/prototype/python/pyproject.toml +++ /dev/null @@ -1,19 +0,0 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project] -name = "thoth" -version = "0.1.0" -requires-python = ">=3.11" -dependencies = [] -# typer and rich will return with the query/display slice - -[project.scripts] -tth = "tth.cli:main" - -[tool.hatch.build.targets.wheel] -packages = ["tth"] - -[tool.pytest.ini_options] -testpaths = ["tests"] diff --git a/prototype/python/tests/__init__.py b/prototype/python/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/prototype/python/tests/conftest.py b/prototype/python/tests/conftest.py deleted file mode 100644 index 15e6490..0000000 --- a/prototype/python/tests/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest -from tth.database import connect, apply_migrations - - -@pytest.fixture -def mem_conn(): - conn = connect() - apply_migrations(conn) - yield conn - conn.close() diff --git a/prototype/python/tests/test_cli.py b/prototype/python/tests/test_cli.py deleted file mode 100644 index f7ef2b2..0000000 --- a/prototype/python/tests/test_cli.py +++ /dev/null @@ -1,115 +0,0 @@ -import os -import sqlite3 -import sys - -import pytest - -from tth.database import apply_migrations - - -def _make_conn(db_path): - conn = sqlite3.connect(str(db_path), check_same_thread=False) - conn.row_factory = sqlite3.Row - apply_migrations(conn) - return conn - - -@pytest.fixture -def temp_db(tmp_path, monkeypatch): - db_path = tmp_path / "test.db" - - def _patched_get_connection(path=None): - return _make_conn(db_path) - - monkeypatch.setattr("tth.cli.get_connection", _patched_get_connection) - return db_path - - -def _invoke(argv): - from tth import cli - try: - cli.main(argv) - return 0, None - except SystemExit as e: - return (e.code if e.code is not None else 0), None - except Exception as exc: - return 1, exc - - -def test_typer_not_imported(): - for mod in list(sys.modules.keys()): - if mod == "tth.cli" or mod.startswith("typer") or mod.startswith("click"): - del sys.modules[mod] - import tth.cli # noqa: F401 - assert "typer" not in sys.modules, "typer was imported by tth.cli (hot-path regression)" - assert "click" not in sys.modules, "click was imported by tth.cli (hot-path regression)" - - -def test_cli_exits_0_on_failure(monkeypatch, tmp_path_factory): - log_dir = tmp_path_factory.mktemp("cli_fail_log") - log_file = log_dir / "error.log" - - monkeypatch.setattr( - "tth.cli.get_connection", - lambda: (_ for _ in ()).throw(RuntimeError("db boom")), - ) - monkeypatch.setattr("tth.recorder.ERROR_LOG", log_file) - - code, exc = _invoke(["record", "--cmd", "echo test"]) - assert code == 0, f"Expected exit 0, got {code} (exc={exc})" - - -def test_setup_failure_logs_and_exits_0(monkeypatch, tmp_path_factory): - log_dir = tmp_path_factory.mktemp("setup_fail_logs") - log_file = log_dir / "error.log" - - monkeypatch.setattr( - "tth.cli.get_connection", - lambda: (_ for _ in ()).throw(RuntimeError("disk full")), - ) - monkeypatch.setattr("tth.recorder.ERROR_LOG", log_file) - - code, _ = _invoke(["record", "--cmd", "echo hi"]) - assert code == 0 - assert log_file.exists(), "Error log must be written on setup failure" - assert "disk full" in log_file.read_text() - - -def test_cli_all_six_flags(temp_db): - code, exc = _invoke([ - "record", - "--cmd", "make build", - "--dir", "/home/user/project", - "--exit", "1", - "--duration", "3500", - "--timestamp", "1700001000", - "--tags", '["ci","build"]', - ]) - assert code == 0, f"Exit {code}, exc={exc}" - conn = sqlite3.connect(str(temp_db)) - conn.row_factory = sqlite3.Row - row = conn.execute( - "SELECT exit_code, duration_ms, tags FROM commands WHERE command='make build'" - ).fetchone() - conn.close() - assert row is not None - assert row["exit_code"] == 1 - assert row["duration_ms"] == 3500 - assert row["tags"] == '["ci","build"]' - - -def test_cli_default_dir_is_cwd(temp_db, monkeypatch, tmp_path): - monkeypatch.chdir(tmp_path) - code, exc = _invoke(["record", "--cmd", "pwd"]) - assert code == 0, f"Exit {code}, exc={exc}" - conn = sqlite3.connect(str(temp_db)) - conn.row_factory = sqlite3.Row - row = conn.execute( - "SELECT directory FROM commands WHERE command='pwd'" - ).fetchone() - conn.close() - assert row is not None - expected = os.path.realpath(str(tmp_path)) - assert row["directory"] == expected, ( - f"Expected directory '{expected}', got '{row['directory']}'" - ) diff --git a/prototype/python/tests/test_db.py b/prototype/python/tests/test_db.py deleted file mode 100644 index e406c5c..0000000 --- a/prototype/python/tests/test_db.py +++ /dev/null @@ -1,192 +0,0 @@ -import pytest -from unittest.mock import patch - -from tth.database import connect, apply_migrations, current_version, fts5_available, MIGRATIONS - - -def _table_names(conn): - rows = conn.execute( - "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" - ).fetchall() - return {row[0] for row in rows} - - -def test_schema_created_from_empty(): - conn = connect() - apply_migrations(conn) - tables = _table_names(conn) - assert {"commands", "sessions", "projects", "schema_version"}.issubset(tables) - conn.close() - - -def test_idempotent_rerun(mem_conn): - apply_migrations(mem_conn) - rows = mem_conn.execute("SELECT COUNT(*) FROM schema_version").fetchone()[0] - assert rows == len([v for v, _ in MIGRATIONS]) - - -def test_pending_migrations_applied(): - conn = connect() - dummy_v1 = "CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL);" - dummy_v2 = "CREATE TABLE IF NOT EXISTS _dummy_v2 (x INTEGER);" - fake_migrations = [(1, dummy_v1), (2, dummy_v2)] - with patch("tth.database.MIGRATIONS", fake_migrations): - apply_migrations(conn) - assert current_version(conn) == 2 - conn.close() - - -def test_fts5_insert_sync(mem_conn): - if not fts5_available(mem_conn): - pytest.skip("FTS5 not available") - mem_conn.execute( - "INSERT INTO commands(command, directory, project, session_id, timestamp, exit_code, duration_ms, tags) " - "VALUES('git status', '/home/user/app', 'app', 'sess-1', 1700000000, 0, 10, '[]')" - ) - mem_conn.commit() - row = mem_conn.execute( - "SELECT rowid FROM commands_fts WHERE commands_fts MATCH 'git'" - ).fetchone() - assert row is not None - - -def test_fts5_delete_sync(mem_conn): - if not fts5_available(mem_conn): - pytest.skip("FTS5 not available") - mem_conn.execute( - "INSERT INTO commands(command, directory, project, session_id, timestamp, exit_code, duration_ms, tags) " - "VALUES('unique-cmd-xyz', '/home/user/app', 'app', 'sess-2', 1700000001, 0, 5, '[]')" - ) - mem_conn.commit() - cmd_id = mem_conn.execute( - "SELECT id FROM commands WHERE command='unique-cmd-xyz'" - ).fetchone()[0] - mem_conn.execute("DELETE FROM commands WHERE id=?", (cmd_id,)) - mem_conn.commit() - row = mem_conn.execute( - "SELECT rowid FROM commands_fts WHERE commands_fts MATCH '\"unique-cmd-xyz\"'" - ).fetchone() - assert row is None - - -def test_fts5_unavailable_no_raise(): - conn = connect() - with patch("tth.database.fts5_available", return_value=False): - apply_migrations(conn) - conn.close() - - -def test_fts5_skip_does_not_record_version(): - conn = connect() - with patch("tth.database.fts5_available", return_value=False): - apply_migrations(conn) - ver = current_version(conn) - assert ver == 1, f"Expected version 1 after FTS5 skip, got {ver}" - conn.close() - - -def test_fts5_skip_then_available_applies_migration(): - conn = connect() - with patch("tth.database.fts5_available", return_value=False): - apply_migrations(conn) - assert current_version(conn) == 1 - - apply_migrations(conn) - if not fts5_available(conn): - import pytest - pytest.skip("FTS5 not available in this build") - assert current_version(conn) == 2 - tables = _table_names(conn) - assert "commands_fts" in tables, "commands_fts not found after FTS5 migration" - conn.close() - - -def test_migration_is_atomic(): - conn = connect() - - valid_v1 = ( - "CREATE TABLE IF NOT EXISTS schema_version " - "(version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL);" - ) - bad_v2 = ( - "CREATE TABLE IF NOT EXISTS partial_table (x INTEGER);" - "THIS IS NOT VALID SQL;" - ) - fake_migrations = [(1, valid_v1), (2, bad_v2)] - - with patch("tth.database.MIGRATIONS", fake_migrations): - try: - apply_migrations(conn) - except Exception: - pass - - ver = current_version(conn) - assert ver != 2, f"schema_version advanced to {ver} despite bad SQL - migration is not atomic" - - tables = _table_names(conn) - assert "partial_table" not in tables, "partial_table exists after failed migration - DDL was not rolled back" - - conn.close() - - -def test_trigger_with_case_end_splits_correctly(): - conn = connect() - - apply_migrations(conn) - - trigger_sql = """ -CREATE TABLE IF NOT EXISTS audit_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - label TEXT NOT NULL -); - -CREATE TRIGGER IF NOT EXISTS commands_audit AFTER INSERT ON commands BEGIN - INSERT INTO audit_log(label) - VALUES( - CASE - WHEN new.exit_code = 0 THEN 'ok' - ELSE 'fail' - END - ); -END; -""" - from unittest.mock import patch - fake_migrations = list(MIGRATIONS) + [(99, trigger_sql)] - with patch("tth.database.MIGRATIONS", fake_migrations): - apply_migrations(conn) - - row = conn.execute( - "SELECT name FROM sqlite_master WHERE type='trigger' AND name='commands_audit'" - ).fetchone() - assert row is not None, "trigger commands_audit was not created - CASE END split the body" - - conn.execute( - "INSERT INTO commands(command, directory, project, session_id, timestamp, exit_code, duration_ms, tags) " - "VALUES('ls', '/tmp', 'p', 'sid', 1700000000, 0, 1, '[]')" - ) - conn.commit() - audit = conn.execute("SELECT label FROM audit_log").fetchone() - assert audit is not None and audit[0] == "ok" - - conn.close() - - -def test_string_literal_with_semicolon_begin_and_comment(): - conn = connect() - apply_migrations(conn) - - tricky_value = "hello; BEGIN -- not a comment" - literal_sql = ( - "CREATE TABLE IF NOT EXISTS string_test (val TEXT NOT NULL);\n" - f"INSERT INTO string_test(val) VALUES('{tricky_value}');\n" - ) - from unittest.mock import patch - fake_migrations = list(MIGRATIONS) + [(98, literal_sql)] - with patch("tth.database.MIGRATIONS", fake_migrations): - apply_migrations(conn) - - row = conn.execute("SELECT val FROM string_test").fetchone() - assert row is not None, "string_test row was not inserted" - assert row[0] == tricky_value, f"value was mangled: {row[0]!r}" - - conn.close() diff --git a/prototype/python/tests/test_logging.py b/prototype/python/tests/test_logging.py deleted file mode 100644 index 69db0dd..0000000 --- a/prototype/python/tests/test_logging.py +++ /dev/null @@ -1,60 +0,0 @@ -import logging -from unittest.mock import patch - - -def test_fts5_warning_does_not_reach_stderr(tmp_path, monkeypatch, capsys): - import tth.database as db_mod - - log_file = tmp_path / "error.log" - monkeypatch.setenv("THOTH_ERROR_LOG", str(log_file)) - - from tth import logging_config - logging_config.setup(log_file) - - with patch.object(db_mod, "fts5_available", return_value=False): - from tth.database import connect, apply_migrations - conn = connect() - apply_migrations(conn) - conn.close() - - captured = capsys.readouterr() - assert "FTS5" not in captured.err, ( - f"FTS5 warning leaked to stderr: {captured.err!r}" - ) - - -def test_warning_goes_to_log_file(tmp_path): - log_file = tmp_path / "warn.log" - from tth import logging_config - logging_config.setup(log_file) - - logger = logging.getLogger("tth.test_warning_routing") - logger.warning("sentinel-warning-xyz") - - for h in logging.getLogger("tth").handlers: - h.flush() - - assert log_file.exists(), "Log file was not created" - content = log_file.read_text() - assert "sentinel-warning-xyz" in content, f"Warning not in log: {content!r}" - - -def test_log_rotation_bounds_file_size(tmp_path): - log_file = tmp_path / "rotate.log" - from tth import logging_config - - max_bytes = 10_000 - logging_config.setup(log_file, max_bytes=max_bytes, backup_count=2) - - logger = logging.getLogger("tth.rotation_test") - line = "x" * 200 - for _ in range(200): - logger.warning(line) - - for h in logging.getLogger("tth").handlers: - h.flush() - - assert log_file.exists() - assert log_file.stat().st_size <= max_bytes * 2, ( - f"Log file too large: {log_file.stat().st_size}" - ) diff --git a/prototype/python/tests/test_paths.py b/prototype/python/tests/test_paths.py deleted file mode 100644 index 2a15507..0000000 --- a/prototype/python/tests/test_paths.py +++ /dev/null @@ -1,55 +0,0 @@ -from pathlib import Path - - -def test_thoth_db_env_override(tmp_path, monkeypatch): - from tth import database - - custom = str(tmp_path / "custom.db") - monkeypatch.setenv("THOTH_DB", custom) - resolved = database._resolve_db_path() - assert str(resolved) == custom - - -def test_thoth_db_xdg_data_home(tmp_path, monkeypatch): - from tth import database - - monkeypatch.delenv("THOTH_DB", raising=False) - monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg")) - resolved = database._resolve_db_path() - assert resolved == tmp_path / "xdg" / "thoth" / "history.db" - - -def test_thoth_db_fallback(monkeypatch): - from tth import database - - monkeypatch.delenv("THOTH_DB", raising=False) - monkeypatch.delenv("XDG_DATA_HOME", raising=False) - resolved = database._resolve_db_path() - assert resolved == Path("~/.local/share/thoth/history.db").expanduser() - - -def test_thoth_error_log_env_override(tmp_path, monkeypatch): - from tth import recorder - - custom = str(tmp_path / "custom.log") - monkeypatch.setenv("THOTH_ERROR_LOG", custom) - resolved = recorder._resolve_error_log() - assert str(resolved) == custom - - -def test_thoth_error_log_xdg(tmp_path, monkeypatch): - from tth import recorder - - monkeypatch.delenv("THOTH_ERROR_LOG", raising=False) - monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg")) - resolved = recorder._resolve_error_log() - assert resolved == tmp_path / "xdg" / "thoth" / "error.log" - - -def test_thoth_error_log_fallback(monkeypatch): - from tth import recorder - - monkeypatch.delenv("THOTH_ERROR_LOG", raising=False) - monkeypatch.delenv("XDG_DATA_HOME", raising=False) - resolved = recorder._resolve_error_log() - assert resolved == Path("~/.local/share/thoth/error.log").expanduser() diff --git a/prototype/python/tests/test_project.py b/prototype/python/tests/test_project.py deleted file mode 100644 index 6915ada..0000000 --- a/prototype/python/tests/test_project.py +++ /dev/null @@ -1,178 +0,0 @@ -import json -import time -from unittest.mock import patch - - -from tth.project import infer_project, _walk_markers - - -def test_git_marker(tmp_path): - project_dir = tmp_path / "my-app" - project_dir.mkdir() - (project_dir / ".git").mkdir() - result = _walk_markers(str(project_dir)) - assert result == "my-app" - - -def test_package_json_over_pyproject(tmp_path): - project_dir = tmp_path / "frontend" - project_dir.mkdir() - (project_dir / "package.json").write_text(json.dumps({"name": "frontend-app"})) - (project_dir / "pyproject.toml").write_text('[project]\nname = "backend"\n') - result = _walk_markers(str(project_dir)) - assert result == "frontend-app" - - -def test_pyproject_project_name(tmp_path): - project_dir = tmp_path / "mylib" - project_dir.mkdir() - (project_dir / "pyproject.toml").write_text('[project]\nname = "foo"\n') - result = _walk_markers(str(project_dir)) - assert result == "foo" - - -def test_pyproject_poetry_name(tmp_path): - project_dir = tmp_path / "poetic" - project_dir.mkdir() - (project_dir / "pyproject.toml").write_text('[tool.poetry]\nname = "bar"\n') - result = _walk_markers(str(project_dir)) - assert result == "bar" - - -def test_cargo_toml(tmp_path): - project_dir = tmp_path / "rustproj" - project_dir.mkdir() - (project_dir / "Cargo.toml").write_text('[package]\nname = "mycrate"\n') - result = _walk_markers(str(project_dir)) - assert result == "mycrate" - - -def test_go_mod(tmp_path): - project_dir = tmp_path / "goprj" - project_dir.mkdir() - (project_dir / "go.mod").write_text("module github.com/org/mylib\n\ngo 1.21\n") - result = _walk_markers(str(project_dir)) - assert result == "mylib" - - -def test_docker_compose(tmp_path): - project_dir = tmp_path / "myservice" - project_dir.mkdir() - (project_dir / "docker-compose.yml").write_text("version: '3'\n") - result = _walk_markers(str(project_dir)) - assert result == "myservice" - - -def test_fallback_ungrouped(tmp_path): - project_dir = tmp_path / "emptyfolder" - project_dir.mkdir() - result = _walk_markers(str(project_dir)) - assert result == "ungrouped" - - -def test_depth_cap(tmp_path): - deep = tmp_path - for i in range(25): - deep = deep / f"level{i}" - deep.mkdir() - result = _walk_markers(str(deep)) - assert result == "ungrouped" - - -def test_cache_hit(mem_conn, tmp_path): - project_dir = str(tmp_path / "cached-app") - now = int(time.time()) - one_hour_ago = now - 3600 - mem_conn.execute( - "INSERT INTO projects(path, name, last_seen, command_count) VALUES(?, ?, ?, ?)", - (project_dir, "cached-app", one_hour_ago, 0), - ) - mem_conn.commit() - - with patch("tth.project._walk_markers", side_effect=RuntimeError("should not walk")): - result = infer_project(project_dir, mem_conn) - - assert result == "cached-app" - - -def test_cache_miss_stale(mem_conn, tmp_path): - project_dir = tmp_path / "stale-app" - project_dir.mkdir() - (project_dir / "pyproject.toml").write_text('[project]\nname = "fresh-name"\n') - - now = int(time.time()) - stale_time = now - (25 * 3600) - mem_conn.execute( - "INSERT INTO projects(path, name, last_seen, command_count) VALUES(?, ?, ?, ?)", - (str(project_dir), "old-name", stale_time, 0), - ) - mem_conn.commit() - - result = infer_project(str(project_dir), mem_conn) - assert result == "fresh-name" - - -def test_stale_cache_triggers_rewalk(mem_conn, tmp_path): - import time - - from tth.project import infer_project - - project_dir = tmp_path / "rewalk-app" - project_dir.mkdir() - (project_dir / "pyproject.toml").write_text('[project]\nname = "rewalk-app"\n') - - now = int(time.time()) - stale_time = now - (25 * 3600) - - mem_conn.execute( - "INSERT INTO projects(path, name, last_seen, command_count) VALUES(?, ?, ?, ?)", - (str(project_dir), "old-name", stale_time, 0), - ) - mem_conn.commit() - - result = infer_project(str(project_dir), mem_conn) - assert result == "rewalk-app", f"Expected 'rewalk-app' from re-walk, got '{result}'" - - -def test_recorder_upsert_increments_command_count(mem_conn, tmp_path): - import time - - from tth.recorder import _record_inner - - project_dir = tmp_path / "counted-app" - project_dir.mkdir() - (project_dir / "pyproject.toml").write_text('[project]\nname = "counted-app"\n') - - now = int(time.time()) - stale_time = now - (25 * 3600) - - mem_conn.execute( - "INSERT INTO projects(path, name, last_seen, command_count) VALUES(?, ?, ?, ?)", - (str(project_dir), "counted-app", stale_time, 5), - ) - mem_conn.commit() - - _record_inner( - command="ls", - directory=str(project_dir), - exit_code=0, - duration_ms=1, - timestamp=now, - tags_json="[]", - conn=mem_conn, - ) - - row = mem_conn.execute( - "SELECT command_count FROM projects WHERE path=?", (str(project_dir),) - ).fetchone() - assert row is not None - count = row[0] - assert count >= 6, f"command_count was reset to {count}; expected >= 6" - - -def test_malformed_pyproject_continues(tmp_path): - project_dir = tmp_path / "broken" - project_dir.mkdir() - (project_dir / "pyproject.toml").write_text("this is not toml [[[\n") - result = _walk_markers(str(project_dir)) - assert result == "ungrouped" diff --git a/prototype/python/tests/test_recorder.py b/prototype/python/tests/test_recorder.py deleted file mode 100644 index 475a648..0000000 --- a/prototype/python/tests/test_recorder.py +++ /dev/null @@ -1,211 +0,0 @@ -import sqlite3 -import time -from unittest.mock import patch - - -from tth.recorder import record, _record_inner, _normalize_tags - - -def _insert_project(conn, path, name="app", count=0): - now = int(time.time()) - conn.execute( - "INSERT OR REPLACE INTO projects(path, name, last_seen, command_count) VALUES(?,?,?,?)", - (path, name, now, count), - ) - conn.commit() - - -def test_successful_record(mem_conn, tmp_path): - app_dir = tmp_path / "my-app" - app_dir.mkdir() - (app_dir / ".git").mkdir() - t0 = int(time.time()) - _record_inner("git status", str(app_dir), 0, 120, t0, "[]", mem_conn) - row = mem_conn.execute( - "SELECT command, directory, exit_code, duration_ms, tags, project, session_id " - "FROM commands WHERE command='git status'" - ).fetchone() - assert row is not None - assert row["directory"] == str(app_dir) - assert row["exit_code"] == 0 - assert row["duration_ms"] == 120 - assert row["tags"] == "[]" - assert row["project"] is not None - assert row["session_id"] is not None - - -def test_projects_upsert_increments_count(mem_conn, tmp_path): - app_dir = tmp_path / "proj" - app_dir.mkdir() - _insert_project(mem_conn, str(app_dir), count=5) - t0 = int(time.time()) - _record_inner("ls", str(app_dir), 0, 5, t0, "[]", mem_conn) - row = mem_conn.execute( - "SELECT command_count FROM projects WHERE path=?", (str(app_dir),) - ).fetchone() - assert row["command_count"] == 6 - - -def test_sessions_updated_after_record(mem_conn, tmp_path): - t0 = int(time.time()) - _record_inner("pwd", str(tmp_path), 0, 2, t0, "[]", mem_conn) - row = mem_conn.execute( - "SELECT ended_at, command_count FROM sessions" - ).fetchone() - assert row is not None - assert row["ended_at"] == t0 - assert row["command_count"] >= 1 - - -def test_exception_logged_not_raised(mem_conn, tmp_path, tmp_path_factory): - log_dir = tmp_path_factory.mktemp("logs") - log_file = log_dir / "error.log" - t0 = int(time.time()) - - with patch("tth.recorder._record_inner", side_effect=Exception("boom")): - with patch("tth.recorder.ERROR_LOG", log_file): - record("cmd", str(tmp_path), 0, 0, t0, "[]", mem_conn) - - assert log_file.exists() - assert "boom" in log_file.read_text() - - -def test_sqlite_lock_both_fail_logs(mem_conn, tmp_path, tmp_path_factory): - log_dir = tmp_path_factory.mktemp("logs2") - log_file = log_dir / "error.log" - t0 = int(time.time()) - - with patch( - "tth.recorder._record_inner", - side_effect=sqlite3.OperationalError("database is locked"), - ): - with patch("tth.recorder.ERROR_LOG", log_file): - record("cmd", str(tmp_path), 0, 0, t0, "[]", mem_conn) - - assert log_file.exists() - content = log_file.read_text() - assert content, "Error log must not be empty when both attempts fail" - assert "locked" in content, f"Expected error text in log, got: {content!r}" - - -def test_normalize_tags_valid(): - assert _normalize_tags('["a","b"]') == '["a","b"]' - - -def test_normalize_tags_empty_string(): - assert _normalize_tags("") == "[]" - - -def test_normalize_tags_invalid(): - assert _normalize_tags("not-json") == "[]" - - -def test_normalize_tags_none(): - assert _normalize_tags(None) == "[]" # type: ignore[arg-type] - - -def test_normalize_tags_integer_elements_rejected(): - assert _normalize_tags("[1, 2]") == "[]" - - -def test_normalize_tags_nested_element_rejected(): - assert _normalize_tags('["a", {"b": 1}]') == "[]" - - -def test_normalize_tags_valid_strings_preserved_verbatim(): - raw = '["a","b"]' - assert _normalize_tags(raw) == raw - - -def test_retry_runs_on_clean_transaction(mem_conn, tmp_path): - t0 = int(time.time()) - call_count = 0 - original_record_inner = _record_inner - - def _failing_first_then_ok(command, directory, exit_code, duration_ms, timestamp, tags_json, conn): - nonlocal call_count - call_count += 1 - if call_count == 1: - raise sqlite3.OperationalError("database is locked") - original_record_inner(command, directory, exit_code, duration_ms, timestamp, tags_json, conn) - - with patch("tth.recorder._record_inner", side_effect=_failing_first_then_ok): - record("retry-cmd", str(tmp_path), 0, 0, t0, "[]", mem_conn) - - rows = mem_conn.execute( - "SELECT COUNT(*) FROM commands WHERE command='retry-cmd'" - ).fetchone()[0] - assert rows == 1, f"Expected 1 row, got {rows}" - - assert call_count == 2, f"Expected 2 calls (original + retry), got {call_count}" - - -class _FailOnCommandInsert: - def __init__(self, real_conn, *, fail_count=1): - self._conn = real_conn - self._fail_count = fail_count - self._calls = 0 - - def execute(self, sql, params=()): - if sql.strip().upper().startswith("INSERT INTO COMMANDS"): - self._calls += 1 - if self._calls <= self._fail_count: - raise sqlite3.OperationalError("simulated command insert failure") - return self._conn.execute(sql, params) - - def commit(self): - return self._conn.commit() - - def rollback(self): - return self._conn.rollback() - - def close(self): - return self._conn.close() - - def __getattr__(self, name): - return getattr(self._conn, name) - - -def test_atomicity_command_failure_leaves_no_session(mem_conn, tmp_path): - t0 = int(time.time()) - failing_conn = _FailOnCommandInsert(mem_conn, fail_count=1) - - try: - _record_inner("atomicity-test", str(tmp_path), 0, 0, t0, "[]", failing_conn) - except Exception: - pass - - sessions = mem_conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] - commands = mem_conn.execute( - "SELECT COUNT(*) FROM commands WHERE command='atomicity-test'" - ).fetchone()[0] - assert sessions == 0, f"Orphan session found after rollback (got {sessions})" - assert commands == 0, f"Orphan command found after rollback (got {commands})" - - -def test_atomicity_retry_produces_exactly_one_row(mem_conn, tmp_path, tmp_path_factory): - log_dir = tmp_path_factory.mktemp("atom_logs") - log_file = log_dir / "error.log" - t0 = int(time.time()) - - call_count = 0 - original = _record_inner - - def _fail_once(command, directory, exit_code, duration_ms, timestamp, tags_json, conn): - nonlocal call_count - call_count += 1 - if call_count == 1: - raise sqlite3.OperationalError("simulated lock") - original(command, directory, exit_code, duration_ms, timestamp, tags_json, conn) - - with patch("tth.recorder._record_inner", side_effect=_fail_once): - with patch("tth.recorder.ERROR_LOG", log_file): - record("retry-atomicity", str(tmp_path), 0, 0, t0, "[]", mem_conn) - - sessions = mem_conn.execute("SELECT COUNT(*) FROM sessions").fetchone()[0] - commands = mem_conn.execute( - "SELECT COUNT(*) FROM commands WHERE command='retry-atomicity'" - ).fetchone()[0] - assert sessions == 1, f"Expected 1 session after retry, got {sessions}" - assert commands == 1, f"Expected 1 command after retry, got {commands}" - assert call_count == 2, f"Expected 2 calls, got {call_count}" diff --git a/prototype/python/tests/test_session.py b/prototype/python/tests/test_session.py deleted file mode 100644 index 1a3ebb8..0000000 --- a/prototype/python/tests/test_session.py +++ /dev/null @@ -1,40 +0,0 @@ -import time -import uuid - - -from tth.session import get_or_create - - -def test_first_ever_creates_session(mem_conn): - t0 = int(time.time()) - sid = get_or_create("my-app", t0, mem_conn) - assert sid is not None - uuid.UUID(sid) - row = mem_conn.execute( - "SELECT session_id FROM sessions WHERE session_id=?", (sid,) - ).fetchone() - assert row is not None - - -def test_reuse_within_gap_same_project(mem_conn): - t0 = int(time.time()) - (10 * 60) - t1 = int(time.time()) - sid0 = get_or_create("my-app", t0, mem_conn) - sid1 = get_or_create("my-app", t1, mem_conn) - assert sid0 == sid1 - - -def test_new_session_on_gap_over_30min(mem_conn): - t0 = int(time.time()) - (31 * 60) - t1 = int(time.time()) - sid0 = get_or_create("my-app", t0, mem_conn) - sid1 = get_or_create("my-app", t1, mem_conn) - assert sid0 != sid1 - - -def test_new_session_on_project_change(mem_conn): - t0 = int(time.time()) - (5 * 60) - t1 = int(time.time()) - sid0 = get_or_create("my-app", t0, mem_conn) - sid1 = get_or_create("other-lib", t1, mem_conn) - assert sid0 != sid1 diff --git a/prototype/python/tth/__init__.py b/prototype/python/tth/__init__.py deleted file mode 100644 index 3dc1f76..0000000 --- a/prototype/python/tth/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.1.0" diff --git a/prototype/python/tth/cli.py b/prototype/python/tth/cli.py deleted file mode 100644 index bde0136..0000000 --- a/prototype/python/tth/cli.py +++ /dev/null @@ -1,44 +0,0 @@ -import argparse -import os -import sys -import time - -from tth.database import get_connection -from tth.logging_config import setup as _setup_logging -from tth.recorder import record, _log_error, _resolve_error_log - - -def main(argv=None): - parser = argparse.ArgumentParser(prog="tth") - sub = parser.add_subparsers(dest="command") - - rec = sub.add_parser("record", help="Record a shell command into Thoth's history database.") - rec.add_argument("--cmd", required=True, help="The shell command that was executed") - rec.add_argument("--dir", dest="dir_", default=None, help="Working directory (default: cwd)") - rec.add_argument("--exit", dest="exit_", type=int, default=0, help="Exit code of the command") - rec.add_argument("--duration", type=int, default=0, help="Duration in milliseconds") - rec.add_argument("--timestamp", type=int, default=None, help="Unix timestamp (default: now)") - rec.add_argument("--tags", default="[]", help="JSON array of tags") - - args = parser.parse_args(argv) - - _setup_logging(_resolve_error_log()) - - if args.command == "record": - conn = None - try: - if args.dir_ is None: - args.dir_ = os.getcwd() - if args.timestamp is None: - args.timestamp = int(time.time()) - conn = get_connection() - record(args.cmd, args.dir_, args.exit_, args.duration, args.timestamp, args.tags, conn) - except Exception as exc: - _log_error(exc) - finally: - if conn is not None: - try: - conn.close() - except Exception: - pass - sys.exit(0) diff --git a/prototype/python/tth/database.py b/prototype/python/tth/database.py deleted file mode 100644 index e5f5a62..0000000 --- a/prototype/python/tth/database.py +++ /dev/null @@ -1,111 +0,0 @@ -import logging -import os -import sqlite3 -import time -from pathlib import Path - -from tth.schema import SCHEMA_V1, SCHEMA_V2_FTS - -logger = logging.getLogger(__name__) - -BUSY_TIMEOUT_MS = 2000 - -DEFAULT_DB_PATH = Path("~/.local/share/thoth/history.db").expanduser() - - -def _resolve_db_path() -> Path: - override = os.environ.get("THOTH_DB") - if override: - return Path(override) - xdg = os.environ.get("XDG_DATA_HOME") - if xdg: - return Path(xdg) / "thoth" / "history.db" - return Path("~/.local/share/thoth/history.db").expanduser() - - -MIGRATIONS: list[tuple[int, str]] = [ - (1, SCHEMA_V1), - (2, SCHEMA_V2_FTS), -] - - -def connect(db_path: str = ":memory:") -> sqlite3.Connection: - conn = sqlite3.connect(db_path, check_same_thread=False) - conn.row_factory = sqlite3.Row - return conn - - -def get_connection(db_path: Path | None = None) -> sqlite3.Connection: - db_path = Path(db_path) if db_path is not None else _resolve_db_path() - db_path.parent.mkdir(parents=True, exist_ok=True) - conn = sqlite3.connect(str(db_path), check_same_thread=False) - conn.row_factory = sqlite3.Row - conn.execute("PRAGMA journal_mode=WAL") - conn.execute(f"PRAGMA busy_timeout={BUSY_TIMEOUT_MS}") - conn.execute("PRAGMA synchronous=NORMAL") - apply_migrations(conn) - return conn - - -def current_version(conn: sqlite3.Connection) -> int: - try: - row = conn.execute( - "SELECT COALESCE(MAX(version), 0) FROM schema_version" - ).fetchone() - return row[0] if row else 0 - except sqlite3.OperationalError: - return 0 - - -def fts5_available(conn: sqlite3.Connection) -> bool: - try: - conn.execute("CREATE VIRTUAL TABLE temp.__fts_probe USING fts5(x)") - conn.execute("DROP TABLE IF EXISTS temp.__fts_probe") - return True - except sqlite3.OperationalError: - return False - - -def _split_sql(sql: str) -> list[str]: - statements: list[str] = [] - buf = "" - for line in sql.splitlines(keepends=True): - buf += line - if sqlite3.complete_statement(buf): - stmt = buf.strip() - if stmt: - statements.append(stmt) - buf = "" - - remainder = buf.strip() - if remainder: - statements.append(remainder) - - return statements - - -def apply_migrations(conn: sqlite3.Connection) -> None: - ver = current_version(conn) - - for version, sql in MIGRATIONS: - if version <= ver: - continue - - if version == 2 and not fts5_available(conn): - logger.warning("FTS5 not available - skipping FTS migration v%d", version) - continue - - statements = _split_sql(sql) - - conn.execute("BEGIN IMMEDIATE") - try: - for stmt in statements: - conn.execute(stmt) - conn.execute( - "INSERT OR IGNORE INTO schema_version(version, applied_at) VALUES(?, ?)", - (version, int(time.time())), - ) - conn.execute("COMMIT") - except Exception: - conn.execute("ROLLBACK") - raise diff --git a/prototype/python/tth/logging_config.py b/prototype/python/tth/logging_config.py deleted file mode 100644 index 5a40cab..0000000 --- a/prototype/python/tth/logging_config.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging -import logging.handlers -from pathlib import Path - -_DEFAULT_MAX_BYTES = 1_000_000 -_DEFAULT_BACKUP_COUNT = 2 - - -def setup(log_path: Path, max_bytes: int = _DEFAULT_MAX_BYTES, backup_count: int = _DEFAULT_BACKUP_COUNT) -> None: - log_path = Path(log_path) - log_path.parent.mkdir(parents=True, exist_ok=True) - - root = logging.getLogger("tth") - root.handlers.clear() - root.setLevel(logging.WARNING) - - handler = logging.handlers.RotatingFileHandler( - str(log_path), - maxBytes=max_bytes, - backupCount=backup_count, - encoding="utf-8", - ) - handler.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")) - root.addHandler(handler) - root.propagate = False diff --git a/prototype/python/tth/project.py b/prototype/python/tth/project.py deleted file mode 100644 index 2332878..0000000 --- a/prototype/python/tth/project.py +++ /dev/null @@ -1,116 +0,0 @@ -import json -import sqlite3 -import time -import tomllib -from collections.abc import Callable -from pathlib import Path - -_CACHE_TTL_SECONDS = 86400 -_MAX_WALK_DEPTH = 20 - - -def infer_project(cwd: str, conn: sqlite3.Connection) -> str: - now = int(time.time()) - cutoff = now - _CACHE_TTL_SECONDS - - row = conn.execute( - "SELECT name FROM projects WHERE path=? AND last_seen > ?", - (cwd, cutoff), - ).fetchone() - if row: - return row[0] - - return _walk_markers(cwd) or "ungrouped" - - -def _read_nested(data: dict, *keys: str) -> str | None: - node = data - for key in keys: - if not isinstance(node, dict): - return None - node = node.get(key) - return node if isinstance(node, str) and node else None - - -def _load_toml(p: Path) -> dict | None: - try: - return tomllib.loads(p.read_text(encoding="utf-8")) - except Exception: - return None - - -def _load_json(p: Path) -> dict | None: - try: - data = json.loads(p.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else None - except Exception: - return None - - -def _extract_pyproject(p: Path) -> str | None: - data = _load_toml(p) - if data is None: - return None - return _read_nested(data, "project", "name") or _read_nested(data, "tool", "poetry", "name") - - -def _extract_package_json(p: Path) -> str | None: - data = _load_json(p) - return _read_nested(data, "name") if data is not None else None - - -def _extract_cargo(p: Path) -> str | None: - data = _load_toml(p) - return _read_nested(data, "package", "name") if data is not None else None - - -def _extract_gomod(p: Path) -> str | None: - try: - for line in p.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if line.startswith("module "): - module_path = line[len("module "):].strip() - return module_path.rstrip("/").split("/")[-1] or None - return None - except Exception: - return None - - -_MARKER_STRATEGIES: list[tuple[str, Callable[[Path], str | None]]] = [ - (".git", lambda p: p.parent.name), - ("package.json", _extract_package_json), - ("pyproject.toml", _extract_pyproject), - ("Cargo.toml", _extract_cargo), - ("go.mod", _extract_gomod), - ("docker-compose.yml", lambda p: p.parent.name), - ("compose.yml", lambda p: p.parent.name), -] - -_DIR_MARKERS = {".git"} - - -def _walk_markers(cwd: str) -> str: - current = Path(cwd).resolve() - for _ in range(_MAX_WALK_DEPTH): - name = _check_markers(current) - if name: - return name - parent = current.parent - if parent == current: - break - current = parent - return "ungrouped" - - -def _check_markers(directory: Path) -> str | None: - for filename, extractor in _MARKER_STRATEGIES: - candidate = directory / filename - if filename in _DIR_MARKERS: - if candidate.is_dir(): - return extractor(candidate) - else: - if candidate.exists(): - name = extractor(candidate) - if name: - return name - return None diff --git a/prototype/python/tth/recorder.py b/prototype/python/tth/recorder.py deleted file mode 100644 index c4d3fd6..0000000 --- a/prototype/python/tth/recorder.py +++ /dev/null @@ -1,106 +0,0 @@ -import json -import os -import sqlite3 -import traceback -from pathlib import Path - -from tth.project import infer_project -from tth.session import get_or_create - - -def _resolve_error_log() -> Path: - override = os.environ.get("THOTH_ERROR_LOG") - if override: - return Path(override) - xdg = os.environ.get("XDG_DATA_HOME") - if xdg: - return Path(xdg) / "thoth" / "error.log" - return Path("~/.local/share/thoth/error.log").expanduser() - - -ERROR_LOG = _resolve_error_log() - - -def _normalize_tags(tags_json) -> str: - if not tags_json: - return "[]" - try: - parsed = json.loads(tags_json) - if not isinstance(parsed, list) or not all(isinstance(e, str) for e in parsed): - return "[]" - return tags_json - except (json.JSONDecodeError, TypeError, ValueError): - return "[]" - - -def _record_inner( - command: str, - directory: str, - exit_code: int, - duration_ms: int, - timestamp: int, - tags_json: str, - conn: sqlite3.Connection, -) -> None: - tags = _normalize_tags(tags_json) - conn.execute("BEGIN IMMEDIATE") - try: - project = infer_project(directory, conn) - sid = get_or_create(project, timestamp, conn) - - conn.execute( - "INSERT INTO commands(command, directory, project, session_id, timestamp, exit_code, duration_ms, tags) " - "VALUES(?, ?, ?, ?, ?, ?, ?, ?)", - (command, directory, project, sid, timestamp, exit_code, duration_ms, tags), - ) - - conn.execute( - "INSERT INTO projects(path, name, last_seen, command_count) VALUES(?, ?, ?, 1) " - "ON CONFLICT(path) DO UPDATE SET " - "name=excluded.name, last_seen=excluded.last_seen, command_count=command_count + 1", - (directory, project, timestamp), - ) - - conn.execute( - "UPDATE sessions SET ended_at=?, command_count=command_count + 1 WHERE session_id=?", - (timestamp, sid), - ) - - conn.commit() - except Exception: - conn.rollback() - raise - - -def _log_error(exc: Exception) -> None: - try: - ERROR_LOG.parent.mkdir(parents=True, exist_ok=True) - with ERROR_LOG.open("a", encoding="utf-8") as f: - f.write(traceback.format_exc()) - f.write(f"\n{exc}\n") - except Exception: - pass - - -def record( - command: str, - directory: str, - exit_code: int, - duration_ms: int, - timestamp: int, - tags_json: str, - conn: sqlite3.Connection, -) -> None: - try: - _record_inner(command, directory, exit_code, duration_ms, timestamp, tags_json, conn) - except sqlite3.OperationalError: - try: - conn.rollback() - except Exception: - pass - try: - _record_inner(command, directory, exit_code, duration_ms, timestamp, tags_json, conn) - except Exception as exc2: - _log_error(exc2) - except Exception as exc: - _log_error(exc) diff --git a/prototype/python/tth/schema.py b/prototype/python/tth/schema.py deleted file mode 100644 index 56ee0ba..0000000 --- a/prototype/python/tth/schema.py +++ /dev/null @@ -1,49 +0,0 @@ -SCHEMA_V1 = """ -CREATE TABLE IF NOT EXISTS schema_version ( - version INTEGER PRIMARY KEY, - applied_at INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS commands ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - command TEXT NOT NULL, - directory TEXT NOT NULL, - project TEXT NOT NULL, - session_id TEXT NOT NULL, - timestamp INTEGER NOT NULL, - exit_code INTEGER NOT NULL DEFAULT 0, - duration_ms INTEGER NOT NULL DEFAULT 0, - tags TEXT NOT NULL DEFAULT '[]' -); - -CREATE INDEX IF NOT EXISTS idx_commands_session ON commands(session_id); -CREATE INDEX IF NOT EXISTS idx_commands_timestamp ON commands(timestamp); - -CREATE TABLE IF NOT EXISTS sessions ( - session_id TEXT PRIMARY KEY, - project TEXT NOT NULL, - started_at INTEGER NOT NULL, - ended_at INTEGER NOT NULL, - command_count INTEGER NOT NULL DEFAULT 0 -); - -CREATE TABLE IF NOT EXISTS projects ( - path TEXT PRIMARY KEY, - name TEXT NOT NULL, - last_seen INTEGER NOT NULL, - command_count INTEGER NOT NULL DEFAULT 0 -); -""" - -SCHEMA_V2_FTS = """ -CREATE VIRTUAL TABLE IF NOT EXISTS commands_fts - USING fts5(command, content='commands', content_rowid='id'); - -CREATE TRIGGER IF NOT EXISTS commands_ai AFTER INSERT ON commands BEGIN - INSERT INTO commands_fts(rowid, command) VALUES (new.id, new.command); -END; - -CREATE TRIGGER IF NOT EXISTS commands_ad AFTER DELETE ON commands BEGIN - INSERT INTO commands_fts(commands_fts, rowid, command) VALUES ('delete', old.id, old.command); -END; -""" diff --git a/prototype/python/tth/session.py b/prototype/python/tth/session.py deleted file mode 100644 index 34ec903..0000000 --- a/prototype/python/tth/session.py +++ /dev/null @@ -1,34 +0,0 @@ -import sqlite3 -import uuid - -SESSION_GAP_MINUTES = 30 - -_GAP_SECONDS = SESSION_GAP_MINUTES * 60 - - -def get_or_create(project: str, timestamp: int, conn: sqlite3.Connection) -> str: - row = conn.execute( - "SELECT session_id, project, ended_at FROM sessions ORDER BY ended_at DESC LIMIT 1" - ).fetchone() - - create_new = ( - row is None - or (timestamp - row["ended_at"]) > _GAP_SECONDS - or row["project"] != project - ) - - if create_new: - sid = str(uuid.uuid4()) - conn.execute( - "INSERT INTO sessions(session_id, project, started_at, ended_at, command_count) " - "VALUES(?, ?, ?, ?, 0)", - (sid, project, timestamp, timestamp), - ) - else: - sid = row["session_id"] - conn.execute( - "UPDATE sessions SET ended_at=? WHERE session_id=?", - (timestamp, sid), - ) - - return sid From 4e57530f2c274f833c285c62528dbacc8760da37 Mon Sep 17 00:00:00 2001 From: Ray Droplet <262660609+raydroplet@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:20:33 -0300 Subject: [PATCH 2/6] feat(cli): improve theme subcommand --- src/cli.rs | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index c5eaa09..37fd0db 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -299,8 +299,24 @@ pub struct UninstallArgs { #[derive(clap::Args, Debug, Clone)] pub struct ThemeArgs { - #[arg(help = "Theme name to switch to, or 'list' to list available themes")] - pub name: Option, + #[command(subcommand)] + pub action: ThemeAction, +} + +#[derive(clap::Subcommand, Debug, Clone)] +pub enum ThemeAction { + #[command(about = "Preview the given theme")] + Preview, + #[command(about = "List available themes")] + List, + #[command(about = "Sets the given theme")] + Set(ThemeSetArgs), +} + +#[derive(clap::Args, Debug, Clone)] +pub struct ThemeSetArgs { + #[arg(help = "Theme name to set")] + pub name: String, } pub fn run() -> Result<(), crate::error::ThothError> { @@ -692,11 +708,11 @@ pub fn run() -> Result<(), crate::error::ThothError> { Some(Cmd::Theme(args)) => { let cfg = crate::config::load(); let themes_dir = crate::config::resolve_themes_dir(); - match args.name.as_deref() { - None => { + match args.action { + ThemeAction::Preview => { println!("Current theme: {}", cfg.theme.name); } - Some("list") => { + ThemeAction::List => { let current = &cfg.theme.name; println!("Built-in themes:"); for name in crate::theme::builtin_names() { @@ -718,18 +734,18 @@ pub fn run() -> Result<(), crate::error::ThothError> { } } } - Some(name) => { - if !crate::config::theme_exists(name, &themes_dir) { + ThemeAction::Set(a) => { + if !crate::config::theme_exists(&a.name, &themes_dir) { let builtin_list = crate::theme::builtin_names().join(", "); return Err(crate::error::ThothError::Config(format!( "unknown theme '{}'; built-in themes: {}; user themes go in {}", - name, + a.name, builtin_list, themes_dir.display() ))); } - crate::config::write_set("theme.name", name)?; - println!("Theme set to '{}'. Reopen the TUI to see it.", name); + crate::config::write_set("theme.name", &a.name)?; + println!("Theme set to '{}'. Reopen the TUI to see it.", a.name); } } } From 735a2223e40c600d6e21106ea96357b97465971e Mon Sep 17 00:00:00 2001 From: Ray Droplet <262660609+raydroplet@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:12:09 -0300 Subject: [PATCH 3/6] feat(cli): theme preview with colors --- Cargo.lock | 1143 +++++++++++++++++++++++++++++++++++++++++++--------- Cargo.toml | 4 +- src/cli.rs | 90 ++++- 3 files changed, 1032 insertions(+), 205 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d12026..846c817 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -76,7 +76,22 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", ] [[package]] @@ -94,27 +109,72 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] @@ -124,10 +184,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] -name = "cassowary" -version = "0.3.0" +name = "by_address" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "castaway" @@ -140,9 +206,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "shlex", @@ -154,6 +220,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.6.1" @@ -185,7 +257,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -206,16 +278,16 @@ version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ - "crossterm 0.29.0", + "crossterm", "unicode-segmentation", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] name = "compact_str" -version = "0.8.2" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -226,32 +298,44 @@ dependencies = [ ] [[package]] -name = "crossterm" -version = "0.28.1" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "bitflags", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.44", - "signal-hook", - "signal-hook-mio", - "winapi", + "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossterm" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags", + "bitflags 2.13.0", "crossterm_winapi", + "derive_more", "document-features", + "mio", "parking_lot", - "rustix 1.1.4", + "rustix", + "signal-hook", + "signal-hook-mio", "winapi", ] @@ -264,6 +348,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + [[package]] name = "darling" version = "0.23.0" @@ -284,7 +388,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.118", ] [[package]] @@ -295,7 +399,41 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", ] [[package]] @@ -304,6 +442,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "document-features" version = "0.2.12" @@ -332,7 +480,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys", +] + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", ] [[package]] @@ -347,18 +504,57 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "float-cmp" version = "0.10.0" @@ -368,11 +564,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "futures-core" @@ -398,6 +600,28 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -406,7 +630,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -420,9 +644,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", @@ -434,6 +658,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "hashlink" @@ -450,6 +679,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "ident_case" version = "1.0.1" @@ -485,7 +720,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -496,9 +731,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] @@ -511,21 +746,50 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -538,10 +802,13 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "line-clipping" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.13.0", +] [[package]] name = "linux-raw-sys" @@ -572,11 +839,21 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru" -version = "0.12.5" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.17.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix", + "winapi", ] [[package]] @@ -585,6 +862,27 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "1.2.1" @@ -594,7 +892,30 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys 0.61.2", + "windows-sys", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] @@ -613,6 +934,23 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -622,6 +960,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -634,6 +981,39 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -645,23 +1025,111 @@ dependencies = [ ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pest" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pest_meta" +version = "2.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +dependencies = [ + "pest", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] -name = "paste" -version = "1.0.15" +name = "phf_shared" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] [[package]] name = "pin-project-lite" @@ -675,6 +1143,18 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "predicates" version = "3.1.4" @@ -723,31 +1203,132 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ - "bitflags", - "cassowary", - "compact_str", - "crossterm 0.28.1", - "indoc", "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.0", + "compact_str", + "critical-section", + "hashbrown 0.17.1", "itertools", + "kasuari", "lru", - "paste", + "palette", + "serde", "strum", + "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" +dependencies = [ + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", ] [[package]] @@ -756,7 +1337,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.0", ] [[package]] @@ -794,7 +1375,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "bitflags", + "bitflags 2.13.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -803,16 +1384,12 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "0.38.44" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "semver", ] [[package]] @@ -821,18 +1398,18 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.0", "errno", "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "linux-raw-sys", + "windows-sys", ] [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" @@ -846,6 +1423,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -873,7 +1456,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -898,6 +1481,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "2.0.1" @@ -935,6 +1529,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "slab" version = "0.4.12" @@ -961,24 +1561,34 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", - "syn", + "syn 2.0.118", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -999,10 +1609,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", + "rustix", + "windows-sys", +] + +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.0", + "parking_lot", + "rustix", + "signal-hook", + "windows-sys", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", ] [[package]] @@ -1011,13 +1655,75 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.13.0", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix", + "num-derive", + "num-traits", + "ordered-float", + "pest", + "pest_derive", + "phf", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", ] [[package]] @@ -1028,7 +1734,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] @@ -1038,7 +1744,7 @@ dependencies = [ "assert_cmd", "clap", "comfy-table", - "crossterm 0.28.1", + "crossterm", "nucleo-matcher", "predicates", "ratatui", @@ -1047,12 +1753,33 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror", + "thiserror 2.0.18", "toml", "toml_edit", "uuid", ] +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "toml" version = "0.8.23" @@ -1094,6 +1821,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1108,26 +1847,20 @@ checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ "itertools", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] [[package]] name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "utf8parse" @@ -1137,11 +1870,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom", + "atomic", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -1158,6 +1892,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "wait-timeout" version = "0.2.1" @@ -1173,11 +1916,20 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1188,9 +1940,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1198,135 +1950,134 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] [[package]] -name = "winapi" -version = "0.3.9" +name = "wezterm-bidi" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "log", + "wezterm-dynamic", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "wezterm-blob-leases" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "wezterm-color-types" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] [[package]] -name = "windows-link" +name = "wezterm-dynamic" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" dependencies = [ - "windows-targets", + "log", + "ordered-float", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", ] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "wezterm-dynamic-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" dependencies = [ - "windows-link", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "wezterm-input-types" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] [[package]] name = "winnow" @@ -1337,24 +2088,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.118", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0fd4f6a..57e2d60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,8 @@ toml = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" comfy-table = "7" -ratatui = "0.29" -crossterm = "0.28" +ratatui = { version = "0.30", features = ["crossterm"] } +crossterm = "0.29" nucleo-matcher = "0.3" toml_edit = "0.22" regex = "1" diff --git a/src/cli.rs b/src/cli.rs index 37fd0db..65e39d5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,7 @@ use clap::{Parser, Subcommand}; +use crossterm::style::{ResetColor, SetBackgroundColor, SetForegroundColor}; +use ratatui::backend::IntoCrossterm; +use ratatui::style::Color; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; @@ -306,13 +309,19 @@ pub struct ThemeArgs { #[derive(clap::Subcommand, Debug, Clone)] pub enum ThemeAction { #[command(about = "Preview the given theme")] - Preview, + Preview(ThemePreviewArgs), #[command(about = "List available themes")] List, #[command(about = "Sets the given theme")] Set(ThemeSetArgs), } +#[derive(clap::Args, Debug, Clone)] +pub struct ThemePreviewArgs { + #[arg(help = "Theme name to preview")] + pub name: String, +} + #[derive(clap::Args, Debug, Clone)] pub struct ThemeSetArgs { #[arg(help = "Theme name to set")] @@ -709,8 +718,18 @@ pub fn run() -> Result<(), crate::error::ThothError> { let cfg = crate::config::load(); let themes_dir = crate::config::resolve_themes_dir(); match args.action { - ThemeAction::Preview => { - println!("Current theme: {}", cfg.theme.name); + ThemeAction::Preview(a) => { + if !crate::config::theme_exists(&a.name, &themes_dir) { + return unknown_theme_error(&a.name, &themes_dir); + } + if !crate::config::use_color() { + println!("Theme: {}", cfg.theme.name); + println!("Colors are disabled. No preview available."); + return Ok(()); + } + + let theme = crate::config::resolve_theme(&a.name, &themes_dir); + preview_theme_colors(&theme, &a.name)?; } ThemeAction::List => { let current = &cfg.theme.name; @@ -736,13 +755,7 @@ pub fn run() -> Result<(), crate::error::ThothError> { } ThemeAction::Set(a) => { if !crate::config::theme_exists(&a.name, &themes_dir) { - let builtin_list = crate::theme::builtin_names().join(", "); - return Err(crate::error::ThothError::Config(format!( - "unknown theme '{}'; built-in themes: {}; user themes go in {}", - a.name, - builtin_list, - themes_dir.display() - ))); + return unknown_theme_error(&a.name, &themes_dir); } crate::config::write_set("theme.name", &a.name)?; println!("Theme set to '{}'. Reopen the TUI to see it.", a.name); @@ -753,6 +766,63 @@ pub fn run() -> Result<(), crate::error::ThothError> { Ok(()) } +pub fn preview_theme_colors( + theme: &crate::theme::Theme, + theme_name: &str, +) -> Result<(), crate::error::ThothError> { + let print_color_line = |label: &str, color: Color| -> Result<(), crate::error::ThothError> { + let crossterm_color = color.into_crossterm(); + + print!(" {:<14}", label); + print!("{}{} ", SetForegroundColor(crossterm_color), "██"); + println!("{}{}", SetBackgroundColor(crossterm_color), ResetColor,); + Ok(()) + }; + + println!("Theme: {}\n", theme_name); + + print_color_line("selection_bg", theme.selection_bg)?; + print_color_line("selection_fg", theme.selection_fg)?; + print_color_line("accent", theme.accent)?; + print_color_line("dim", theme.dim)?; + print_color_line("border", theme.border)?; + print_color_line("ok", theme.ok)?; + print_color_line("fail", theme.fail)?; + print_color_line("project", theme.project)?; + print_color_line("command", theme.command)?; + print_color_line("header", theme.header)?; + print_color_line("controls", theme.controls)?; + print_color_line("directory", theme.directory)?; + print_color_line("tags", theme.tags)?; + + Ok(()) + + // Theme: {name} + // + // selection_bg ██ #000000 + // selection_fg ██ #FF0000 + // accent ██ #AA0000 + // dim ██ #00bb00 + // border ██ #000000 + // ok ██ #000000 + // fail ██ #003300 + // project ██ #000000 + // command ██ #220000 + // header ██ #000000 + // controls ██ #000099 + // directory ██ #000000 + // tags ██ #909900 +} +fn unknown_theme_error(name: &str, themes_dir: &PathBuf) -> Result<(), crate::error::ThothError> { + let builtin_list = crate::theme::builtin_names().join(", "); + return Err(crate::error::ThothError::Config(format!( + "unknown theme '{}'; built-in themes: {}; user themes go in {}", + name, + builtin_list, + themes_dir.display() + ))); +} + fn which_tth() -> bool { std::env::var_os("PATH") .map(|paths| std::env::split_paths(&paths).any(|dir| dir.join("tth").exists())) From 17632b6db6b1290be09409a214513a6d187628be Mon Sep 17 00:00:00 2001 From: Ray Droplet <262660609+raydroplet@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:59:50 -0300 Subject: [PATCH 4/6] fix(cli): handle clippy warnings + small Cargo.toml tweak --- Cargo.toml | 2 +- src/cli.rs | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 57e2d60..a57bb0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ toml = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1" comfy-table = "7" -ratatui = { version = "0.30", features = ["crossterm"] } +ratatui = { version = "0.30"} crossterm = "0.29" nucleo-matcher = "0.3" toml_edit = "0.22" diff --git a/src/cli.rs b/src/cli.rs index 65e39d5..4870b9a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use clap::{Parser, Subcommand}; use crossterm::style::{ResetColor, SetBackgroundColor, SetForegroundColor}; use ratatui::backend::IntoCrossterm; use ratatui::style::Color; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Parser)] @@ -774,8 +774,14 @@ pub fn preview_theme_colors( let crossterm_color = color.into_crossterm(); print!(" {:<14}", label); - print!("{}{} ", SetForegroundColor(crossterm_color), "██"); - println!("{}{}", SetBackgroundColor(crossterm_color), ResetColor,); + print!("{} ██", SetForegroundColor(crossterm_color)); + println!( + // "{}{} {}", + "{}{}", + SetBackgroundColor(crossterm_color), + ResetColor, + // color + ); Ok(()) }; @@ -813,14 +819,14 @@ pub fn preview_theme_colors( // directory ██ #000000 // tags ██ #909900 } -fn unknown_theme_error(name: &str, themes_dir: &PathBuf) -> Result<(), crate::error::ThothError> { +fn unknown_theme_error(name: &str, themes_dir: &Path) -> Result<(), crate::error::ThothError> { let builtin_list = crate::theme::builtin_names().join(", "); - return Err(crate::error::ThothError::Config(format!( + Err(crate::error::ThothError::Config(format!( "unknown theme '{}'; built-in themes: {}; user themes go in {}", name, builtin_list, themes_dir.display() - ))); + ))) } fn which_tth() -> bool { From f388751eb806084a58251cbfdc1efc3e82082fac Mon Sep 17 00:00:00 2001 From: Ray Droplet <262660609+raydroplet@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:39:57 -0300 Subject: [PATCH 5/6] feat(cli): hex value output and proper NO_COLOR support on theme preview + new cli test --- src/cli.rs | 55 +++++++++++++++++++++++++++++----------------------- tests/cli.rs | 32 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 4870b9a..d4930f8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -722,14 +722,9 @@ pub fn run() -> Result<(), crate::error::ThothError> { if !crate::config::theme_exists(&a.name, &themes_dir) { return unknown_theme_error(&a.name, &themes_dir); } - if !crate::config::use_color() { - println!("Theme: {}", cfg.theme.name); - println!("Colors are disabled. No preview available."); - return Ok(()); - } - + let use_color = crate::config::use_color(); let theme = crate::config::resolve_theme(&a.name, &themes_dir); - preview_theme_colors(&theme, &a.name)?; + preview_theme_colors(&theme, &a.name, use_color)?; } ThemeAction::List => { let current = &cfg.theme.name; @@ -769,6 +764,7 @@ pub fn run() -> Result<(), crate::error::ThothError> { pub fn preview_theme_colors( theme: &crate::theme::Theme, theme_name: &str, + use_color: bool, ) -> Result<(), crate::error::ThothError> { let print_color_line = |label: &str, color: Color| -> Result<(), crate::error::ThothError> { let crossterm_color = color.into_crossterm(); @@ -776,32 +772,41 @@ pub fn preview_theme_colors( print!(" {:<14}", label); print!("{} ██", SetForegroundColor(crossterm_color)); println!( - // "{}{} {}", - "{}{}", + "{}{} {}", SetBackgroundColor(crossterm_color), ResetColor, - // color + color ); Ok(()) }; + let print_no_color_line = |label: &str, color: Color| -> Result<(), crate::error::ThothError> { + print!(" {:<14}", label); + println!(" {}", color); + Ok(()) + }; + println!("Theme: {}\n", theme_name); - print_color_line("selection_bg", theme.selection_bg)?; - print_color_line("selection_fg", theme.selection_fg)?; - print_color_line("accent", theme.accent)?; - print_color_line("dim", theme.dim)?; - print_color_line("border", theme.border)?; - print_color_line("ok", theme.ok)?; - print_color_line("fail", theme.fail)?; - print_color_line("project", theme.project)?; - print_color_line("command", theme.command)?; - print_color_line("header", theme.header)?; - print_color_line("controls", theme.controls)?; - print_color_line("directory", theme.directory)?; - print_color_line("tags", theme.tags)?; + let print_line = if use_color { + print_color_line + } else { + print_no_color_line + }; - Ok(()) + print_line("selection_bg", theme.selection_bg)?; + print_line("selection_fg", theme.selection_fg)?; + print_line("accent", theme.accent)?; + print_line("dim", theme.dim)?; + print_line("border", theme.border)?; + print_line("ok", theme.ok)?; + print_line("fail", theme.fail)?; + print_line("project", theme.project)?; + print_line("command", theme.command)?; + print_line("header", theme.header)?; + print_line("controls", theme.controls)?; + print_line("directory", theme.directory)?; + print_line("tags", theme.tags)?; // Theme: {name} // @@ -818,6 +823,8 @@ pub fn preview_theme_colors( // controls ██ #000099 // directory ██ #000000 // tags ██ #909900 + + Ok(()) } fn unknown_theme_error(name: &str, themes_dir: &Path) -> Result<(), crate::error::ThothError> { let builtin_list = crate::theme::builtin_names().join(", "); diff --git a/tests/cli.rs b/tests/cli.rs index 7d6c1a1..db73926 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -585,3 +585,35 @@ fn doctor_exits_0_and_prints_checklist() { .and(predicate::str::contains("prompt tags visibility")), ); } + +#[test] +fn builtins_theme_preview_contains_fields() { + let theme_list = thoth::theme::builtin_names(); + let field_list = [ + "selection_bg", + "selection_fg", + "accent", + "dim", + "border", + "ok", + "fail", + "project", + "command", + "header", + "controls", + "directory", + "tags", + ]; + + for theme in theme_list { + let output = tth().args(["theme", "preview", theme]).output().unwrap(); + assert!(output.status.success(), "tth theme preview must exit 0"); + let stdout = String::from_utf8(output.stdout).unwrap(); + for field in field_list { + assert!( + stdout.contains(field), + "theme field not present; got:\n{stdout}" + ); + } + } +} From ffc2a2b2742f3b1ca0b4ae7c9ee6b9fc25d7d644 Mon Sep 17 00:00:00 2001 From: JoseVelazcoH Date: Thu, 9 Jul 2026 14:47:32 -0600 Subject: [PATCH 6/6] refactor(cli): remove dead SetBackgroundColor in theme preview The background color was reset in the same println, so it never painted. The swatch renders from the foreground color alone. Also drop the leftover example comment block. --- src/cli.rs | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index d4930f8..5c4faa8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,5 @@ use clap::{Parser, Subcommand}; -use crossterm::style::{ResetColor, SetBackgroundColor, SetForegroundColor}; +use crossterm::style::{ResetColor, SetForegroundColor}; use ratatui::backend::IntoCrossterm; use ratatui::style::Color; use std::path::{Path, PathBuf}; @@ -770,13 +770,8 @@ pub fn preview_theme_colors( let crossterm_color = color.into_crossterm(); print!(" {:<14}", label); - print!("{} ██", SetForegroundColor(crossterm_color)); - println!( - "{}{} {}", - SetBackgroundColor(crossterm_color), - ResetColor, - color - ); + print!("{} ██{}", SetForegroundColor(crossterm_color), ResetColor); + println!(" {}", color); Ok(()) }; @@ -808,22 +803,6 @@ pub fn preview_theme_colors( print_line("directory", theme.directory)?; print_line("tags", theme.tags)?; - // Theme: {name} - // - // selection_bg ██ #000000 - // selection_fg ██ #FF0000 - // accent ██ #AA0000 - // dim ██ #00bb00 - // border ██ #000000 - // ok ██ #000000 - // fail ██ #003300 - // project ██ #000000 - // command ██ #220000 - // header ██ #000000 - // controls ██ #000099 - // directory ██ #000000 - // tags ██ #909900 - Ok(()) } fn unknown_theme_error(name: &str, themes_dir: &Path) -> Result<(), crate::error::ThothError> {