use crate::ported::ipython::{IPythonInfo, IPythonPowerline, RewriteResult};
use serde_json::{Map, Value};
use std::sync::{Mutex, OnceLock};
pub fn old_prompt_manager() -> &'static Mutex<Option<Value>> {
static M: OnceLock<Mutex<Option<Value>>> = OnceLock::new();
M.get_or_init(|| Mutex::new(None))
}
pub struct PowerlineMagics {
pub last_reload_call: bool,
}
impl Default for PowerlineMagics {
fn default() -> Self {
Self::new()
}
}
impl PowerlineMagics {
pub fn new() -> Self {
Self {
last_reload_call: false,
}
}
pub fn powerline(&mut self, line: &str) -> Result<(), String> {
if line == "reload" {
self.last_reload_call = true;
Ok(())
} else {
Err(format!("Expected `reload`, but got {}", line))
}
}
}
pub struct ShutdownHook {
pub powerline_live: bool,
pub call_count: u32,
}
impl Default for ShutdownHook {
fn default() -> Self {
Self::new()
}
}
impl ShutdownHook {
pub fn new() -> Self {
Self {
powerline_live: false,
call_count: 0,
}
}
pub fn set_powerline(&mut self) {
self.powerline_live = true;
}
pub fn call(&mut self) -> &'static str {
if self.powerline_live {
self.call_count += 1;
}
"TryNext"
}
}
pub struct PowerlinePromptManager {
pub segment_info: IPythonInfo,
pub txtwidth: usize,
pub width: usize,
}
impl PowerlinePromptManager {
pub fn new(segment_info: IPythonInfo) -> Self {
Self {
segment_info,
txtwidth: 0,
width: 0,
}
}
pub fn render<R>(&mut self, name: &str, color: bool, mut render: R) -> PromptManagerResult
where
R: FnMut(bool, &str, bool, bool, &str) -> (String, String, usize),
{
let is_prompt = name.starts_with("in");
let res = render(is_prompt, "left", true, !color, name);
self.txtwidth = res.2;
self.width = res.2;
let ret = if color { res.0 } else { res.1 };
if name == "rewrite" {
PromptManagerResult::Rewrite(RewriteResult::new(ret))
} else {
PromptManagerResult::Plain(ret)
}
}
}
#[derive(Debug, Clone)]
pub enum PromptManagerResult {
Rewrite(RewriteResult),
Plain(String),
}
pub struct ConfigurableIPythonPowerline {
pub base: IPythonPowerline,
pub setup_done: bool,
}
impl Default for ConfigurableIPythonPowerline {
fn default() -> Self {
Self::new()
}
}
impl ConfigurableIPythonPowerline {
pub fn new() -> Self {
Self {
base: IPythonPowerline::new(),
setup_done: false,
}
}
pub fn init(
&mut self,
powerline_config: &Map<String, Value>,
has_prompt_manager: bool,
) -> &'static str {
if let Some(overrides) = powerline_config
.get("config_overrides")
.and_then(|v| v.as_object())
{
self.base.config_overrides = Some(overrides.clone());
}
if let Some(themes) = powerline_config
.get("theme_overrides")
.and_then(|v| v.as_object())
{
self.base.theme_overrides = themes.clone();
}
if let Some(paths) = powerline_config
.get("config_paths")
.and_then(|v| v.as_array())
{
self.base.config_paths = paths
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
}
if has_prompt_manager {
".pre_5"
} else {
".since_7"
}
}
pub fn do_setup(&mut self, ip: &mut Map<String, Value>, shutdown_hook: &mut ShutdownHook) {
let mut slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
if slot.is_none() {
*slot = ip
.get("prompt_manager")
.cloned()
.or(Some(Value::String("<unset>".into())));
}
ip.insert(
"prompt_manager".to_string(),
Value::String("<PowerlinePromptManager>".into()),
);
shutdown_hook.set_powerline();
self.setup_done = true;
}
}
pub fn load_ipython_extension(has_prompt_manager: bool) -> LoadResult {
if has_prompt_manager {
LoadResult::PromptManager
} else {
LoadResult::Deprecated
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadResult {
PromptManager,
Deprecated,
}
pub fn unload_ipython_extension(ip: &mut Map<String, Value>) {
let mut slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(saved) = slot.take() {
ip.insert("prompt_manager".to_string(), saved);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
macro_rules! lock_globals {
() => {{
TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}};
}
fn reset_old_prompt_manager() {
let mut slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
*slot = None;
}
#[test]
fn powerline_magics_reload_succeeds() {
let mut m = PowerlineMagics::new();
assert!(m.powerline("reload").is_ok());
assert!(m.last_reload_call);
}
#[test]
fn powerline_magics_other_arg_errors() {
let mut m = PowerlineMagics::new();
let r = m.powerline("foo");
assert!(r.is_err());
assert!(r.unwrap_err().contains("Expected `reload`"));
assert!(!m.last_reload_call);
}
#[test]
fn shutdown_hook_initial_state_no_call_count() {
let h = ShutdownHook::new();
assert!(!h.powerline_live);
assert_eq!(h.call_count, 0);
}
#[test]
fn shutdown_hook_set_powerline_marks_live() {
let mut h = ShutdownHook::new();
h.set_powerline();
assert!(h.powerline_live);
}
#[test]
fn shutdown_hook_call_returns_try_next() {
let mut h = ShutdownHook::new();
h.set_powerline();
let r = h.call();
assert_eq!(r, "TryNext");
assert_eq!(h.call_count, 1);
}
#[test]
fn shutdown_hook_call_skips_shutdown_when_target_dead() {
let mut h = ShutdownHook::new();
h.call();
assert_eq!(h.call_count, 0);
}
#[test]
fn powerline_prompt_manager_render_returns_plain_for_non_rewrite() {
let mut pm = PowerlinePromptManager::new(IPythonInfo::new(0));
let result = pm.render("in", true, |_is_prompt, _side, _ow, _or, _name| {
("colored".to_string(), "raw".to_string(), 5)
});
match result {
PromptManagerResult::Plain(s) => assert_eq!(s, "colored"),
_ => panic!("expected Plain"),
}
assert_eq!(pm.txtwidth, 5);
assert_eq!(pm.width, 5);
}
#[test]
fn powerline_prompt_manager_render_returns_rewrite_for_rewrite_name() {
let mut pm = PowerlinePromptManager::new(IPythonInfo::new(0));
let result = pm.render("rewrite", true, |_is_prompt, _side, _ow, _or, _name| {
("rewritten".to_string(), "raw".to_string(), 7)
});
match result {
PromptManagerResult::Rewrite(r) => assert_eq!(r.prompt, "rewritten"),
_ => panic!("expected Rewrite"),
}
}
#[test]
fn powerline_prompt_manager_render_returns_raw_when_color_false() {
let mut pm = PowerlinePromptManager::new(IPythonInfo::new(0));
let result = pm.render("in", false, |_is_prompt, _side, _ow, _or, _name| {
("colored".to_string(), "raw_text".to_string(), 8)
});
match result {
PromptManagerResult::Plain(s) => assert_eq!(s, "raw_text"),
_ => panic!("expected Plain"),
}
}
#[test]
fn powerline_prompt_manager_render_passes_is_prompt_true_for_in_names() {
let mut pm = PowerlinePromptManager::new(IPythonInfo::new(0));
let mut saw_is_prompt = false;
let _ = pm.render("in", true, |is_prompt, _side, _ow, _or, _name| {
saw_is_prompt = is_prompt;
(String::new(), String::new(), 0)
});
assert!(saw_is_prompt);
}
#[test]
fn powerline_prompt_manager_render_passes_is_prompt_false_for_other_names() {
let mut pm = PowerlinePromptManager::new(IPythonInfo::new(0));
let mut saw_is_prompt = true;
let _ = pm.render("rewrite", true, |is_prompt, _side, _ow, _or, _name| {
saw_is_prompt = is_prompt;
(String::new(), String::new(), 0)
});
assert!(!saw_is_prompt);
}
#[test]
fn configurable_init_returns_pre_5_when_has_prompt_manager() {
let mut c = ConfigurableIPythonPowerline::new();
let cfg = Map::new();
assert_eq!(c.init(&cfg, true), ".pre_5");
}
#[test]
fn configurable_init_returns_since_7_without_prompt_manager() {
let mut c = ConfigurableIPythonPowerline::new();
let cfg = Map::new();
assert_eq!(c.init(&cfg, false), ".since_7");
}
#[test]
fn configurable_init_reads_overrides() {
let mut c = ConfigurableIPythonPowerline::new();
let mut overrides = Map::new();
overrides.insert("k".to_string(), Value::from(1));
let mut cfg = Map::new();
cfg.insert("config_overrides".to_string(), Value::Object(overrides));
c.init(&cfg, true);
assert!(c.base.config_overrides.is_some());
}
#[test]
fn configurable_do_setup_swaps_prompt_manager_and_wires_hook() {
let _g = lock_globals!();
reset_old_prompt_manager();
let mut c = ConfigurableIPythonPowerline::new();
let mut ip = Map::new();
ip.insert("prompt_manager".to_string(), Value::String("<orig>".into()));
let mut hook = ShutdownHook::new();
c.do_setup(&mut ip, &mut hook);
assert_eq!(
ip.get("prompt_manager"),
Some(&Value::String("<PowerlinePromptManager>".into()))
);
assert!(hook.powerline_live);
assert!(c.setup_done);
let slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
assert_eq!(*slot, Some(Value::String("<orig>".into())));
}
#[test]
fn configurable_do_setup_does_not_overwrite_existing_snapshot() {
let _g = lock_globals!();
let mut slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
*slot = Some(Value::String("<pre-existing>".into()));
drop(slot);
let mut c = ConfigurableIPythonPowerline::new();
let mut ip = Map::new();
ip.insert("prompt_manager".to_string(), Value::String("<new>".into()));
let mut hook = ShutdownHook::new();
c.do_setup(&mut ip, &mut hook);
let slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
assert_eq!(*slot, Some(Value::String("<pre-existing>".into())));
}
#[test]
fn load_ipython_extension_with_prompt_manager_returns_prompt_manager() {
assert_eq!(load_ipython_extension(true), LoadResult::PromptManager);
}
#[test]
fn load_ipython_extension_without_prompt_manager_returns_deprecated() {
assert_eq!(load_ipython_extension(false), LoadResult::Deprecated);
}
#[test]
fn unload_ipython_extension_restores_snapshot() {
let _g = lock_globals!();
reset_old_prompt_manager();
let mut slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
*slot = Some(Value::String("<saved>".into()));
drop(slot);
let mut ip = Map::new();
ip.insert(
"prompt_manager".to_string(),
Value::String("<powerline>".into()),
);
unload_ipython_extension(&mut ip);
assert_eq!(
ip.get("prompt_manager"),
Some(&Value::String("<saved>".into()))
);
let slot = old_prompt_manager()
.lock()
.unwrap_or_else(|e| e.into_inner());
assert!(slot.is_none());
}
#[test]
fn unload_ipython_extension_noop_when_no_snapshot() {
let _g = lock_globals!();
reset_old_prompt_manager();
let mut ip = Map::new();
ip.insert(
"prompt_manager".to_string(),
Value::String("<original>".into()),
);
unload_ipython_extension(&mut ip);
assert_eq!(
ip.get("prompt_manager"),
Some(&Value::String("<original>".into()))
);
}
}