Skip to content
Merged
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ notes = "gh username/notes"
# Options: "google" (default), "ddg", "bing"
default_search = "ddg"

# Stock website provider (optional)
# Options: "yahoo" (default), "finviz", "tradingview", "google", "investing"
stock_provider = "finviz"

# Command history settings (optional)
[history]
enabled = true
Expand Down Expand Up @@ -396,7 +400,7 @@ You can set your default search engine to `http://localhost:8000/?cmd=%s` and us
| `az` | `amzn`, `azn`, `amazon` | Navigate to Amazon or search for products | `az headphones` |
| `rei` | — | Navigate to REI or search for outdoor gear | `rei hiking boots` |
| `schwab` | — | Charles Schwab shortcuts (`billpay`, `orders`, `trade`, `transfer`, `security`, `contact`) | `schwab trade` |
| `stock` | `stocks`, `finance`, `$<ticker>` | Look up stock prices on Yahoo Finance | `stock META` or `$META` |
| `stock` | `stocks`, `finance`, `$<ticker>` | Look up stock prices (Yahoo Finance, Finviz, TradingView, Google Finance, Investing.com) | `stock META` or `stock finviz AAPL` or `$META` |

### Other Services

Expand All @@ -415,7 +419,7 @@ You can set your default search engine to `http://localhost:8000/?cmd=%s` and us

### Special Syntax

- **Stock tickers**: Prefix with `$` → `$AAPL` redirects to Yahoo Finance
- **Stock tickers**: Prefix with `$` → `$AAPL`
- **Twitter profiles**: Prefix with `@` → `tw @username`
- **Instagram profiles**: Prefix with `@` → `ig @username`
- **Threads profiles**: Prefix with `@` → `threads @username`
Expand Down
24 changes: 14 additions & 10 deletions src/bunnylol_command_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ use std::collections::HashMap;
use std::sync::OnceLock;

use crate::commands::bunnylol_command::{BunnylolCommand, BunnylolCommandInfo};
use crate::config::BunnylolConfig;

// Type alias for command handler functions
type CommandHandler = fn(&str) -> String;
// Type alias for command handler functions with config injection
type CommandHandler = fn(&str, Option<&BunnylolConfig>) -> String;

// Global command lookup table, initialized once on first access
static COMMAND_LOOKUP: OnceLock<HashMap<&'static str, CommandHandler>> = OnceLock::new();
Expand All @@ -21,7 +22,7 @@ macro_rules! register_commands {

$(
for alias in <$cmd>::BINDINGS {
map.insert(*alias, <$cmd>::process_args as CommandHandler);
map.insert(*alias, <$cmd>::process_args_with_config as CommandHandler);
}
)+

Expand Down Expand Up @@ -97,15 +98,18 @@ impl BunnylolCommandRegistry {
}

/// Process commands that use special prefixes (like $ for stock tickers)
fn process_prefix_commands(command: &str) -> Option<String> {
fn process_prefix_commands(
command: &str,
config: Option<&BunnylolConfig>,
) -> Option<String> {
use crate::commands::*;

if command.starts_with('$') {
// Don't process bare $ - let it fall through to default search
if command.len() <= 1 {
return None;
}
return Some(StockCommand::process_ticker(command));
return Some(StockCommand::process_ticker(command, config));
}

None
Expand All @@ -120,20 +124,20 @@ impl BunnylolCommandRegistry {
pub fn process_command_with_config(
command: &str,
full_args: &str,
config: Option<&crate::config::BunnylolConfig>,
config: Option<&BunnylolConfig>,
) -> String {
use crate::commands::*;

// Check for prefix commands first (special case)
if let Some(url) = Self::process_prefix_commands(command) {
if let Some(url) = Self::process_prefix_commands(command, config) {
return url;
}

// Initialize lookup table once, then use O(1) HashMap lookup
let lookup = COMMAND_LOOKUP.get_or_init(Self::initialize_command_lookup);

match lookup.get(command) {
Some(handler) => handler(full_args),
Some(handler) => handler(full_args, config),
None => {
// Use configured search engine if provided, otherwise default to Google
if let Some(cfg) = config {
Expand Down Expand Up @@ -183,11 +187,11 @@ mod cache_tests {

// Test GitHub command handler
let gh_handler = lookup.get("gh").expect("GitHub command should exist");
assert_eq!(gh_handler("gh"), GitHubCommand::process_args("gh"));
assert_eq!(gh_handler("gh", None), GitHubCommand::process_args("gh"));

// Test Instagram command handler
let ig_handler = lookup.get("ig").expect("Instagram command should exist");
assert_eq!(ig_handler("ig"), InstagramCommand::process_args("ig"));
assert_eq!(ig_handler("ig", None), InstagramCommand::process_args("ig"));
}

#[test]
Expand Down
10 changes: 10 additions & 0 deletions src/commands/bunnylol_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ pub trait BunnylolCommand {
/// Process the command arguments and return the appropriate URL
fn process_args(args: &str) -> String;

/// Process the command arguments with config support
///
/// The command registry calls this on every command, passing optional config
fn process_args_with_config(
args: &str,
_config: Option<&crate::config::BunnylolConfig>,
) -> String {
Self::process_args(args)
}

/// Get the command portion from the full arguments string
fn get_command_args(args: &str) -> &str {
// Check if args starts with any of the bindings
Expand Down
Loading
Loading