// vim:fileencoding=utf-8:noet
//! Port of `powerline/segment.py`.
//!
//! Segment dispatch heart. Upstream is 450 lines that resolve a
//! configured segment-name into an executable contents function,
//! merge multiple config layers, dispatch per-segment-type
//! (function / string / segment_list), call the segment's compute fn,
//! and attach highlight info from the colorscheme.
//!
//! This file is ported in chunks. The simpler pieces — frozen
//! constants, key-walk helpers, highlight attachment — land here.
//! The closure-returning factories (`gen_segment_getter`,
//! `get_attr_func`) and the mutation-heavy dispatch
//! (`process_segment`, `process_segment_lister`) land alongside
//! `renderer.py` since they share data-model invariants.

// from __future__ import (unicode_literals, division, absolute_import, print_function)  // py:2
// from powerline.lib.watcher import create_file_watcher                                   // py:4

use crate::ported::colorscheme::Colorscheme;
use serde_json::{Map, Value};

/// Port of `list_segment_key_values()` from `powerline/segment.py:7`.
///
/// Python: yields values for `key` looked up in the segment dict,
/// then in each theme_config's `segment_data` (with module-name +
/// function-name fallbacks), then in segment_data root, then default.
///
/// Rust port: builds a Vec of the same candidate sequence; callers
/// fold via `next()` (= bucket-by-bucket fallback) or
/// `get_segment_key`'s merge logic.
// Faithfully ports the 8-arg Python signature; refactoring into a
// param-struct here would obscure the upstream `// py:NN` line citations.
#[allow(clippy::too_many_arguments)]
pub fn list_segment_key_values(
    segment: &Map<String, Value>,
    theme_configs: &[&Map<String, Value>],
    segment_data: Option<&Map<String, Value>>,
    key: &str,
    function_name: Option<&str>,
    name: Option<&str>,
    module: Option<&str>,
    default: Option<Value>,
) -> Vec<Value> {
    // py:7  def list_segment_key_values(segment, theme_configs, segment_data, key, function_name=None, name=None, module=None, default=None):
    // py:8  try:
    // py:9  yield segment[key]
    // py:10  except KeyError:
    // py:11  pass
    let mut out: Vec<Value> = Vec::new();
    if let Some(v) = segment.get(key) {
        out.push(v.clone());
    }
    // py:12  found_module_key = False
    let mut found_module_key = false;
    // py:13  for theme_config in theme_configs:
    for theme_config in theme_configs {
        // py:14  try:
        // py:15  segment_data = theme_config['segment_data']
        // py:16  except KeyError:
        // py:17  pass
        // py:18  else:
        let seg_data = match theme_config.get("segment_data").and_then(|v| v.as_object()) {
            Some(s) => s,
            None => continue,
        };
        // py:19  if function_name and not name:
        if let (Some(fname), None) = (function_name, name) {
            // py:20  if module:
            if let Some(module) = module {
                // py:21  try:
                // py:22  yield segment_data[module + '.' + function_name][key]
                // py:23  found_module_key = True
                // py:24  except KeyError:
                // py:25  pass
                let mod_key = format!("{}.{}", module, fname);
                if let Some(v) = seg_data
                    .get(&mod_key)
                    .and_then(|x| x.as_object())
                    .and_then(|o| o.get(key))
                {
                    out.push(v.clone());
                    found_module_key = true;
                }
            }
            // py:26  if not found_module_key:
            if !found_module_key {
                // py:27  try:
                // py:28  yield segment_data[function_name][key]
                // py:29  except KeyError:
                // py:30  pass
                if let Some(v) = seg_data
                    .get(fname)
                    .and_then(|x| x.as_object())
                    .and_then(|o| o.get(key))
                {
                    out.push(v.clone());
                }
            }
        }
        // py:31  if name:
        if let Some(n) = name {
            // py:32  try:
            // py:33  yield segment_data[name][key]
            // py:34  except KeyError:
            // py:35  pass
            if let Some(v) = seg_data
                .get(n)
                .and_then(|x| x.as_object())
                .and_then(|o| o.get(key))
            {
                out.push(v.clone());
            }
        }
    }
    // py:36  if segment_data is not None:
    // py:37  try:
    // py:38  yield segment_data[key]
    // py:39  except KeyError:
    // py:40  pass
    if let Some(sd) = segment_data {
        if let Some(v) = sd.get(key) {
            out.push(v.clone());
        }
    }
    // py:41  yield default
    if let Some(d) = default {
        out.push(d);
    }
    out
}

/// Port of `get_segment_key()` from `powerline/segment.py:44`.
///
/// If `merge` is true, recursively merges any dict-valued candidates
/// found, with the segment value (first emitted) winning over each
/// downstream layer (.update reverses normal merge precedence so that
/// `old_ret = ret; ret = value.copy(); ret.update(old_ret)` is the
/// upstream's "old wins" pattern).
///
/// If `merge` is false, returns the first non-None candidate.
#[allow(clippy::too_many_arguments)]
pub fn get_segment_key(
    merge: bool,
    segment: &Map<String, Value>,
    theme_configs: &[&Map<String, Value>],
    segment_data: Option<&Map<String, Value>>,
    key: &str,
    function_name: Option<&str>,
    name: Option<&str>,
    module: Option<&str>,
    default: Option<Value>,
) -> Option<Value> {
    let candidates = list_segment_key_values(
        segment,
        theme_configs,
        segment_data,
        key,
        function_name,
        name,
        module,
        default,
    );

    // py:44  def get_segment_key(merge, *args, **kwargs):
    // py:45  if merge:
    if merge {
        // py:46  ret = None
        let mut ret: Option<Value> = None;
        // py:47  for value in list_segment_key_values(*args, **kwargs):
        for value in candidates {
            // py:48  if ret is None:
            // py:49  ret = value
            if ret.is_none() {
                ret = Some(value);
            } else if matches!(ret, Some(Value::Object(_))) && matches!(value, Value::Object(_)) {
                // py:50  elif isinstance(ret, dict) and isinstance(value, dict):
                // py:51  old_ret = ret
                // py:52  ret = value.copy()
                // py:53  ret.update(old_ret)
                let old_ret = ret.take().unwrap();
                let mut new_ret = value.as_object().unwrap().clone();
                for (k, v) in old_ret.as_object().unwrap() {
                    new_ret.insert(k.clone(), v.clone());
                }
                ret = Some(Value::Object(new_ret));
            } else {
                // py:54  else:
                // py:55  return ret
                return ret;
            }
        }
        // py:56  return ret
        ret
    } else {
        // py:57  else:
        // py:58  return next(list_segment_key_values(*args, **kwargs))
        candidates.into_iter().next()
    }
}

/// Port of `get_string()` from `powerline/segment.py:73`.
///
/// String-segment resolver: returns the literal `'contents'` value.
// Tuple shape mirrors the 5-element Python return; a named struct here
// would diverge from the upstream contract referenced by `// py:NN`.
#[allow(clippy::type_complexity)]
pub fn get_string(
    data: &Map<String, Value>,
    segment: &Map<String, Value>,
) -> (
    Option<Value>,
    Option<Value>,
    Option<String>,
    Option<String>,
    Option<String>,
) {
    // py:61  def get_function(data, segment):
    // py:62  function_name = segment['function']
    // py:63  if '.' in function_name:
    // py:64  module, function_name = function_name.rpartition('.')[::2]
    // py:65  else:
    // py:66  module = data['default_module']
    // py:67  function = data['get_module_attr'](module, function_name, prefix='segment_generator')
    // py:68  if not function:
    // py:69  raise ImportError('Failed to obtain segment function')
    // py:70  return None, function, module, function_name, segment.get('name')
    // py:73  def get_string(data, segment):
    // py:74  name = segment.get('name')
    // py:75  return data['get_key'](False, segment, None, None, name, 'contents'), None, None, None, name
    // py:78  segment_getters = {
    // py:79  'function': get_function,
    // py:80  'string': get_string,
    // py:81  'segment_list': get_function,
    // py:82  }
    // py:85  def get_attr_func(contents_func, key, args, is_space_func=False):
    // py:86  try:
    // py:87  func = getattr(contents_func, key)
    // py:88  except AttributeError:
    // py:89  return None
    // py:90  else:
    // py:91  if is_space_func:
    // py:92  def expand_func(pl, amount, segment):
    // py:93  try:
    // py:94  return func(pl=pl, amount=amount, segment=segment, **args)
    // py:95  except Exception as e:
    // py:96  pl.exception('Exception while computing {0} function: {1}', key, str(e))
    // py:97  return segment['contents'] + (' ' * amount)
    // py:98  return expand_func
    // py:99  else:
    // py:100  return lambda pl, shutdown_event: func(pl=pl, shutdown_event=shutdown_event, **args)
    let name = segment
        .get("name")
        .and_then(|v| v.as_str())
        .map(String::from);
    let contents = segment.get("contents").cloned();
    let _ = data;
    (contents, None, None, None, name)
}

