A Productivity Shell for Task Tracking and Time Management
TruShell is an interactive shell environment designed to integrate task tracking and time management tools seamlessly with traditional terminal commands. Built in Rust with a custom expression parser, TruShell extends the Unix philosophy by providing a unified interface where productivity features and system commands coexist naturally.
- Overview
- Architecture
- Installation & Building
- Usage
- Language Features
- Module Reference
- Parsing System
- Command Execution
- Examples
- Development
TruShell is not a replacement shell but rather a productivity layer that bridges the gap between system administration and personal task management. It runs as an interactive REPL (Read-Eval-Print Loop) that:
- Accepts and executes ordinary shell commands (
ls,cd,grep, etc.) - Parses and interprets custom expressions for task and time operations
- Maintains compatibility with existing Unix tools through fallback command execution
- Provides a consistent interface for both system-level and productivity-level operations
- Hybrid Parsing: Intelligently distinguishes between shell commands and custom expressions
- Expression Evaluation: Supports arithmetic, comparisons, variables, and pipelines
- Pipeline Support: Chain operations together using the pipe operator (
|) - Task Integration: Foundation for task tracking and time management (future expansion)
- Graceful Fallback: Executes as system commands if parsing fails
TruShell follows a modular, layered architecture typical of interpreted languages:
┌─────────────────────────────────────────┐
│ Interactive REPL (main.rs) │
│ • User Input Loop │
│ • Command/Expression Routing │
│ • Fallback Execution │
└──────────────┬──────────────────────────┘
│
┌───────┴────────┐
│ │
┌──────▼──────┐ ┌──────▼──────────┐
│ Lexer │ │ Parser │
│ (Tokenize) │ │ (AST Build) │
└─────────────┘ └─────────────────┘
│ │
└───────┬────────┘
│
┌──────▼──────────┐
│ AST Nodes │
│ (Expressions) │
└─────────────────┘
│
┌──────▼──────────────────┐
│ Command Execution │
│ • System Calls │
│ • Process Management │
└─────────────────────────┘
- Separation of Concerns: Lexing, parsing, and execution are distinct phases
- Error Resilience: Parse failures trigger fallback to system command execution
- Minimal Dependencies: Uses only
crosstermfor terminal handling - Extensibility: AST-based design allows easy addition of new expression types
- Rust 1.70+ (MSRV: Edition 2021)
- Cargo (Rust's package manager)
git clone https://github.com/TruFoundation/TruShell.git
cd TruShell
cargo build --releaseThe compiled binary will be located at target/release/trushell.
./target/release/trushellOr directly via Cargo:
cargo runUpon startup, TruShell displays a welcome message and enters a prompt loop:
Welcome to TruShell Native Engine
trushell ❯
exit: Gracefully shut down the shellCtrl+D(EOF): Safely terminate the shell
Execute any command available in your PATH:
trushell ❯ ls -la
trushell ❯ pwd
trushell ❯ echo "Hello, World!"
trushell ❯ cd /tmp
trushell ❯ cd ~
The cd command is handled specially to modify TruShell's working directory (not spawned as a subprocess).
Define variables using let:
trushell ❯ let x = 42
trushell ❯ let name = "Alice"
trushell ❯ let flag = true
Perform arithmetic and logical operations:
trushell ❯ let result = 10 + 5
trushell ❯ let product = 3 * 7
trushell ❯ let ratio = 100 / 4
trushell ❯ let is_big = 42 > 10
trushell ❯ let is_equal = 5 == 5
trushell ❯ let not_empty = "text" != ""
Chain operations together using |:
trushell ❯ ls() | filter { $it.size > 1mb }
TruShell's lexer recognizes the following token categories:
| Token Class | Examples | Purpose |
|---|---|---|
| Keywords | let, true, false |
Language control structures |
| Identifiers | x, $var, _private |
Variable and function names |
| Numbers | 42, 3mb, 100kb |
Numeric literals with optional units |
| Strings | "hello" |
Quoted string literals |
| Flags | -la, --verbose, --help |
Command-line flags (preserved) |
| Operators | +, -, *, /, >, <, ==, != |
Binary operations |
| Delimiters | (), {}, [], ., ,, ; |
Structure and grouping |
| Pipes | | |
Pipeline sequencing |
TruShell supports the following literal types:
pub enum Literal {
Number { value: i64, unit: Option<String> }, // 42, 1mb, 500ms
String(String), // "text"
Boolean(bool), // true, false
}| Operator | Type | Precedence | Example |
|---|---|---|---|
+ |
Addition | Term | 5 + 3 |
- |
Subtraction | Term | 10 - 4 |
* |
Multiplication | Factor | 3 * 4 |
/ |
Division | Factor | 12 / 3 |
> |
Greater Than | Comparison | 5 > 3 |
< |
Less Than | Comparison | 2 < 5 |
>= |
Greater or Equal | Comparison | 5 >= 5 |
<= |
Less or Equal | Comparison | 3 <= 5 |
== |
Equals | Comparison | 5 == 5 |
!= |
Not Equals | Comparison | 3 != 5 |
TruShell follows standard mathematical precedence, evaluated in this order (lowest to highest):
- Comparison Operators (
>,<,>=,<=,==,!=) - Term Operators (
+,-) - Factor Operators (
*,/) - Primary (Literals, Identifiers, Parentheses, Blocks)
Variables are declared with let and referenced with $:
trushell ❯ let count = 10
trushell ❯ let doubled = $count * 2
Variables starting with $ are treated as Variable tokens and can be accessed in expressions.
Code blocks are enclosed in {} and contain semicolon-separated statements:
trushell ❯ let data = { let x = 5; let y = 10; $x + $y }
Purpose: Orchestrates the shell's interactive loop and manages command execution.
- Initializes the shell with a welcome message
- Enters an infinite loop reading user input
- Routes input to the appropriate handler (exit, cd, parse, or fallback)
Key Invariants:
- Reads lines until EOF (
Ctrl+D) or explicitexitcommand - Maintains current working directory for the shell process
- Handles all I/O errors gracefully with user-friendly messages
- Spawns a new process for the given command
- Inherits stdin, stdout, stderr for seamless integration
- Reports execution errors with descriptive messages
Execution Flow:
Command::new(cmd)
└─ .args(args)
└─ .stdin(Stdio::inherit())
└─ .stdout(Stdio::inherit())
└─ .stderr(Stdio::inherit())
└─ .spawn()
└─ .wait()
- Heuristic Detection: Attempts to interpret parsed AST as a CLI invocation
- Use Case: Handles cases where
ls -lais parsed asls - la(subtraction) - Strategy:
- Traverses the AST looking for a chain of subtraction operations
- Extracts the leftmost node as the command name
- Collects remaining identifiers/strings as arguments
- Returns
Some((cmd, args))if the pattern matches, otherwiseNone
Example Transformation:
Input: "ls -la"
Parse: BinaryOp { left: Identifier("ls"), op: Subtract, right: Literal(String("-la")) }
Extract: ("ls", ["-la"])
Execute: ls -la
Purpose: Converts raw user input into an Abstract Syntax Tree (AST) for interpretation.
pub enum Token {
Let, // Variable declaration
Flag(String), // CLI flags (-la, --help)
Identifier(String), // Variable/function names
Number(String), // Numeric literals
StringLiteral(String), // Quoted strings
Boolean(bool), // true/false
Equals, // = assignment
Pipe, // | pipeline
LParen, RParen, // ( )
LBrace, RBrace, // { }
Dot, // . property access
Comma, // , separator
Semicolon, // ; statement terminator
GreaterThan, // >
LessThan, // <
GreaterThanOrEqual, // >=
LessThanOrEqual, // <=
EqualsEquals, // ==
BangEquals, // !=
Plus, // +
Minus, // -
Star, // *
Slash, // /
}pub enum ASTNode {
Let { name: String, value: Box<ASTNode> },
Pipeline { stages: Vec<Box<ASTNode>> },
Command { name: String, args: Vec<ASTNode> },
Block { body: Vec<ASTNode> },
BinaryOp { left: Box<ASTNode>, op: BinaryOperator, right: Box<ASTNode> },
Variable(String),
Literal(Literal),
PropertyAccess { target: Box<ASTNode>, property: String },
Identifier(String),
}Responsibility: Converts a string into a sequence of tokens.
- Whitespace: Skipped entirely
- Identifiers: Start with
a-z,A-Z,_, or$; continue with alphanumerics or_ - Keywords:
let,true,false→ special tokens - Numbers: Digits optionally followed by a unit string (e.g.,
5,100ms,1mb) - Strings: Double-quoted; no escape sequences currently supported
- Flags:
-followed by letters/hyphens (e.g.,-la,--help) - Operators: Single and multi-character (
==,!=,>=,<=)
The lexer recognizes CLI flags intelligently:
if let Some(second) = peek_two_chars_ahead {
if second.is_alphabetic() || second == '-' {
// Lex as a flag (e.g., -la, --verbose)
self.lex_flag()
} else {
// Lex as minus operator (e.g., 5 - 3)
Token::Minus
}
}Effect: ls-la is lexed as a command followed by a flag, not as subtraction.
Responsibility: Converts tokens into an AST using recursive descent parsing.
parse_statement
├─ if let: parse_let_statement
└─ else: parse_pipeline
parse_pipeline
└─ parse_expression (with | separator)
parse_expression
└─ parse_comparison
parse_comparison (>/</>=/<=,==,!=)
└─ parse_term
parse_term (+,-)
└─ parse_factor
parse_factor (*,/)
└─ parse_primary
parse_primary (literals, identifiers, parens, blocks)
└─ parse_identifier_expression (handles ., (, {})
let x = 5
├─ Token::Let
├─ expect identifier → "x"
├─ expect =
└─ parse_expression → Literal(Number(5))
Result: ASTNode::Let {
name: "x",
value: Box::new(Literal(Number(5)))
}ls() | filter { $it.size > 1mb }
├─ parse_expression → Command { name: "ls", args: [] }
├─ Token::Pipe
├─ parse_expression → Command { name: "filter", args: [Block {...}] }
└─ Token::Pipe (none)
Result: ASTNode::Pipeline {
stages: [Command{...}, Command{...}]
}Numbers can include units (e.g., 5mb, 100ms):
pub fn parse_number_literal(raw: &str) -> Result<Literal, ParseError> {
let digits: String = raw.chars().take_while(|ch| ch.is_ascii_digit()).collect();
let unit: String = raw.chars().skip_while(|ch| ch.is_ascii_digit()).collect();
Ok(Literal::Number {
value: digits.parse::<i64>()?,
unit: if unit.is_empty() { None } else { Some(unit) },
})
}Examples:
5→Number { value: 5, unit: None }1mb→Number { value: 1, unit: Some("mb") }100ms→Number { value: 100, unit: Some("ms") }
User Input String
↓
[Lexer::tokenize()]
↓
Token Vector
↓
[Parser::parse_statement()]
↓
ASTNode (Abstract Syntax Tree)
↓
[main.rs execution logic]
↓
Output/Side Effects
Input: "let x = 5 + 3"
Step 1: Tokenization
[Let, Identifier("x"), Equals, Number("5"), Plus, Number("3")]
Step 2: Parsing (Recursive Descent)
parse_statement
└─ parse_let_statement
├─ expect Let → ✓
├─ expect_identifier → "x"
├─ expect Equals → ✓
└─ parse_expression (for "5 + 3")
└─ parse_comparison
└─ parse_term
├─ parse_factor → Literal(5)
├─ detect Plus
├─ parse_factor → Literal(3)
└─ combine: BinaryOp { left: 5, op: Add, right: 3 }
Step 3: AST Result
ASTNode::Let {
name: "x",
value: Box::new(
ASTNode::BinaryOp {
left: Box::new(Literal(Number { value: 5, unit: None })),
op: Add,
right: Box::new(Literal(Number { value: 3, unit: None }))
}
)
}The parser provides descriptive error messages:
pub struct ParseError {
pub message: String,
}Common Errors:
"Expected identifier, found ...""Unexpected token in expression""Unterminated string literal""Unexpected character: '...'"(from lexer)
loop {
1. Read user input
2. Trim whitespace
3. Check for special commands:
├─ "exit" → break loop
└─ "cd ..." → change directory
4. Attempt to parse:
├─ Success → check if it's a probable CLI command
│ ├─ Yes → execute as system command
│ └─ No → print AST (debug mode)
└─ Failure → fallback to system command execution
5. Display output
}
Terminates the shell gracefully:
if trimmed_input == "exit" {
println!("Goodbye!");
break;
}Handled specially without spawning a subprocess:
if trimmed_input.starts_with("cd") {
let parts: Vec<&str> = trimmed_input.split_whitespace().collect();
let new_dir = parts.get(1).copied().unwrap_or(".");
if let Err(e) = std::env::set_current_dir(new_dir) {
eprintln!("trushell: cd: {}: {}", new_dir, e);
}
continue;
}When parsing fails or the AST doesn't match a known pattern, TruShell falls back to executing the input as a system command:
Err(err) => {
eprintln!("Parse error: {}", err);
let parts: Vec<&str> = trimmed_input.split_whitespace().collect();
let command = parts[0];
let args = &parts[1..];
execute_system_command(command, args);
}Result: Most Unix commands work transparently even if parsing fails.
Commands are executed with inherited I/O streams:
Command::new(cmd)
.args(args)
.stdin(Stdio::inherit()) // User input reaches subprocess
.stdout(Stdio::inherit()) // Subprocess output visible
.stderr(Stdio::inherit()) // Errors displayed directly
.spawn()
.wait()trushell ❯ let x = 10
Parsed AST: Let { name: "x", value: ... }
trushell ❯ let y = $x * 2
Parsed AST: Let { name: "y", value: ... }
trushell ❯ let z = 100 / 5
Parsed AST: Let { name: "z", value: ... }trushell ❯ let name = "Alice"
trushell ❯ let greeting = "Hello, " + $name
Parsed AST: Let { name: "greeting", value: ... }trushell ❯ let is_adult = 25 > 18
Parsed AST: Let { name: "is_adult", value:
BinaryOp {
left: 25,
op: GreaterThan,
right: 18
}
}
trushell ❯ let in_range = 50 >= 10trushell ❯ ls -la
total 48
drwxr-xr-x 5 user group 160 Jul 5 12:34 .
drwxr-xr-x 10 user group 320 Jul 4 18:22 ..
-rw-r--r-- 1 user group 1234 Jul 05 12:30 README.md
...
trushell ❯ pwd
/home/user/projects/TruShell
trushell ❯ echo "Building..."
Building...trushell ❯ cd /tmp
trushell ❯ pwd
/tmp
trushell ❯ cd -
trushell ❯ pwd
/home/user/projects/TruShelltrushell ❯ ls() | filter { $it.size > 1mb }
Parsed AST: Pipeline {
stages: [
Command { name: "ls", args: [] },
Command { name: "filter", args: [Block { ... }] }
]
}TruShell/
├── Cargo.toml # Rust project manifest
├── Cargo.lock # Dependency lock file
├── src/
│ ├── main.rs # REPL and command execution (4.3 KB)
│ └── parser.rs # Lexer and parser (19.0 KB)
├── target/ # Compiled binaries (excluded from repo)
├── README.md # This file
└── LICENSE.md # Project license
- crossterm (v0.27): Terminal manipulation and event handling
- Standard Library: All core functionality uses
std
# Build in debug mode
cargo build
# Build optimized release binary
cargo build --release
# Run tests
cargo test
# Run with output
cargo run -- --verboseThe parser module includes unit tests:
#[test]
fn tokenize_basic_lets_and_pipeline() { ... }
#[test]
fn parse_let_statement() { ... }
#[test]
fn parse_pipeline_with_function_block() { ... }Run tests with:
cargo test- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit changes with clear messages
- Add tests for new functionality
- Push to your fork
- Submit a pull request
- Task Management API: Commands to create, list, and complete tasks
- Time Tracking:
time start,time stop,time logcommands - Persistence: SQLite backend for task/time storage
- Configuration:
.trushellrcconfiguration file support - Scripting: Multi-line scripts and batching
- Shell Integration:
.bashrc/.zshrcintegration for seamless use - Custom Functions: User-defined functions with parameters
- History & Completion: Command history and tab completion
- Linux Shell Documentation:
man bash,man sh - Rust Book: https://doc.rust-lang.org/book/
- Crossterm Docs: https://docs.rs/crossterm/
- Unix Philosophy: https://en.wikipedia.org/wiki/Unix_philosophy
TruShell is released under the terms specified in LICENSE.md. See that file for full details.
TruFoundation — Empowering productivity through open-source tooling.
Last Updated: July 5, 2026