Download crates/forge_app/src/terminal_context.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_app/src/terminal_context.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_app/src/terminal_context.rs
-
curl -L -o terminal_context.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_app/src/terminal_context.rs
10.8 kB
| use std::sync::Arc; | |
| use forge_domain::{TerminalCommand, TerminalContext}; | |
| use crate::EnvironmentInfra; | |
| /// Environment variable exported by the zsh plugin containing | |
| /// `\x1F`-separated (ASCII Unit Separator) command strings. | |
| pub const ENV_TERM_COMMANDS: &str = "_FORGE_TERM_COMMANDS"; | |
| /// Environment variable exported by the zsh plugin containing | |
| /// `\x1F`-separated exit codes corresponding to [`ENV_TERM_COMMANDS`]. | |
| pub const ENV_TERM_EXIT_CODES: &str = "_FORGE_TERM_EXIT_CODES"; | |
| /// Environment variable exported by the zsh plugin containing | |
| /// `\x1F`-separated Unix timestamps corresponding to [`ENV_TERM_COMMANDS`]. | |
| pub const ENV_TERM_TIMESTAMPS: &str = "_FORGE_TERM_TIMESTAMPS"; | |
| /// The separator used to join and split environment variable lists. | |
| /// | |
| /// ASCII Unit Separator (`\x1F`) is chosen because it cannot appear in | |
| /// shell command strings, paths, URLs, or exit codes — unlike `:` which | |
| /// is common in all of those. | |
| pub const ENV_LIST_SEPARATOR: char = '\x1F'; | |
| /// Service that reads terminal context from environment variables exported by | |
| /// the zsh plugin and constructs a structured [`TerminalContext`]. | |
| /// | |
| /// The zsh plugin exports three `\x1F`-separated environment variables before | |
| /// invoking forge: | |
| /// - [`ENV_TERM_COMMANDS`] — the command strings | |
| /// - [`ENV_TERM_EXIT_CODES`] — the corresponding exit codes | |
| /// - [`ENV_TERM_TIMESTAMPS`] — the corresponding Unix timestamps | |
| pub struct TerminalContextService<S>(Arc<S>); | |
| impl<S> TerminalContextService<S> { | |
| /// Creates a new `TerminalContextService` backed by the provided | |
| /// infrastructure. | |
| pub fn new(infra: Arc<S>) -> Self { | |
| Self(infra) | |
| } | |
| } | |
| impl<S: EnvironmentInfra<Config = forge_config::ForgeConfig>> TerminalContextService<S> { | |
| /// Reads the terminal context from environment variables. | |
| /// | |
| /// Commands are sorted by timestamp (oldest first, most recent last). | |
| /// | |
| /// Returns `None` if none of the required variables are set or if no | |
| /// commands were recorded. | |
| pub fn get_terminal_context(&self) -> Option<TerminalContext> { | |
| let commands_raw = self.0.get_env_var(ENV_TERM_COMMANDS)?; | |
| let commands: Vec<String> = split_env_list(&commands_raw); | |
| if commands.is_empty() { | |
| return None; | |
| } | |
| let exit_codes_raw = self.0.get_env_var(ENV_TERM_EXIT_CODES).unwrap_or_default(); | |
| let timestamps_raw = self.0.get_env_var(ENV_TERM_TIMESTAMPS).unwrap_or_default(); | |
| let exit_codes: Vec<i32> = split_env_list(&exit_codes_raw) | |
| .iter() | |
| .map(|s| s.parse::<i32>().unwrap_or(0)) | |
| .collect(); | |
| let timestamps: Vec<u64> = split_env_list(×tamps_raw) | |
| .iter() | |
| .map(|s| s.parse::<u64>().unwrap_or(0)) | |
| .collect(); | |
| // Zip the three lists together; pad missing exit codes/timestamps with 0. | |
| // The outer zip() truncates to the length of `commands`, so the | |
| // repeat() padding never produces extra entries. | |
| let mut entries: Vec<TerminalCommand> = commands | |
| .into_iter() | |
| .zip(exit_codes.into_iter().chain(std::iter::repeat(0))) | |
| .zip(timestamps.into_iter().chain(std::iter::repeat(0))) | |
| .map(|((command, exit_code), timestamp)| TerminalCommand { | |
| command, | |
| exit_code, | |
| timestamp, | |
| }) | |
| .collect(); | |
| // Sort by timestamp so the most recent command appears last. | |
| entries.sort_by_key(|e| e.timestamp); | |
| if entries.is_empty() { | |
| None | |
| } else { | |
| Some(TerminalContext { commands: entries }) | |
| } | |
| } | |
| } | |
| /// Splits an `\x1F`-separated (ASCII Unit Separator) environment variable | |
| /// value into a list of strings, filtering out any empty segments. | |
| pub fn split_env_list(raw: &str) -> Vec<String> { | |
| raw.split(ENV_LIST_SEPARATOR) | |
| .filter(|s| !s.is_empty()) | |
| .map(String::from) | |
| .collect() | |
| } | |
| mod tests { | |
| use std::collections::BTreeMap; | |
| use std::sync::Arc; | |
| use forge_domain::{Environment, TerminalCommand, TerminalContext}; | |
| use pretty_assertions::assert_eq; | |
| use super::*; | |
| struct MockInfra { | |
| env_vars: BTreeMap<String, String>, | |
| } | |
| impl MockInfra { | |
| fn new(vars: &[(&str, &str)]) -> Arc<Self> { | |
| Arc::new(Self { | |
| env_vars: vars | |
| .iter() | |
| .map(|(k, v)| (k.to_string(), v.to_string())) | |
| .collect(), | |
| }) | |
| } | |
| } | |
| impl crate::EnvironmentInfra for MockInfra { | |
| type Config = forge_config::ForgeConfig; | |
| fn get_environment(&self) -> Environment { | |
| use fake::{Fake, Faker}; | |
| Faker.fake() | |
| } | |
| fn get_config(&self) -> anyhow::Result<forge_config::ForgeConfig> { | |
| Ok(forge_config::ForgeConfig::default()) | |
| } | |
| async fn update_environment( | |
| &self, | |
| _ops: Vec<forge_domain::ConfigOperation>, | |
| ) -> anyhow::Result<()> { | |
| Ok(()) | |
| } | |
| fn get_env_var(&self, key: &str) -> Option<String> { | |
| self.env_vars.get(key).cloned() | |
| } | |
| fn get_env_vars(&self) -> BTreeMap<String, String> { | |
| self.env_vars.clone() | |
| } | |
| } | |
| fn test_no_env_vars_returns_none() { | |
| let fixture = TerminalContextService::new(MockInfra::new(&[])); | |
| let actual = fixture.get_terminal_context(); | |
| assert_eq!(actual, None); | |
| } | |
| fn test_empty_commands_returns_none() { | |
| let fixture = TerminalContextService::new(MockInfra::new(&[(ENV_TERM_COMMANDS, "")])); | |
| let actual = fixture.get_terminal_context(); | |
| assert_eq!(actual, None); | |
| } | |
| fn test_single_command_no_extras() { | |
| let fixture = | |
| TerminalContextService::new(MockInfra::new(&[(ENV_TERM_COMMANDS, "cargo build")])); | |
| let actual = fixture.get_terminal_context(); | |
| let expected = Some(TerminalContext { | |
| commands: vec![TerminalCommand { | |
| command: "cargo build".to_string(), | |
| exit_code: 0, | |
| timestamp: 0, | |
| }], | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_multiple_commands_with_exit_codes_and_timestamps() { | |
| let sep = ENV_LIST_SEPARATOR; | |
| let fixture = TerminalContextService::new(MockInfra::new(&[ | |
| ( | |
| ENV_TERM_COMMANDS, | |
| &format!("ls{sep}cargo test{sep}git status"), | |
| ), | |
| (ENV_TERM_EXIT_CODES, &format!("0{sep}1{sep}0")), | |
| ( | |
| ENV_TERM_TIMESTAMPS, | |
| &format!("1700000001{sep}1700000002{sep}1700000003"), | |
| ), | |
| ])); | |
| let actual = fixture.get_terminal_context(); | |
| let expected = Some(TerminalContext { | |
| commands: vec![ | |
| TerminalCommand { | |
| command: "ls".to_string(), | |
| exit_code: 0, | |
| timestamp: 1700000001, | |
| }, | |
| TerminalCommand { | |
| command: "cargo test".to_string(), | |
| exit_code: 1, | |
| timestamp: 1700000002, | |
| }, | |
| TerminalCommand { | |
| command: "git status".to_string(), | |
| exit_code: 0, | |
| timestamp: 1700000003, | |
| }, | |
| ], | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_split_env_list_empty() { | |
| let actual = split_env_list(""); | |
| let expected: Vec<String> = vec![]; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_split_env_list_single() { | |
| let actual = split_env_list("hello"); | |
| let expected = vec!["hello".to_string()]; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_split_env_list_multiple() { | |
| let sep = ENV_LIST_SEPARATOR; | |
| let actual = split_env_list(&format!("a{sep}b{sep}c")); | |
| let expected = vec!["a".to_string(), "b".to_string(), "c".to_string()]; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_split_env_list_command_with_colon() { | |
| // Commands containing `:` (e.g. URLs, port mappings) must not be split. | |
| let sep = ENV_LIST_SEPARATOR; | |
| let actual = split_env_list(&format!( | |
| "curl https://example.com{sep}docker run -p 8080:80 nginx" | |
| )); | |
| let expected = vec![ | |
| "curl https://example.com".to_string(), | |
| "docker run -p 8080:80 nginx".to_string(), | |
| ]; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_commands_sorted_by_timestamp_oldest_first() { | |
| // Supply commands in reverse-timestamp order to confirm sorting is applied. | |
| let sep = ENV_LIST_SEPARATOR; | |
| let fixture = TerminalContextService::new(MockInfra::new(&[ | |
| ( | |
| ENV_TERM_COMMANDS, | |
| &format!("git status{sep}cargo test{sep}ls"), | |
| ), | |
| (ENV_TERM_EXIT_CODES, &format!("0{sep}1{sep}0")), | |
| ( | |
| ENV_TERM_TIMESTAMPS, | |
| &format!("1700000003{sep}1700000002{sep}1700000001"), | |
| ), | |
| ])); | |
| let actual = fixture.get_terminal_context(); | |
| let expected = Some(TerminalContext { | |
| commands: vec![ | |
| TerminalCommand { | |
| command: "ls".to_string(), | |
| exit_code: 0, | |
| timestamp: 1700000001, | |
| }, | |
| TerminalCommand { | |
| command: "cargo test".to_string(), | |
| exit_code: 1, | |
| timestamp: 1700000002, | |
| }, | |
| TerminalCommand { | |
| command: "git status".to_string(), | |
| exit_code: 0, | |
| timestamp: 1700000003, | |
| }, | |
| ], | |
| }); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_all_commands_included() { | |
| // All captured commands are included (no limit). | |
| let sep = ENV_LIST_SEPARATOR; | |
| let fixture = TerminalContextService::new(MockInfra::new(&[ | |
| ( | |
| ENV_TERM_COMMANDS, | |
| &format!("ls{sep}cargo test{sep}git status"), | |
| ), | |
| (ENV_TERM_EXIT_CODES, &format!("0{sep}1{sep}0")), | |
| ( | |
| ENV_TERM_TIMESTAMPS, | |
| &format!("1700000001{sep}1700000002{sep}1700000003"), | |
| ), | |
| ])); | |
| let actual = fixture.get_terminal_context(); | |
| assert_eq!(actual.unwrap().commands.len(), 3); | |
| } | |
| } | |