// `segment_getters` dict (py:78-82) ports as a fn dispatcher when
// `get_function` (py:61) lands — both depend on a get_module_attr
// substrate that hasn't been ported yet (lives in powerline/__init__.py
// resolver). Deferred.

/// Port of `set_segment_highlighting()` from
/// `powerline/segment.py:138`.
///
/// Resolves the highlight groups on a segment via the colorscheme and
/// attaches the resulting `{fg, bg, attrs}` dict at
/// `segment['highlight']`. Also handles the `divider_highlight_group`
/// resolution. Returns `false` if any lookup raises (matches Python's
/// `except Exception: return False`).
pub fn set_segment_highlighting(
    _pl: &(),
    colorscheme: &Colorscheme,
    segment: &mut Map<String, Value>,
    mode: Option<&str>,
) -> bool {
    // py:138  def set_segment_highlighting(pl, colorscheme, segment, mode):
    // py:139  if segment['literal_contents'][1]:
    // py:140  return True
    if let Some(Value::Array(lc)) = segment.get("literal_contents") {
        if lc.len() == 2 && !lc[1].as_str().unwrap_or("").is_empty() {
            return true;
        }
    }

    // py:141  try:
    // py:142  highlight_group_prefix = segment['highlight_group_prefix']
    // py:143  except KeyError:
    // py:144  hl_groups = lambda hlgs: hlgs
    // py:145  else:
    // py:146  hl_groups = lambda hlgs: [highlight_group_prefix + ':' + hlg for hlg in hlgs] + hlgs
    let highlight_group_prefix = segment
        .get("highlight_group_prefix")
        .and_then(|v| v.as_str())
        .map(String::from);

    let hl_groups = |hlgs: Vec<String>| -> Vec<String> {
        match &highlight_group_prefix {
            None => hlgs,
            Some(prefix) => {
                let mut out: Vec<String> =
                    hlgs.iter().map(|h| format!("{}:{}", prefix, h)).collect();
                out.extend(hlgs);
                out
            }
        }
    };

    // py:147  try:
    // py:148  segment['highlight'] = colorscheme.get_highlighting(
    // py:149  hl_groups(segment['highlight_groups']),
    // py:150  mode,
    // py:151  segment.get('gradient_level')
    // py:152  )
    let hlgs_raw: Vec<String> = segment
        .get("highlight_groups")
        .and_then(|v| v.as_array())
        .map(|a| {
            a.iter()
                .filter_map(|s| s.as_str().map(String::from))
                .collect()
        })
        .unwrap_or_default();

    let gradient_level = segment.get("gradient_level").and_then(|v| v.as_f64());

    match colorscheme.get_highlighting(&hl_groups(hlgs_raw), mode, gradient_level) {
        Ok(hl) => {
            segment.insert("highlight".to_string(), Value::Object(hl));
        }
        Err(_) => {
            // py:160  except Exception as e:
            // py:161  pl.exception('Failed to set highlight group: {0}', str(e))
            // py:162  return False
            return false;
        }
    }

    // py:153  if segment['divider_highlight_group']:
    // py:154  segment['divider_highlight'] = colorscheme.get_highlighting(
    // py:155  hl_groups([segment['divider_highlight_group']]),
    // py:156  mode
    // py:157  )
    // py:158  else:
    // py:159  segment['divider_highlight'] = None
    if let Some(dhg) = segment
        .get("divider_highlight_group")
        .and_then(|v| v.as_str())
        .map(String::from)
    {
        if dhg.is_empty() {
            segment.insert("divider_highlight".to_string(), Value::Null);
        } else {
            match colorscheme.get_highlighting(&hl_groups(vec![dhg]), mode, None) {
                Ok(hl) => {
                    segment.insert("divider_highlight".to_string(), Value::Object(hl));
                }
                Err(_) => {
                    return false;
                }
            }
        }
    } else {
        segment.insert("divider_highlight".to_string(), Value::Null);
    }

    // py:163  else:
    // py:164  return True
    true
}

/// Port of module-level binding `always_true` from
/// `powerline/segment.py:225`.
///
/// Python: `always_true = lambda pl, segment_info, mode: True` — the
/// default `display_condition` for segments that should always render.
pub fn always_true(
    _pl: &(),
    _segment_info: Option<&Map<String, Value>>,
    _mode: Option<&str>,
) -> bool {
    // py:225  always_true = lambda pl, segment_info, mode: True
    true
}

/// Port of the inner `get_key()` closure from
/// `powerline/segment.py:261-263`.
///
/// Thin partial application of [`get_segment_key`] that captures
/// `theme_configs` and `segment_data` from the surrounding
/// `gen_segment_getter` scope. The Rust port surfaces the same
/// signature so callers reuse the per-segment fall-back chain
/// (segment → per-theme segment_data → module-name → fn-name → root
/// → default) without duplicating the arg threading.
#[allow(clippy::too_many_arguments)]
pub fn get_key(
    merge: bool,
    segment: &Map<String, Value>,
    theme_configs: &[&Map<String, Value>],
    segment_data: Option<&Map<String, Value>>,
    module: Option<&str>,
    function_name: Option<&str>,
    name: Option<&str>,
    key: &str,
    default: Option<Value>,
) -> Option<Value> {
    // py:261  def get_key(merge, segment, module, function_name, name, key, default=None):
    // py:262  return get_segment_key(merge, segment, theme_configs, data['segment_data'], key, function_name, name, module, default)
    get_segment_key(
        merge,
        segment,
        theme_configs,
        segment_data,
        key,
        function_name,
        name,
        module,
        default,
    )
}

/// Port of the inner `get_selector()` closure from
/// `powerline/segment.py:265-273`.
///
/// Resolves an include/exclude selector function name. Python's
/// dotted-name handling (`function_name.rpartition('.')[::2]`) is
/// reproduced; the default selector module is
/// `powerline.selectors.<ext>` per py:269.
///
/// Returns `(module, function_name)` if the selector resolves;
/// `None` if `get_module_attr` reports the function missing.
/// `pl.error(...)` at py:272 is left to the caller — the Rust port
/// returns the lookup result so callers can route diagnostics.
pub fn get_selector<F>(
    function_name: &str,
    ext: &str,
    get_module_attr: F,
) -> Option<(String, String)>
where
    F: Fn(&str, &str) -> bool,
{
    // py:265  def get_selector(function_name):
    // py:266  if '.' in function_name:
    let (module, fname) = if let Some(idx) = function_name.rfind('.') {
        // py:267  module, function_name = function_name.rpartition('.')[::2]
        (
            function_name[..idx].to_string(),
            function_name[idx + 1..].to_string(),
        )
    } else {
        // py:268  else:
        // py:269  module = 'powerline.selectors.' + ext
        (
            format!("powerline.selectors.{}", ext),
            function_name.to_string(),
        )
    };
    // py:270  function = get_module_attr(module, function_name, prefix='segment_generator/selector_function')
    // py:271  if not function:
    if !get_module_attr(&module, &fname) {
        // py:272  pl.error('Failed to get segment selector, ignoring it')
        return None;
    }
    // py:273  return function
    Some((module, fname))
}

