Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/tree/src/tree.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::path::Path;

use promkit::{preset::tree::Tree, widgets::tree::node::Node, Prompt};
use promkit::{preset::tree::Tree, widgets::structured::tree::Document, Prompt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../promkit/src");
let ret = Tree::new(Node::try_from(&root)?)
let document = Document::from_path(&root)?;
let ret = Tree::new(document)
.title("Select a directory or file")
.tree_lines(10)
.run()
Expand Down
6 changes: 1 addition & 5 deletions promkit-widgets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub mod cursor;
#[cfg_attr(docsrs, doc(cfg(feature = "checkbox")))]
pub mod checkbox;

#[cfg(any(feature = "json", feature = "yaml"))]
#[cfg(any(feature = "json", feature = "yaml", feature = "tree"))]
pub mod structured;
Comment on lines +11 to 12
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the tree functionality moved under structured, enabling the tree feature no longer provides a top-level promkit_widgets::tree module. This is a breaking public API change for downstream users. If backward compatibility is desired, consider adding a pub use structured::tree as tree; (behind the tree feature) or a small shim module re-exporting the previous public types.

Copilot uses AI. Check for mistakes.

#[cfg(feature = "json")]
Expand Down Expand Up @@ -41,10 +41,6 @@ pub mod status;
#[cfg_attr(docsrs, doc(cfg(feature = "texteditor")))]
pub mod text_editor;

#[cfg(feature = "tree")]
#[cfg_attr(docsrs, doc(cfg(feature = "tree")))]
pub mod tree;

#[cfg(feature = "spinner")]
#[cfg_attr(docsrs, doc(cfg(feature = "spinner")))]
pub mod spinner;
4 changes: 4 additions & 0 deletions promkit-widgets/src/structured/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ pub mod json;
#[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
pub mod yaml;

#[cfg(feature = "tree")]
#[cfg_attr(docsrs, doc(cfg(feature = "tree")))]
pub mod tree;

/// Container type of structured widget.
#[derive(Clone, Debug, PartialEq)]
pub enum ContainerType {
Expand Down
60 changes: 60 additions & 0 deletions promkit-widgets/src/structured/tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use promkit_core::{Widget, grapheme::StyledGraphemes};

mod document;
pub use document::Document;
pub mod config;
pub use config::Config;
pub mod path;
pub mod treez;
pub use treez::Row;

/// Represents the state of a tree structure within the application.
#[derive(Clone)]
pub struct State {
pub document: Document,
pub config: Config,
}

impl Widget for State {
fn create_graphemes(&self, _width: u16, height: u16) -> StyledGraphemes {
let symbol = |row: &Row| -> &str {
if row.has_children && !row.collapsed {
&self.config.unfolded_symbol
} else {
&self.config.folded_symbol
}
};

let height = match self.config.lines {
Some(lines) => lines.min(height as usize),
None => height as usize,
};

let rows = self.document.extract_rows_from_current(height);
let lines = rows.iter().enumerate().map(|(offset, row)| {
if offset == 0 {
StyledGraphemes::from_str(
format!(
"{}{}{}",
symbol(row),
" ".repeat(row.depth * self.config.indent),
row.id,
),
self.config.active_item_style,
)
} else {
StyledGraphemes::from_str(
format!(
"{}{}{}",
" ".repeat(StyledGraphemes::from(symbol(row)).widths()),
" ".repeat(row.depth * self.config.indent),
row.id,
),
self.config.inactive_item_style,
)
}
});

StyledGraphemes::from_lines(lines)
}
}
80 changes: 80 additions & 0 deletions promkit-widgets/src/structured/tree/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::structured::RowOperation;

use super::{Row, path::PathAdapter, treez::Adapter};

/// Represents a navigable tree document, allowing for efficient row navigation and folding.
#[derive(Clone)]
pub struct Document {
rows: Vec<Row>,
position: usize,
}

impl Document {
pub fn new(rows: Vec<Row>) -> Self {
Self { rows, position: 0 }
}

pub fn from_path(path: &std::path::Path) -> anyhow::Result<Self> {
Ok(Self::new(PathAdapter.create_rows(&path.to_path_buf())?))
}
}

impl Document {
/// Returns a reference to the underlying vector of rows.
pub fn rows(&self) -> &[Row] {
&self.rows
}

/// Returns the selected tree path represented by visible row labels.
pub fn get(&self) -> Vec<String> {
self.rows
.get(self.position)
.map(|row| row.path.clone())
.unwrap_or_default()
}

/// Extract rows from the current cursor position.
pub fn extract_rows_from_current(&self, n: usize) -> Vec<Row> {
self.rows.extract(self.position, n)
}

/// Toggles the visibility of a node at the cursor's current position.
pub fn toggle(&mut self) {
let index = self.rows.toggle(self.position);
self.position = index;
}

/// Sets the visibility of all rows.
pub fn set_nodes_visibility(&mut self, collapsed: bool) {
self.rows.set_rows_visibility(collapsed);
self.position = self.rows.head();
}

/// Moves the cursor backward through rows.
pub fn up(&mut self) -> bool {
let index = self.rows.up(self.position);
let ret = index != self.position;
self.position = index;
ret
}

/// Moves the cursor to the head position.
pub fn head(&mut self) -> bool {
self.position = self.rows.head();
true
}

/// Moves the cursor forward through rows.
pub fn down(&mut self) -> bool {
let index = self.rows.down(self.position);
let ret = index != self.position;
self.position = index;
ret
}

/// Moves the cursor to the last position.
pub fn tail(&mut self) -> bool {
self.position = self.rows.tail();
true
}
}
45 changes: 45 additions & 0 deletions promkit-widgets/src/structured/tree/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use std::{fs, path};

use super::treez::Adapter;

pub struct PathAdapter;

impl Adapter for PathAdapter {
type Node = path::PathBuf;
type Error = anyhow::Error;

fn id_of(&self, node: &Self::Node) -> Result<String, Self::Error> {
node.file_name()
.and_then(|name| name.to_str())
.map(ToOwned::to_owned)
.or_else(|| {
let rendered = node.display().to_string();
(!rendered.is_empty()).then_some(rendered)
})
.ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))
}

fn children_of(&self, node: &Self::Node) -> Result<Vec<Self::Node>, Self::Error> {
if !node.is_dir() {
return Ok(Vec::new());
}

let mut directories = Vec::new();
let mut files = Vec::new();

for entry in fs::read_dir(node)? {
let path = entry?.path();
if path.is_dir() {
directories.push(path);
} else if path.is_file() {
files.push(path);
}
}

directories.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
files.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
directories.extend(files);

Ok(directories)
}
}
Loading
Loading