aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2025-05-25 02:03:56 +0800
committerHsiangNianian <i@jyunko.cn>2025-05-25 02:03:56 +0800
commit14918d4d997eac2b5cc8aa4bc279482da62a5f97 (patch)
tree70ba7d151d5070b692d9e856ecc73a10b7c3492e
parent048d776c89a53b624fc64d31e6aa431fb2117e35 (diff)
downloadsoon-14918d4d997eac2b5cc8aa4bc279482da62a5f97.tar.gz
soon-14918d4d997eac2b5cc8aa4bc279482da62a5f97.zip
feat: Add initial project files and CI configuration
- Created CI workflow for continuous integration using GitHub Actions. - Added Python version specification. - Initialized Cargo.toml and Cargo.lock for Rust project dependencies. - Implemented main functionality in Rust with command-line interface using Clap. - Added Python project configuration with Maturin for building and publishing. - Implemented command history prediction feature in Python.
-rw-r--r--.github/workflows/CI.yml168
-rw-r--r--.python-version1
-rw-r--r--Cargo.lock383
-rw-r--r--Cargo.toml16
-rw-r--r--pyproject.toml23
-rw-r--r--soon/__init__.py0
-rw-r--r--soon/__main__.py25
-rw-r--r--src/main.rs213
-rw-r--r--uv.lock66
9 files changed, 895 insertions, 0 deletions
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
new file mode 100644
index 0000000..34d6b29
--- /dev/null
+++ b/.github/workflows/CI.yml
@@ -0,0 +1,168 @@
+# This file is autogenerated by maturin v1.8.2
+# To update, run
+#
+# maturin generate-ci github
+#
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ tags:
+ - '*'
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ linux:
+ runs-on: ${{ matrix.platform.runner }}
+ strategy:
+ matrix:
+ platform:
+ - runner: ubuntu-22.04
+ target: x86_64
+ - runner: ubuntu-22.04
+ target: x86
+ - runner: ubuntu-22.04
+ target: aarch64
+ - runner: ubuntu-22.04
+ target: armv7
+ - runner: ubuntu-22.04
+ target: s390x
+ - runner: ubuntu-22.04
+ target: ppc64le
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build wheels
+ uses: PyO3/maturin-action@v1
+ with:
+ target: ${{ matrix.platform.target }}
+ args: --release --out dist
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
+ manylinux: auto
+ - name: Upload wheels
+ uses: actions/upload-artifact@v4
+ with:
+ name: wheels-linux-${{ matrix.platform.target }}
+ path: dist
+
+ musllinux:
+ runs-on: ${{ matrix.platform.runner }}
+ strategy:
+ matrix:
+ platform:
+ - runner: ubuntu-22.04
+ target: x86_64
+ - runner: ubuntu-22.04
+ target: x86
+ - runner: ubuntu-22.04
+ target: aarch64
+ - runner: ubuntu-22.04
+ target: armv7
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build wheels
+ uses: PyO3/maturin-action@v1
+ with:
+ target: ${{ matrix.platform.target }}
+ args: --release --out dist
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
+ manylinux: musllinux_1_2
+ - name: Upload wheels
+ uses: actions/upload-artifact@v4
+ with:
+ name: wheels-musllinux-${{ matrix.platform.target }}
+ path: dist
+
+ windows:
+ runs-on: ${{ matrix.platform.runner }}
+ strategy:
+ matrix:
+ platform:
+ - runner: windows-latest
+ target: x64
+ - runner: windows-latest
+ target: x86
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build wheels
+ uses: PyO3/maturin-action@v1
+ with:
+ target: ${{ matrix.platform.target }}
+ args: --release --out dist
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
+ - name: Upload wheels
+ uses: actions/upload-artifact@v4
+ with:
+ name: wheels-windows-${{ matrix.platform.target }}
+ path: dist
+
+ macos:
+ runs-on: ${{ matrix.platform.runner }}
+ strategy:
+ matrix:
+ platform:
+ - runner: macos-13
+ target: x86_64
+ - runner: macos-14
+ target: aarch64
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build wheels
+ uses: PyO3/maturin-action@v1
+ with:
+ target: ${{ matrix.platform.target }}
+ args: --release --out dist
+ sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
+ - name: Upload wheels
+ uses: actions/upload-artifact@v4
+ with:
+ name: wheels-macos-${{ matrix.platform.target }}
+ path: dist
+
+ sdist:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Build sdist
+ uses: PyO3/maturin-action@v1
+ with:
+ command: sdist
+ args: --out dist
+ - name: Upload sdist
+ uses: actions/upload-artifact@v4
+ with:
+ name: wheels-sdist
+ path: dist
+
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
+ needs: [linux, musllinux, windows, macos, sdist]
+ permissions:
+ # Use to sign the release artifacts
+ id-token: write
+ # Used to upload release artifacts
+ contents: write
+ # Used to generate artifact attestation
+ attestations: write
+ steps:
+ - uses: actions/download-artifact@v4
+ - name: Generate artifact attestation
+ uses: actions/attest-build-provenance@v1
+ with:
+ subject-path: 'wheels-*/*'
+ - name: Publish to PyPI
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
+ uses: PyO3/maturin-action@v1
+ env:
+ MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
+ with:
+ command: upload
+ args: --non-interactive --skip-existing wheels-*/*
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..c8cfe39
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.10
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..1240bcc
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,383 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "colored"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "counter"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f009fcafa949dc1fc46a762dae84d0c2687d3b550906b633c4979d58d2c6ae52"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "soon"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "colored",
+ "counter",
+ "dirs",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+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",
+]
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..2ec763e
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "soon"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+# 生成 CLI 可执行文件
+[[bin]]
+name = "soon" # CLI 名称与 Python 包名一致
+path = "src/main.rs"
+
+[dependencies]
+clap = { version = "4.0", features = ["derive"] }
+colored = "3.0.0"
+counter = "0.6.0"
+dirs = "6.0.0"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..4b5793b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,23 @@
+[build-system]
+requires = ["maturin>=1.8,<2.0"]
+build-backend = "maturin"
+
+[project]
+name = "soon"
+requires-python = ">=3.10"
+classifiers = [
+ "Programming Language :: Rust",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+]
+dynamic = ["version"]
+dependencies = [
+ "rich>=14.0.0",
+]
+
+[tool.maturin]
+bindings = "bin"
+module-name = "soon"
+
+# [project.scripts]
+# soon = "soon:__main__"
diff --git a/soon/__init__.py b/soon/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/soon/__init__.py
diff --git a/soon/__main__.py b/soon/__main__.py
new file mode 100644
index 0000000..113d6a1
--- /dev/null
+++ b/soon/__main__.py
@@ -0,0 +1,25 @@
+import subprocess
+import sys
+from rich.console import Console
+from rich.panel import Panel
+
+def run_cli(*args):
+ result = subprocess.run(
+ ["soon", *args],
+ capture_output=True,
+ text=True
+ )
+ return result
+
+def main():
+ args = sys.argv[1:]
+ result = run_cli(*args)
+ console = Console()
+ if result.returncode == 0:
+ console.print(Panel(result.stdout.strip(), title="Result"))
+ else:
+ console.print(Panel(result.stderr.strip() or "Unknown error", title="Error", style="red"))
+ return result.returncode
+
+if __name__ == "__main__":
+ sys.exit(main()) \ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..a16d18a
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,213 @@
+use clap::{Parser, Subcommand};
+use counter::Counter;
+use std::collections::HashMap;
+use std::env;
+use std::fs::File;
+use std::io::{BufRead, BufReader};
+use std::path::PathBuf;
+use colored::*;
+
+#[derive(Parser, Debug)]
+#[command(name = "soon", about = "Predict your next shell command based on history")]
+struct Cli {
+ #[command(subcommand)]
+ command: Option<Commands>,
+ #[arg(long)]
+ shell: Option<String>,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+ /// Show the most likely next command
+ Now,
+ /// Show most used commands
+ Stats,
+ /// Train prediction (WIP)
+ Learn,
+ /// Display detected current shell
+ Which,
+}
+
+fn detect_shell() -> String {
+ if let Ok(shell) = env::var("SHELL") {
+ let shell = shell.to_lowercase();
+ if shell.contains("zsh") {
+ "zsh".to_string()
+ } else if shell.contains("bash") {
+ "bash".to_string()
+ } else if shell.contains("fish") {
+ "fish".to_string()
+ } else {
+ "unknown".to_string()
+ }
+ } else {
+ "unknown".to_string()
+ }
+}
+
+fn history_path(shell: &str) -> Option<PathBuf> {
+ let home = dirs::home_dir()?;
+ match shell {
+ "bash" => Some(home.join(".bash_history")),
+ "zsh" => Some(home.join(".zsh_history")),
+ "fish" => Some(home.join(".local/share/fish/fish_history")),
+ _ => None,
+ }
+}
+
+#[derive(Debug)]
+struct HistoryItem {
+ cmd: String,
+ path: Option<String>,
+}
+
+fn load_history(shell: &str) -> Vec<HistoryItem> {
+ let path = match history_path(shell) {
+ Some(p) => p,
+ None => return vec![],
+ };
+ let file = match File::open(&path) {
+ Ok(f) => f,
+ Err(_) => return vec![],
+ };
+ let reader = BufReader::new(file);
+
+ let mut result = Vec::new();
+ if shell == "fish" {
+ let mut last_cmd: Option<String> = None;
+ let mut last_path: Option<String> = None;
+ for line in reader.lines().flatten() {
+ if let Some(cmd) = line.strip_prefix("- cmd: ") {
+ last_cmd = Some(cmd.trim().to_string());
+ last_path = None;
+ } else if let Some(path) = line.strip_prefix(" path: ") {
+ last_path = Some(path.trim().to_string());
+ }
+
+ if let Some(cmd) = &last_cmd {
+ if line.starts_with("- cmd: ") || line.is_empty() {
+ result.push(HistoryItem {
+ cmd: cmd.clone(),
+ path: last_path.clone(),
+ });
+ last_cmd = None;
+ last_path = None;
+ }
+ }
+ }
+
+ if let Some(cmd) = last_cmd {
+ result.push(HistoryItem {
+ cmd,
+ path: last_path,
+ });
+ }
+ } else {
+ for line in reader.lines().flatten() {
+ let line = if shell == "zsh" {
+ line.trim_start_matches(|c: char| c == ':' || c.is_digit(10) || c == ';').trim().to_string()
+ } else {
+ line.trim().to_string()
+ };
+ if !line.is_empty() {
+ result.push(HistoryItem { cmd: line, path: None });
+ }
+ }
+ }
+ result
+}
+
+fn weighted_suggestions(history: &[HistoryItem], cwd: &str, shell: &str) -> Option<String> {
+ let dir_name = std::path::Path::new(cwd)
+ .file_name()
+ .and_then(|s| s.to_str())
+ .unwrap_or("");
+ let mut scores: HashMap<&str, f64> = HashMap::new();
+ for (i, item) in history.iter().rev().enumerate() {
+ let mut score = 100.0 - i as f64 * 0.5;
+
+ if let Some(ref p) = item.path {
+ if p == cwd {
+ score *= 2.0;
+ }
+ }
+
+ if !cwd.is_empty() && item.cmd.contains(cwd) {
+ score *= 1.5;
+ }
+
+ if !dir_name.is_empty() && item.cmd.contains(dir_name) {
+ score *= 1.2;
+ }
+ if !shell.is_empty() && item.cmd.contains(shell) {
+ score *= 1.1;
+ }
+ *scores.entry(item.cmd.as_str()).or_insert(0.0) += score;
+ }
+ scores.into_iter().max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).map(|(cmd, _)| cmd.to_string())
+}
+
+fn soon_now(shell: &str) {
+ let history = load_history(shell);
+ if history.is_empty() {
+ eprintln!("{}", format!("⚠️ Failed to load history for {shell}.").red());
+ std::process::exit(1);
+ }
+ let cwd = env::current_dir().unwrap_or_default();
+ let cwd = cwd.to_string_lossy();
+ let suggestion = weighted_suggestions(&history, &cwd, shell);
+ println!("\n{}", "🔮 You might run next:".magenta().bold());
+ if let Some(cmd) = suggestion {
+ println!("{} {}", "👉".green().bold(), cmd.green().bold());
+ } else {
+ println!("{}", "No suggestion found.".yellow());
+ }
+}
+
+fn soon_stats(shell: &str) {
+ let history = load_history(shell);
+ if history.is_empty() {
+ eprintln!("{}", format!("⚠️ Failed to load history for {shell}.").red());
+ std::process::exit(1);
+ }
+ let mut counter = Counter::<&String, i32>::new();
+ for item in &history {
+ counter.update([&item.cmd]);
+ }
+ let mut most_common: Vec<_> = counter.most_common().into_iter().collect();
+ most_common.truncate(10);
+
+ println!("{}", "📊 Top 10 most used commands".bold().cyan());
+ println!("{:<30}{}", "Command".cyan().bold(), "Usage Count".magenta().bold());
+ for (cmd, freq) in most_common {
+ println!("{:<30}{}", cmd, freq);
+ }
+}
+
+fn soon_learn(_shell: &str) {
+ println!("{}", "🧠 [soon learn] feature under development...".yellow());
+}
+
+fn soon_which(shell: &str) {
+ println!("{}", format!("🕵️ Current shell: {shell}").yellow().bold());
+}
+
+fn main() {
+ let cli = Cli::parse();
+ let shell = cli.shell.clone().unwrap_or_else(detect_shell);
+
+ if shell == "unknown" && !matches!(cli.command, Some(Commands::Which)) {
+ eprintln!("{}", "⚠️ Unknown shell. Please specify with --shell.".red());
+ std::process::exit(1);
+ }
+
+ match cli.command {
+ Some(Commands::Now) => soon_now(&shell),
+ Some(Commands::Stats) => soon_stats(&shell),
+ Some(Commands::Learn) => soon_learn(&shell),
+ Some(Commands::Which) => soon_which(&shell),
+ None => {
+ soon_now(&shell);
+ }
+ }
+} \ No newline at end of file
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..f93bfed
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,66 @@
+version = 1
+revision = 1
+requires-python = ">=3.10"
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
+]
+
+[[package]]
+name = "rich"
+version = "14.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
+]
+
+[[package]]
+name = "soon"
+source = { editable = "." }
+dependencies = [
+ { name = "rich" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "rich", specifier = ">=14.0.0" }]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
+]