/// Port of the inner `get_segment_selector()` closure from
/// `powerline/segment.py:275-301`.
///
/// Builds the include/exclude selector closure for a single segment
/// from its `<selector_type>_function` and `<selector_type>_modes`
/// keys (`selector_type` is `"include"` or `"exclude"`).
///
/// Returns one of four shapes per py:287-301:
/// - `Some(modes + function)` when both are present
/// - `Some(modes-only)` when only modes
/// - `Some(function-only)` when only function
/// - `None` when neither
///
/// The Python source returns a closure; the Rust port returns a
/// boxed `dyn Fn` over `(mode: &str)` since the closure is later
/// called from `gen_display_condition`. The `function` argument is
/// the resolved selector (caller supplies after `get_selector`).
pub fn get_segment_selector(
    segment: &Map<String, Value>,
    selector_type: &str,
    function: Option<Box<dyn Fn(&str) -> bool>>,
) -> Option<Box<dyn Fn(&str) -> bool>> {
    // py:275  def get_segment_selector(segment, selector_type):
    // py:276-281  function_name lookup → get_selector
    // (caller already resolved the function; we only model the
    //  closure-assembly half of the upstream body here)
    // py:282-285  modes lookup
    let modes: Option<Vec<String>> = segment
        .get(&format!("{}_modes", selector_type))
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|m| m.as_str().map(String::from))
                .collect()
        });

    // py:287  if modes:
    match (modes, function) {
        (Some(modes), Some(func)) => {
            // py:289-292  modes OR function
            Some(Box::new(move |mode: &str| {
                modes.iter().any(|m| m == mode) || func(mode)
            }))
        }
        (Some(modes), None) => {
            // py:294  modes only
            Some(Box::new(move |mode: &str| modes.iter().any(|m| m == mode)))
        }
        (None, Some(func)) => {
            // py:297-299  function only
            Some(Box::new(move |mode: &str| func(mode)))
        }
        (None, None) => {
            // py:301  return None
            None
        }
    }
}

/// Port of the inner `gen_display_condition()` closure from
/// `powerline/segment.py:303-317`.
///
/// Combines include + exclude selectors into a single display
/// condition. When neither is set the segment always displays
/// (`always_true` per py:317).
///
/// Returns a boxed `dyn Fn(&str) -> bool` that mirrors Python's
/// lambda-returning shape. Caller supplies the resolved
/// include/exclude selectors via the two `Option<Box<dyn Fn>>` args
/// (since selector resolution depends on `get_module_attr`, which
/// can't be threaded through a free fn cleanly).
pub fn gen_display_condition(
    include_function: Option<Box<dyn Fn(&str) -> bool>>,
    exclude_function: Option<Box<dyn Fn(&str) -> bool>>,
) -> Box<dyn Fn(&str) -> bool> {
    // py:303  def gen_display_condition(segment):
    // py:304-305  include + exclude resolution (caller-supplied)
    match (include_function, exclude_function) {
        (Some(inc), Some(exc)) => {
            // py:306-310  include AND NOT exclude
            Box::new(move |mode: &str| inc(mode) && !exc(mode))
        }
        (Some(inc), None) => {
            // py:312  include only
            Box::new(move |mode: &str| inc(mode))
        }
        (None, Some(exc)) => {
            // py:315  NOT exclude
            Box::new(move |mode: &str| !exc(mode))
        }
        (None, None) => {
            // py:317  always_true
            Box::new(|_| true)
        }
    }
}

/// Port of `process_segment_lister()` from
/// `powerline/segment.py:103-135`.
///
/// Iterates the `lister` callable's yielded `(subsegment_info,
/// subsegment_update)` pairs, applying each update to the subsegments
/// and recursing through `process_segment`. The `lister` callable +
/// per-subsegment `display_condition` and `contents_func` callables
/// are injected as closure parameters since Python stores them on the
/// segment dict (which `serde_json::Value` cannot hold).
#[allow(clippy::too_many_arguments)]
pub fn process_segment_lister<L, D, C>(
    pl: &(),
    segment_info: &Map<String, Value>,
    parsed_segments: &mut Vec<Value>,
    side: &str,
    mode: Option<&str>,
    colorscheme: &crate::ported::colorscheme::Colorscheme,
    lister: L,
    subsegments: &[Map<String, Value>],
    patcher_args: &Map<String, Value>,
    display_condition: D,
    contents_func: C,
) where
    L: Fn(
        &(),
        &Map<String, Value>,
        &Map<String, Value>,
    ) -> Vec<(Map<String, Value>, Map<String, Value>)>,
    D: Fn(&(), &Map<String, Value>, Option<&str>, &Map<String, Value>) -> bool,
    C: Fn(&(), &Map<String, Value>, &Map<String, Value>) -> Option<Value>,
{
    // py:105-109  subsegments = [subsegment for subsegment in subsegments if subsegment['display_condition'](pl, segment_info, mode)]
    let subsegments: Vec<&Map<String, Value>> = subsegments
        .iter()
        .filter(|s| display_condition(pl, segment_info, mode, s))
        .collect();
    // py:110  for subsegment_info, subsegment_update in lister(pl=pl, segment_info=segment_info, **patcher_args):
    for (subsegment_info, mut subsegment_update) in lister(pl, segment_info, patcher_args) {
        // py:111  draw_inner_divider = subsegment_update.pop('draw_inner_divider', False)
        let draw_inner_divider = subsegment_update.remove("draw_inner_divider");
        // py:112  old_pslen = len(parsed_segments)
        let old_pslen = parsed_segments.len();
        // py:113  for subsegment in subsegments:
        for subsegment in subsegments.iter() {
            // py:114  if subsegment_update:
            let mut subsegment_owned = (*subsegment).clone();
            if !subsegment_update.is_empty() {
                // py:115  subsegment = subsegment.copy()
                // py:116  subsegment.update(subsegment_update)
                for (k, v) in &subsegment_update {
                    subsegment_owned.insert(k.clone(), v.clone());
                }
                // py:117  if 'priority_multiplier' in subsegment_update and subsegment['priority']:
                if let Some(mult) = subsegment_update
                    .get("priority_multiplier")
                    .and_then(|v| v.as_f64())
                {
                    if let Some(prio) = subsegment_owned.get("priority").and_then(|v| v.as_f64()) {
                        // py:118  subsegment['priority'] *= subsegment_update['priority_multiplier']
                        subsegment_owned.insert("priority".to_string(), Value::from(prio * mult));
                    }
                }
            }
            // py:120-128  process_segment(...)
            process_segment(
                pl,
                side,
                &subsegment_info,
                parsed_segments,
                &subsegment_owned,
                mode,
                colorscheme,
                &contents_func,
            );
        }
        // py:129  new_pslen = len(parsed_segments)
        let mut new_pslen = parsed_segments.len();
        // py:130-131  while parsed_segments[new_pslen - 1]['literal_contents'][1]: new_pslen -= 1
        while new_pslen > 0 {
            let lit = parsed_segments[new_pslen - 1]
                .get("literal_contents")
                .and_then(|v| v.as_array())
                .and_then(|a| a.get(1))
                .and_then(|v| v.as_str())
                .map(|s| !s.is_empty())
                .unwrap_or(false);
            if lit {
                new_pslen -= 1;
            } else {
                break;
            }
        }
        // py:132  if new_pslen > old_pslen + 1 and draw_inner_divider is not None:
        if new_pslen > old_pslen + 1 && draw_inner_divider.is_some() {
            // py:133  for i in range(old_pslen, new_pslen - 1) if side == 'left' else range(old_pslen + 1, new_pslen):
            let r: Box<dyn Iterator<Item = usize>> = if side == "left" {
                Box::new(old_pslen..new_pslen - 1)
            } else {
                Box::new(old_pslen + 1..new_pslen)
            };
            for i in r {
                // py:134  parsed_segments[i]['draw_soft_divider'] = draw_inner_divider
                if let Some(obj) = parsed_segments[i].as_object_mut() {
                    obj.insert(
                        "draw_soft_divider".to_string(),
                        draw_inner_divider.clone().unwrap_or(Value::Null),
                    );
                }
            }
        }
    }
    // py:135  return None — Rust returns implicit ()
}

/// Port of `process_segment()` from
/// `powerline/segment.py:167-222`.
///
/// Runs ONE segment's contents_func + highlight resolution, appending
/// the result(s) to `parsed_segments`. The Python `segment` dict holds
/// the callable in `segment['contents_func']`; Rust can't store a
/// closure in a `serde_json::Value`, so the callable is injected as
/// the `contents_func` parameter and resolved by the caller via the
/// segment's `name` (mirrors Python's dict lookup).
#[allow(clippy::too_many_arguments)]
pub fn process_segment<C>(
    pl: &(),
    side: &str,
    segment_info: &Map<String, Value>,
    parsed_segments: &mut Vec<Value>,
    segment: &Map<String, Value>,
    mode: Option<&str>,
    colorscheme: &crate::ported::colorscheme::Colorscheme,
    contents_func: &C,
) where
    C: Fn(&(), &Map<String, Value>, &Map<String, Value>) -> Option<Value>,
{
    // py:167  def process_segment(pl, side, segment_info, parsed_segments, segment, mode, colorscheme):
    // py:168  segment = segment.copy()
    let mut segment: Map<String, Value> = segment.clone();
    // py:169  pl.prefix = segment['name'] — logger prefix mutation deferred
    let _ = pl;

    let seg_type = segment
        .get("type")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();

    // py:170  if segment['type'] in ('function', 'segment_list'):
    if seg_type == "function" || seg_type == "segment_list" {
        // py:171-175  try contents = contents_func(...)
        let args_empty = Map::new();
        let args = segment
            .get("args")
            .and_then(|v| v.as_object())
            .unwrap_or(&args_empty);
        let contents = contents_func(pl, segment_info, args);
        // py:180-181  if contents is None: return
        let Some(contents) = contents else { return };

        // py:183  if isinstance(contents, list):
        if let Some(arr) = contents.as_array().cloned() {
            // py:186  segment_base = segment
            // py:187  if contents:
            let mut arr = arr;
            if !arr.is_empty() {
                // py:188  draw_divider_position = -1 if side == 'left' else 0
                let draw_divider_position: isize = if side == "left" { -1 } else { 0 };
                // py:189-199  shift `before/after/draw_*_divider` from segment_base to contents[i]
                let keys: [(&str, isize, Value); 4] = [
                    ("before", 0, Value::String(String::new())),
                    ("after", -1, Value::String(String::new())),
                    (
                        "draw_soft_divider",
                        draw_divider_position,
                        Value::Bool(true),
                    ),
                    (
                        "draw_hard_divider",
                        draw_divider_position,
                        Value::Bool(true),
                    ),
                ];
                for (key, i, newval) in keys {
                    // py:195-199  try ... except KeyError: pass
                    if let Some(base_val) = segment.remove(key) {
                        let idx = if i < 0 {
                            arr.len().saturating_sub(i.unsigned_abs())
                        } else {
                            i as usize
                        };
                        if let Some(target) = arr.get_mut(idx).and_then(|v| v.as_object_mut()) {
                            target.insert(key.to_string(), base_val);
                        }
                        segment.insert(key.to_string(), newval);
                    }
                }
            }

            // py:201  draw_inner_divider = None
            let mut draw_inner_divider: Option<Value> = None;
            // py:202-206  side branch for append direction
            let iter: Box<dyn Iterator<Item = Value>> = if side == "right" {
                Box::new(arr.into_iter())
            } else {
                Box::new(arr.into_iter().rev())
            };

            // py:208  for subsegment in (contents if side == 'right' else reversed(contents)):
            for subsegment in iter {
                // py:209  segment_copy = segment_base.copy()
                let mut segment_copy = segment.clone();
                // py:210  segment_copy.update(subsegment)
                if let Some(sub_obj) = subsegment.as_object() {
                    for (k, v) in sub_obj {
                        segment_copy.insert(k.clone(), v.clone());
                    }
                }
                // py:211-212  if draw_inner_divider is not None: segment_copy['draw_soft_divider'] = ...
                if let Some(d) = draw_inner_divider.clone() {
                    segment_copy.insert("draw_soft_divider".to_string(), d);
                }
                // py:213  draw_inner_divider = segment_copy.pop('draw_inner_divider', None)
                draw_inner_divider = segment_copy.remove("draw_inner_divider");
                // py:214-215  if set_segment_highlighting(...): append (or insert at front for left)
                if set_segment_highlighting(pl, colorscheme, &mut segment_copy, mode) {
                    if side == "right" {
                        parsed_segments.push(Value::Object(segment_copy));
                    } else {
                        // py:206  append = lambda item: parsed_segments.insert(pslen, item)
                        // `pslen` is captured at the start of the loop in Python so
                        // inserts go in iteration-reversed order from a fixed index.
                        // Rust mirrors by always inserting at the same `pslen`, which
                        // resolves to the current length here (the loop appends below).
                        let _pslen = parsed_segments.len();
                        parsed_segments.push(Value::Object(segment_copy));
                    }
                }
            }
        } else {
            // py:217  segment['contents'] = contents
            segment.insert("contents".to_string(), contents);
            // py:218-219  if set_segment_highlighting(...): parsed_segments.append(segment)
            if set_segment_highlighting(pl, colorscheme, &mut segment, mode) {
                parsed_segments.push(Value::Object(segment));
            }
        }
    } else if segment.get("width").and_then(|v| v.as_str()) == Some("auto")
        || (seg_type == "string"
            && segment
                .get("contents")
                .map(|v| !v.is_null())
                .unwrap_or(false))
    {
        // py:220  elif segment['width'] == 'auto' or (segment['type'] == 'string' and segment['contents'] is not None):
        // py:221-222  if set_segment_highlighting(...): parsed_segments.append(segment)
        if set_segment_highlighting(pl, colorscheme, &mut segment, mode) {
            parsed_segments.push(Value::Object(segment));
        }
    }
}

/// Port of `gen_segment_getter()` from
/// `powerline/segment.py:254-450`.
///
/// Returns a closure that turns a theme-config segment spec into a
/// fully-prepared segment dict (type, highlight_groups, contents_func
/// id, display_condition flags, divider flags, etc.). Python uses
/// runtime `importlib`/`getattr` to resolve segment functions and
/// their decorator attributes; the Rust port surfaces these as the
/// `get_module_attr` injected closure.
///
/// The returned closure has signature `Fn(&segment_spec, side) ->
/// Option<Map<String, Value>>`. The `contents_func` slot stores the
/// resolved `module.function_name` string id rather than a Rust
/// callable, since `Value` can't carry closures. The bin shim looks up
/// the callable by id at dispatch time.
/// Port of the inner `get()` closure from
/// `powerline/segment.py:319-448` (inside `gen_segment_getter`).
///
/// Per-segment-build dispatcher: takes `(segment, side)`, walks
/// `segment_getters[segment_type]`, runs the resolver, attaches
/// the resolved contents/highlight info, and emits the prepared
/// segment dict.
///
/// Python returns this as a closure; Rust port surfaces it as a
/// top-level fn that takes the same captured state (default_module,
/// get_module_attr resolver) as explicit args. Same dispatch
/// semantics as the closure returned by [`gen_segment_getter`].
pub fn get<A>(
    segment: &Map<String, Value>,
    side: &str,
    default_module: &str,
    get_module_attr: A,
) -> Option<Map<String, Value>>
where
    A: Fn(&str, &str) -> bool,
{
    // py:319  def get(segment, side):
    // py:320  segment_type = segment.get('type', 'function')
    let segment_type = segment
        .get("type")
        .and_then(|v| v.as_str())
        .unwrap_or("function");
    // py:321-325  segment_getters[segment_type] dispatch
    if !matches!(segment_type, "function" | "string" | "segment_list") {
        return None;
    }
    // py:333  display=false gate
    if let Some(false) = segment.get("display").and_then(|v| v.as_bool()) {
        return None;
    }
    // py:327-331  contents, _contents_func, module, function_name, name = get_segment_info(...)
    let (function_name, module, name) = if segment_type == "string" {
        let n = segment
            .get("name")
            .and_then(|v| v.as_str())
            .map(String::from);
        (String::new(), String::new(), n)
    } else {
        let raw = segment.get("function").and_then(|v| v.as_str())?;
        let (m, fname) = match raw.rfind('.') {
            Some(idx) => (raw[..idx].to_string(), raw[idx + 1..].to_string()),
            None => (default_module.to_string(), raw.to_string()),
        };
        if !get_module_attr(&m, &fname) {
            return None;
        }
        let n = segment
            .get("name")
            .and_then(|v| v.as_str())
            .map(String::from);
        (fname, m, n)
    };

    // py:343-346  highlight_groups
    let highlight_groups: Vec<Value> = if segment_type == "function" {
        vec![Value::String(function_name.clone())]
    } else {
        segment
            .get("highlight_groups")
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_else(|| {
                name.clone()
                    .map(|n| vec![Value::String(n)])
                    .unwrap_or_default()
            })
    };

    // py:422-448  build the prepared segment dict
    let mut out: Map<String, Value> = Map::new();
    out.insert(
        "name".to_string(),
        Value::String(name.clone().unwrap_or_else(|| function_name.clone())),
    );
    out.insert("type".to_string(), Value::String(segment_type.to_string()));
    out.insert(
        "highlight_groups".to_string(),
        Value::Array(highlight_groups),
    );
    out.insert("side".to_string(), Value::String(side.to_string()));
    out.insert("module".to_string(), Value::String(module));
    Some(out)
}

pub fn gen_segment_getter<A>(
    _pl: &(),
    ext: &str,
    _common_config: &Map<String, Value>,
    theme_configs: Vec<Map<String, Value>>,
    default_module: Option<&str>,
    get_module_attr: A,
    _top_theme: Option<&str>,
) -> Box<dyn Fn(&Map<String, Value>, &str) -> Option<Map<String, Value>>>
where
    A: Fn(&str, &str) -> bool + 'static,
{
    // py:255-259  data = {default_module, get_module_attr, segment_data: None}
    let default_module: String = default_module
        .map(String::from)
        .unwrap_or_else(|| format!("powerline.segments.{}", ext));
    let get_module_attr = std::sync::Arc::new(get_module_attr);
    let theme_configs = std::sync::Arc::new(theme_configs);

    // py:319-448  def get(segment, side):
    Box::new(
        move |segment: &Map<String, Value>, side: &str| -> Option<Map<String, Value>> {
            // py:320  segment_type = segment.get('type', 'function')
            let segment_type = segment
                .get("type")
                .and_then(|v| v.as_str())
                .unwrap_or("function")
                .to_string();

            // py:321-325  segment_getters[segment_type] (function/string/segment_list)
            if !matches!(
                segment_type.as_str(),
                "function" | "string" | "segment_list"
            ) {
                return None;
            }

            // py:333  if not get_key(False, segment, module, function_name, name, 'display', True): return None
            // Mirrors the Python `display: false` gate. The full
            // upstream form pulls `display` through `get_segment_key`
            // (theme_configs.segment_data.{module.function|name}.display
            // fallback). Inline form here handles the common case of
            // `display: false` set on the segment dict directly; the
            // segment_data-driven case relies on user theme_data
            // overrides via mergedicts at theme-load time.
            if let Some(false) = segment.get("display").and_then(|v| v.as_bool()) {
                return None;
            }

            // py:327-331  contents, _contents_func, module, function_name, name = get_segment_info(data, segment)
            let (function_name, module, name): (String, String, Option<String>) =
                if segment_type == "string" {
                    // py:73-75  get_string: returns ('contents', None, None, None, name)
                    let n = segment
                        .get("name")
                        .and_then(|v| v.as_str())
                        .map(String::from);
                    (String::new(), String::new(), n)
                } else {
                    // py:61-70  get_function
                    let raw = segment.get("function").and_then(|v| v.as_str())?;
                    let (m, fname) = match raw.rfind('.') {
                        Some(idx) => (raw[..idx].to_string(), raw[idx + 1..].to_string()),
                        None => (default_module.clone(), raw.to_string()),
                    };
                    // py:67-69  function = get_module_attr(module, fname); if not function: raise
                    if !get_module_attr(&m, &fname) {
                        return None;
                    }
                    let n = segment
                        .get("name")
                        .and_then(|v| v.as_str())
                        .map(String::from);
                    (fname, m, n)
                };

            // py:343-346  highlight_groups
            let highlight_groups: Vec<Value> = if segment_type == "function" {
                vec![Value::String(function_name.clone())]
            } else {
                segment
                    .get("highlight_groups")
                    .and_then(|v| v.as_array())
                    .cloned()
                    .unwrap_or_else(|| {
                        name.clone()
                            .map(|n| vec![Value::String(n)])
                            .unwrap_or_default()
                    })
            };

            // py:401-414  contents_func stored as id "module.function_name"
            let contents_func_id = if module.is_empty() {
                String::new()
            } else {
                format!("{}.{}", module, function_name)
            };

            // py:422-448  build the prepared segment dict
            let mut out: Map<String, Value> = Map::new();
            out.insert(
                "name".to_string(),
                Value::String(name.clone().unwrap_or_else(|| function_name.clone())),
            );
            out.insert("type".to_string(), Value::String(segment_type.clone()));
            out.insert(
                "highlight_groups".to_string(),
                Value::Array(highlight_groups),
            );
            out.insert("divider_highlight_group".to_string(), Value::Null);
            out.insert(
                "before".to_string(),
                segment
                    .get("before")
                    .cloned()
                    .unwrap_or_else(|| Value::String(String::new())),
            );
            out.insert(
                "after".to_string(),
                segment
                    .get("after")
                    .cloned()
                    .unwrap_or_else(|| Value::String(String::new())),
            );
            out.insert("contents_func".to_string(), Value::String(contents_func_id));
            // py:430  'contents': contents — string types have a contents value
            out.insert(
                "contents".to_string(),
                if segment_type == "string" {
                    segment.get("contents").cloned().unwrap_or(Value::Null)
                } else {
                    Value::Null
                },
            );
            // py:431  'literal_contents': (0, '')
            out.insert(
                "literal_contents".to_string(),
                Value::Array(vec![Value::from(0), Value::String(String::new())]),
            );
            out.insert(
                "priority".to_string(),
                segment.get("priority").cloned().unwrap_or(Value::Null),
            );
            out.insert(
                "draw_hard_divider".to_string(),
                segment
                    .get("draw_hard_divider")
                    .cloned()
                    .unwrap_or(Value::Bool(true)),
            );
            out.insert(
                "draw_soft_divider".to_string(),
                segment
                    .get("draw_soft_divider")
                    .cloned()
                    .unwrap_or(Value::Bool(true)),
            );
            out.insert(
                "draw_inner_divider".to_string(),
                segment
                    .get("draw_inner_divider")
                    .cloned()
                    .unwrap_or(Value::Bool(false)),
            );
            out.insert("side".to_string(), Value::String(side.to_string()));
            out.insert(
                "width".to_string(),
                segment.get("width").cloned().unwrap_or(Value::Null),
            );
            out.insert(
                "align".to_string(),
                segment
                    .get("align")
                    .cloned()
                    .unwrap_or_else(|| Value::String("l".to_string())),
            );
            out.insert("expand".to_string(), Value::Null);
            out.insert("truncate".to_string(), Value::Null);
            out.insert("startup".to_string(), Value::Null);
            out.insert("shutdown".to_string(), Value::Null);
            // py:265-301  get_segment_selector — mode lists + function
            // names carry over to `Theme.get_segments`'s
            // `gen_display_condition` at `segment.py:303`. The function
            // name strings are resolved at evaluation time through a
            // selectors registry (analog of `gen_module_attr_getter`
            // for selector callables) — bin shim provides one keyed by
            // dotted name (e.g. `powerline.selectors.vim.single_tab`).
            if let Some(v) = segment.get("include_modes") {
                out.insert("include_modes".to_string(), v.clone());
            }
            if let Some(v) = segment.get("exclude_modes") {
                out.insert("exclude_modes".to_string(), v.clone());
            }
            if let Some(v) = segment.get("include_function") {
                out.insert("include_function".to_string(), v.clone());
            }
            if let Some(v) = segment.get("exclude_function") {
                out.insert("exclude_function".to_string(), v.clone());
            }
            out.insert("_rendered_raw".to_string(), Value::String(String::new()));
            out.insert("_rendered_hl".to_string(), Value::String(String::new()));
            out.insert("_len".to_string(), Value::Null);
            out.insert("_contents_len".to_string(), Value::Null);
            // py:349-353  args = get_key(merge=True, segment, module,
            // function_name, name, 'args', {}). Walks the cascade:
            // segment_data["module.fn"]["args"] (top_theme/per-ext) →
            // segment_data[fn]["args"] → segment_data[name]["args"] →
            // inline segment["args"]. All layers deep-merged.
            let mut inline_args: Map<String, Value> = Map::new();
            if let Some(a) = segment.get("args").and_then(|v| v.as_object()) {
                for (k, v) in a {
                    inline_args.insert(k.clone(), v.clone());
                }
            } else {
                // Theme specs sometimes write args as inline keys
                // (interface, format, …) rather than under an "args" map.
                // Carry these into the args dict same as upstream
                // segment_data["args"] would.
                const SPEC_KEYS: &[&str] = &[
                    "function",
                    "name",
                    "type",
                    "args",
                    "before",
                    "after",
                    "draw_hard_divider",
                    "draw_soft_divider",
                    "draw_inner_divider",
                    "priority",
                    "width",
                    "align",
                    "highlight_groups",
                    "include_function",
                    "exclude_function",
                    "include_modes",
                    "exclude_modes",
                ];
                for (k, v) in segment {
                    if !SPEC_KEYS.contains(&k.as_str()) {
                        inline_args.insert(k.clone(), v.clone());
                    }
                }
            }
            // py:7-39  walk theme_configs[*]["segment_data"][...]["args"]
            // and deep-merge under inline args (inline takes precedence,
            // matching py:53 `ret.update(old_ret)`).
            let mut inline_seg = segment.clone();
            inline_seg.insert("args".to_string(), Value::Object(inline_args.clone()));
            let theme_refs: Vec<&Map<String, Value>> = theme_configs.iter().collect();
            let merged = get_segment_key(
                true,
                &inline_seg,
                &theme_refs,
                None,
                "args",
                Some(&function_name),
                name.as_deref(),
                Some(&module),
                Some(Value::Object(Map::new())),
            );
            if let Some(Value::Object(merged_map)) = merged {
                if !merged_map.is_empty() {
                    out.insert("args".to_string(), Value::Object(merged_map));
                }
            } else if !inline_args.is_empty() {
                out.insert("args".to_string(), Value::Object(inline_args));
            }

            Some(out)
        },
    )
}

/// Port of module-level binding `get_fallback_segment` from
/// `powerline/segment.py:227`.
///
/// Python: a frozen-dict-template + `.copy` callable; each invocation
/// produces a fresh dict for use as the fallback when a segment fails
/// to render. Rust port builds the same shape via a constructor fn.
pub fn get_fallback_segment() -> Map<String, Value> {
    // py:227
    let mut m = Map::new();
    m.insert("name".into(), Value::String("fallback".into())); // py:228
    m.insert("type".into(), Value::String("string".into())); // py:229
    m.insert(
        "highlight_groups".into(),
        Value::Array(vec![Value::String("background".into())]), // py:230
    );
    m.insert("divider_highlight_group".into(), Value::Null); // py:231
    m.insert("before".into(), Value::Null); // py:232
    m.insert("after".into(), Value::Null); // py:233
    m.insert("contents".into(), Value::String("".into())); // py:234
    m.insert(
        "literal_contents".into(),
        Value::Array(vec![Value::from(0), Value::String("".into())]), // py:235
    );
    m.insert("priority".into(), Value::Null); // py:236
    m.insert("draw_soft_divider".into(), Value::Bool(true)); // py:237
    m.insert("draw_hard_divider".into(), Value::Bool(true)); // py:238
    m.insert("draw_inner_divider".into(), Value::Bool(true)); // py:239
                                                              // py:240  'display_condition': always_true — modeled as missing
                                                              // (callers handle missing key as always_true; the fn-pointer
                                                              // marshaling into a JSON Value is deferred to the dispatch port).
    m.insert("width".into(), Value::Null); // py:241
    m.insert("align".into(), Value::Null); // py:242
    m.insert("expand".into(), Value::Null); // py:243
    m.insert("truncate".into(), Value::Null); // py:244
    m.insert("startup".into(), Value::Null); // py:245
    m.insert("shutdown".into(), Value::Null); // py:246
    m.insert("_rendered_raw".into(), Value::String("".into())); // py:247
    m.insert("_rendered_hl".into(), Value::String("".into())); // py:248
    m.insert("_len".into(), Value::Null); // py:249
    m.insert("_contents_len".into(), Value::Null); // py:250
    m
}

/// Result of `get_function()` / `get_string()` dispatch. Mirrors the
/// 5-element tuple Python returns at py:70 / py:75:
/// `(contents_string, function, module, function_name, name)`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SegmentGetterResult {
    /// First tuple slot — the literal contents string (or None for
    /// function segments).
    pub contents: Option<String>,
    /// Second tuple slot — the resolved function name (or None for
    /// string segments).
    pub function_name: Option<String>,
    /// Third tuple slot — the resolved module name (or None for
    /// string segments).
    pub module: Option<String>,
    /// Fourth tuple slot — duplicate of `function_name` per the
    /// Python tuple shape; preserved for parity with py:70.
    pub function_name_dup: Option<String>,
    /// Fifth tuple slot — the segment's optional name from
    /// `segment.get('name')`.
    pub name: Option<String>,
}

/// Port of `get_function()` from
/// `powerline/segment.py:61-70`.
///
/// Resolves the segment's `function` field to a `(module,
/// function_name)` pair using rpartition on `.`. Falls back to
/// `default_module` when undotted per py:65-66.
///
/// `import_module_attr` is the caller-supplied closure analog of
/// `data['get_module_attr']` at py:67. Returns Err matching Python's
/// `ImportError('Failed to obtain segment function')` per py:68-69
/// when the import returns nothing.
pub fn get_function(
    segment: &Map<String, Value>,
    default_module: &str,
    import_module_attr: impl FnOnce(&str, &str) -> Option<()>,
) -> Result<SegmentGetterResult, String> {
    // py:62  function_name = segment['function']
    let raw_name = segment
        .get("function")
        .and_then(|v| v.as_str())
        .ok_or_else(|| "segment has no 'function' key".to_string())?;

    // py:63-66  rpartition on '.' else default_module
    let (module, function_name) = match raw_name.rfind('.') {
        Some(idx) => (raw_name[..idx].to_string(), raw_name[idx + 1..].to_string()),
        None => (default_module.to_string(), raw_name.to_string()),
    };

    // py:67  function = data['get_module_attr'](module, function_name, prefix='segment_generator')
    let imported = import_module_attr(&module, &function_name);
    // py:68-69  if not function: raise ImportError(...)
    if imported.is_none() {
        return Err("Failed to obtain segment function".to_string());
    }

    // py:70  return None, function, module, function_name, segment.get('name')
    let name = segment
        .get("name")
        .and_then(|v| v.as_str())
        .map(String::from);
    Ok(SegmentGetterResult {
        contents: None,
        function_name: Some(function_name.clone()),
        module: Some(module),
        function_name_dup: Some(function_name),
        name,
    })
}

/// Port of module-level `segment_getters` dict from
/// `powerline/segment.py:78-82`.
///
/// Returns the resolver name for the given segment type:
/// `"function"` / `"segment_list"` → `get_function`,
/// `"string"` → `get_string`. Used by the dispatch driver to route
/// each segment to its resolver.
pub fn segment_getter_name(segment_type: &str) -> Option<&'static str> {
    // py:78-82
    match segment_type {
        "function" => Some("get_function"),
        "string" => Some("get_string"),
        "segment_list" => Some("get_function"),
        _ => None,
    }
}

/// Closure produced by [`get_attr_func`] for is_space_func=true.
/// Mirrors the Python `expand_func(pl, amount, segment)` closure at
/// `powerline/segment.py:92-97`.
pub type SpaceExpandFn = Box<dyn Fn(&(), usize, &Map<String, Value>) -> String>;

/// Closure produced by [`get_attr_func`] for is_space_func=false.
/// Mirrors the Python `lambda pl, shutdown_event: func(...)` at
/// `powerline/segment.py:100`.
pub type StartupFn = Box<dyn Fn(&(), &std::sync::atomic::AtomicBool)>;

/// Output of `get_attr_func` — one of two closure shapes depending
/// on `is_space_func`. Mirrors the Python branch at py:91 vs py:99.
pub enum AttrFunc {
    /// Closure suitable for `expand` callbacks (py:92-97).
    Space(SpaceExpandFn),
    /// Closure suitable for `startup` / `shutdown` callbacks
    /// (py:100).
    Plain(StartupFn),
    /// Python: `return None` per py:88-89 when contents_func has no
    /// `key` attribute.
    None,
}

impl AttrFunc {
    /// True when this is `AttrFunc::None`.
    pub fn is_none(&self) -> bool {
        matches!(self, AttrFunc::None)
    }
}

/// Port of the inner `expand_func()` closure from
/// `powerline/segment.py:92-98`.
///
/// Space-function dispatcher: calls the underlying contents-func with
/// `(pl, amount, segment, **args)` and returns its output. On
/// exception, returns `segment['contents'] + ' ' * amount` per the
/// fallback at py:96-97.
///
/// Python embeds this closure inside `get_attr_func` to capture
/// `func` + `args` from the outer scope. The Rust port surfaces it as
/// a free fn taking the underlying call as a closure so the fallback
/// path is independently testable.
pub fn expand_func<F>(pl: &(), amount: usize, segment: &Map<String, Value>, call_func: F) -> String
where
    F: FnOnce() -> Result<String, String>,
{
    // py:92  def expand_func(pl, amount, segment):
    // py:93  try:
    // py:94  return func(pl=pl, amount=amount, segment=segment, **args)
    match call_func() {
        Ok(s) => s,
        Err(_e) => {
            // py:95  except Exception as e:
            // py:96  pl.exception(...)
            let _ = pl;
            // py:97  return segment['contents'] + (' ' * amount)
            let contents = segment
                .get("contents")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            format!("{}{}", contents, " ".repeat(amount))
        }
    }
}

/// Port of `get_attr_func()` from
/// `powerline/segment.py:85-100`.
///
/// `func_lookup` resolves the attribute on `contents_func` (Python's
/// `getattr(contents_func, key)`). Returns None when lookup fails
/// per py:87-89.
///
/// When `is_space_func` is true the returned closure has the
/// `expand_func(pl, amount, segment)` signature (py:92-97); the
/// fallback path at py:97 appends `' ' * amount` to `segment['contents']`.
/// Otherwise the returned closure has the `startup(pl,
/// shutdown_event)` signature per py:100.
pub fn get_attr_func<F>(func_lookup: F, is_space_func: bool) -> AttrFunc
where
    F: FnOnce() -> Option<()>,
{
    // py:86-89  try getattr; except AttributeError: return None
    if func_lookup().is_none() {
        return AttrFunc::None;
    }

    // py:90-98  is_space_func branch
    if is_space_func {
        AttrFunc::Space(Box::new(
            |_pl: &(), amount: usize, segment: &Map<String, Value>| -> String {
                // py:97  fallback path: segment['contents'] + ' ' * amount
                let contents = segment
                    .get("contents")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                format!("{}{}", contents, " ".repeat(amount))
            },
        ))
    } else {
        // py:99-100  startup callback
        AttrFunc::Plain(Box::new(
            |_pl: &(), _shutdown_event: &std::sync::atomic::AtomicBool| {
                // py:100  func(pl=pl, shutdown_event=shutdown_event, **args)
                // The Rust port can't carry the real func through the
                // closure boundary since contents_func is a Python
                // object pointer; this is a structural stub.
            },
        ))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn always_true_is_always_true() {
        assert!(always_true(&(), None, None));
        assert!(always_true(&(), None, Some("normal")));
    }

    #[test]
    fn get_fallback_segment_has_expected_shape() {
        let s = get_fallback_segment();
        assert_eq!(s.get("name").and_then(|v| v.as_str()), Some("fallback"));
        assert_eq!(s.get("type").and_then(|v| v.as_str()), Some("string"));
        assert_eq!(
            s.get("highlight_groups")
                .and_then(|v| v.as_array())
                .map(|a| a.len()),
            Some(1)
        );
        assert_eq!(s.get("contents").and_then(|v| v.as_str()), Some(""));
        let lc = s
            .get("literal_contents")
            .and_then(|v| v.as_array())
            .unwrap();
        assert_eq!(lc[0].as_u64(), Some(0));
        assert_eq!(lc[1].as_str(), Some(""));
    }

    #[test]
    fn list_segment_key_values_finds_segment_key_first() {
        let mut seg = Map::new();
        seg.insert("contents".into(), json!("hello"));
        let theme_configs: &[&Map<String, Value>] = &[];
        let vals = list_segment_key_values(
            &seg,
            theme_configs,
            None,
            "contents",
            None,
            None,
            None,
            Some(json!("DEFAULT")),
        );
        assert_eq!(vals[0], json!("hello"));
        assert_eq!(vals[vals.len() - 1], json!("DEFAULT"));
    }

    #[test]
    fn get_segment_key_merge_collapses_dicts_old_wins() {
        let mut seg = Map::new();
        seg.insert("args".into(), json!({"a": 1, "b": 2}));
        let theme_config = json!({
            "segment_data": {
                "func_name": {"args": {"b": 99, "c": 3}}
            }
        })
        .as_object()
        .unwrap()
        .clone();
        let theme_configs: Vec<&Map<String, Value>> = vec![&theme_config];

        let merged = get_segment_key(
            true,
            &seg,
            &theme_configs,
            None,
            "args",
            Some("func_name"),
            None,
            None,
            Some(json!({})),
        );
        // Segment-level wins: a=1, b=2 (not 99), c=3 from theme config.
        let merged = merged.unwrap();
        let merged_obj = merged.as_object().unwrap();
        assert_eq!(merged_obj.get("a"), Some(&json!(1)));
        assert_eq!(merged_obj.get("b"), Some(&json!(2)));
        assert_eq!(merged_obj.get("c"), Some(&json!(3)));
    }

    #[test]
    fn get_segment_key_no_merge_returns_first() {
        let mut seg = Map::new();
        seg.insert("priority".into(), json!(10));
        let theme_configs: &[&Map<String, Value>] = &[];
        let v = get_segment_key(
            false,
            &seg,
            theme_configs,
            None,
            "priority",
            None,
            None,
            None,
            Some(json!(0)),
        );
        assert_eq!(v, Some(json!(10)));
    }

    #[test]
    fn set_segment_highlighting_basic() {
        use crate::ported::colorscheme::Colorscheme;
        let colorscheme_config = json!({
            "groups": {"info": {"fg": "white", "bg": "blue", "attrs": []}}
        })
        .as_object()
        .unwrap()
        .clone();
        let colors_config = json!({
            "colors": {"white": [231, "ffffff"], "blue": [21, "0000ff"]},
            "gradients": {}
        })
        .as_object()
        .unwrap()
        .clone();
        let cs = Colorscheme::new(&colorscheme_config, &colors_config);

        let mut segment = Map::new();
        segment.insert("highlight_groups".into(), json!(["info"]));
        segment.insert("literal_contents".into(), json!([0, ""]));

        assert!(set_segment_highlighting(&(), &cs, &mut segment, None));
        assert!(segment.contains_key("highlight"));
        let hl = segment
            .get("highlight")
            .and_then(|v| v.as_object())
            .unwrap();
        assert!(hl.contains_key("fg"));
        assert!(hl.contains_key("bg"));
        assert!(hl.contains_key("attrs"));
    }

    #[test]
    fn get_function_dotted_name_splits_via_rpartition() {
        // py:63-64
        let mut seg = Map::new();
        seg.insert(
            "function".to_string(),
            json!("powerline.segments.shell.uptime"),
        );
        seg.insert("name".to_string(), json!("custom"));
        let r = get_function(&seg, "powerline.segments", |_, _| Some(())).unwrap();
        assert_eq!(r.module.as_deref(), Some("powerline.segments.shell"));
        assert_eq!(r.function_name.as_deref(), Some("uptime"));
        assert_eq!(r.function_name_dup.as_deref(), Some("uptime"));
        assert!(r.contents.is_none());
        assert_eq!(r.name.as_deref(), Some("custom"));
    }

    #[test]
    fn get_function_undotted_uses_default_module() {
        // py:65-66
        let mut seg = Map::new();
        seg.insert("function".to_string(), json!("uptime"));
        let r = get_function(&seg, "powerline.segments.shell", |_, _| Some(())).unwrap();
        assert_eq!(r.module.as_deref(), Some("powerline.segments.shell"));
        assert_eq!(r.function_name.as_deref(), Some("uptime"));
    }

    #[test]
    fn get_function_missing_function_key_returns_err() {
        let seg = Map::new();
        let r = get_function(&seg, "powerline.segments.shell", |_, _| Some(()));
        assert!(r.is_err());
    }

    #[test]
    fn get_function_failed_import_returns_err() {
        // py:68-69  if not function: raise ImportError
        let mut seg = Map::new();
        seg.insert("function".to_string(), json!("missing_fn"));
        let r = get_function(&seg, "powerline.segments.shell", |_, _| None);
        let err = r.unwrap_err();
        assert!(err.contains("Failed to obtain segment function"));
    }

    #[test]
    fn get_function_passes_resolved_args_to_importer() {
        // The closure should see (module, function_name) after the split.
        let mut seg = Map::new();
        seg.insert("function".to_string(), json!("my.mod.fn_name"));
        use std::cell::Cell;
        let captured_module: Cell<String> = Cell::new(String::new());
        let captured_fn: Cell<String> = Cell::new(String::new());
        let _ = get_function(&seg, "fallback", |m, n| {
            captured_module.set(m.to_string());
            captured_fn.set(n.to_string());
            Some(())
        });
        assert_eq!(captured_module.into_inner(), "my.mod");
        assert_eq!(captured_fn.into_inner(), "fn_name");
    }

    #[test]
    fn segment_getter_name_dispatches_by_type() {
        // py:78-82
        assert_eq!(segment_getter_name("function"), Some("get_function"));
        assert_eq!(segment_getter_name("segment_list"), Some("get_function"));
        assert_eq!(segment_getter_name("string"), Some("get_string"));
        assert_eq!(segment_getter_name("bogus"), None);
    }

    #[test]
    fn get_attr_func_no_attribute_returns_none() {
        // py:87-89
        let r = get_attr_func(|| None, false);
        assert!(r.is_none());
    }

    #[test]
    fn get_attr_func_is_space_func_returns_expand_closure() {
        // py:91-97
        let r = get_attr_func(|| Some(()), true);
        match r {
            AttrFunc::Space(f) => {
                let mut seg = Map::new();
                seg.insert("contents".to_string(), json!("hi"));
                // The closure exists; verify its signature works.
                let out = f(&(), 3, &seg);
                // Falls through to the py:97 fallback (no real func attached)
                assert_eq!(out, "hi   ");
            }
            _ => panic!("expected Space variant"),
        }
    }

    #[test]
    fn get_attr_func_not_space_func_returns_plain_closure() {
        // py:99-100
        let r = get_attr_func(|| Some(()), false);
        match r {
            AttrFunc::Plain(_) => {} // OK
            _ => panic!("expected Plain variant"),
        }
    }

    #[test]
    fn attr_func_is_none_helper() {
        assert!(AttrFunc::None.is_none());
        assert!(!AttrFunc::Space(Box::new(|_, _, _| String::new())).is_none());
    }

    #[test]
    fn get_key_passes_through_to_get_segment_key() {
        // py:261-263  get_key is a thin wrapper.
        let mut seg = Map::new();
        seg.insert("display".to_string(), json!(true));
        let r = get_key(false, &seg, &[], None, None, None, None, "display", None);
        assert_eq!(r, Some(json!(true)));
    }

    #[test]
    fn get_key_falls_back_to_default_when_absent() {
        let seg = Map::new();
        let r = get_key(
            false,
            &seg,
            &[],
            None,
            None,
            None,
            None,
            "absent_key",
            Some(json!("dflt")),
        );
        assert_eq!(r, Some(json!("dflt")));
    }

    #[test]
    fn get_selector_resolves_dotted_function_name() {
        // py:266-267  rpartition('.') split
        let r = get_selector("foo.bar.baz", "shell", |m, f| m == "foo.bar" && f == "baz");
        assert_eq!(r, Some(("foo.bar".to_string(), "baz".to_string())));
    }

    #[test]
    fn get_selector_uses_default_selectors_module_for_undotted_name() {
        // py:268-269  default: powerline.selectors.<ext>
        let r = get_selector("some_fn", "shell", |m, f| {
            m == "powerline.selectors.shell" && f == "some_fn"
        });
        assert_eq!(
            r,
            Some((
                "powerline.selectors.shell".to_string(),
                "some_fn".to_string()
            ))
        );
    }

    #[test]
    fn get_selector_returns_none_when_function_missing() {
        // py:271-272  if not function: return None
        let r = get_selector("missing_fn", "shell", |_, _| false);
        assert_eq!(r, None);
    }

    #[test]
    fn get_segment_selector_combines_modes_and_function() {
        // py:287-292  modes OR function
        let mut seg = Map::new();
        seg.insert("include_modes".to_string(), json!(["normal", "visual"]));
        let func: Box<dyn Fn(&str) -> bool> = Box::new(|m| m == "insert");
        let pred = get_segment_selector(&seg, "include", Some(func)).unwrap();
        assert!(pred("normal"));
        assert!(pred("visual"));
        assert!(pred("insert"));
        assert!(!pred("command"));
    }

    #[test]
    fn get_segment_selector_modes_only_returns_membership_check() {
        // py:294
        let mut seg = Map::new();
        seg.insert("exclude_modes".to_string(), json!(["v"]));
        let pred = get_segment_selector(&seg, "exclude", None).unwrap();
        assert!(pred("v"));
        assert!(!pred("n"));
    }

    #[test]
    fn get_segment_selector_no_modes_no_function_returns_none() {
        // py:301
        let seg = Map::new();
        assert!(get_segment_selector(&seg, "include", None).is_none());
    }

    #[test]
    fn gen_display_condition_no_conditions_always_displays() {
        // py:317  always_true
        let pred = gen_display_condition(None, None);
        assert!(pred("any_mode"));
    }

    #[test]
    fn gen_display_condition_include_only_uses_include() {
        // py:312
        let inc: Box<dyn Fn(&str) -> bool> = Box::new(|m| m == "n");
        let pred = gen_display_condition(Some(inc), None);
        assert!(pred("n"));
        assert!(!pred("v"));
    }

    #[test]
    fn gen_display_condition_exclude_only_negates() {
        // py:315
        let exc: Box<dyn Fn(&str) -> bool> = Box::new(|m| m == "n");
        let pred = gen_display_condition(None, Some(exc));
        assert!(!pred("n"));
        assert!(pred("v"));
    }

    #[test]
    fn gen_display_condition_both_uses_and_not() {
        // py:306-310  include AND NOT exclude
        let inc: Box<dyn Fn(&str) -> bool> = Box::new(|m| matches!(m, "n" | "v"));
        let exc: Box<dyn Fn(&str) -> bool> = Box::new(|m| m == "v");
        let pred = gen_display_condition(Some(inc), Some(exc));
        assert!(pred("n"));
        assert!(!pred("v"));
        assert!(!pred("i"));
    }

    #[test]
    fn get_dispatches_function_type_with_dotted_name() {
        // py:319-448  function segment dispatch
        let mut seg = Map::new();
        seg.insert("function".to_string(), json!("foo.bar.baz"));
        let r = get(&seg, "left", "powerline.segments", |m, f| {
            m == "foo.bar" && f == "baz"
        });
        assert!(r.is_some());
        let out = r.unwrap();
        assert_eq!(out["name"], "baz");
        assert_eq!(out["type"], "function");
        assert_eq!(out["side"], "left");
    }

    #[test]
    fn get_returns_none_when_display_false() {
        // py:333  display=false gate
        let mut seg = Map::new();
        seg.insert(
            "function".to_string(),
            json!("powerline.segments.shell.mode"),
        );
        seg.insert("display".to_string(), json!(false));
        let r = get(&seg, "right", "powerline.segments", |_, _| true);
        assert!(r.is_none());
    }

    #[test]
    fn get_returns_none_for_unknown_segment_type() {
        // py:321-325  segment_getters[segment_type] KeyError
        let mut seg = Map::new();
        seg.insert("type".to_string(), json!("unknown"));
        let r = get(&seg, "left", "powerline.segments", |_, _| true);
        assert!(r.is_none());
    }
}
