Open FelixZY opened 3 weeks ago
I think that's a good fit for a built-in widget: it's not trivial to make with a grid
, and it's a common enough CLI pattern.
Let's collect some more requirements before we start implementing: are there any other tools that display tree-like data that we could take inspiration from? For example, should the output be colored by default, do tree entries have descriptions etc.
I don't have good answers for most of the questions posed and am not experienced enough with mordant to know if coloured by default is a good fit. I'll try to bring a few things to the discussion though:
With regards to existing tools, tree
is the obvious example. I believe exa has tree layout support as well.
Syntax wise, I'm thinking something like what Daisy UI does for their menu with submenu component would be simplest. Translated to DSL, perhaps something like
tree {
leaf {
}
tree {
leaf {
}
}
}
Personally I'd love multi-line support for "leaves". I think that's more uncommon when looking at other tree-like tools but it would be great for many use-cases.
Being able to show other widgets, such as tables, inside a leaf would be dreamy but then again, perhaps not as hard to implement as one would think if multi-line leaves are supported.
Also, terminology should probably be based on the tree data structure.
I'm noticing a symmetry in the structure: nested nodes are drawn with exactly the same characters as the top level tree. So if each entry of the tree can be a widget, you wouldn't need to differentiate between leaf and middle nodes in a dsl, you could do something like
class Entry(title: String, content: Widget? = null)
class Tree(vararg entries: Entry): Widget
And the have the class definition like
Tree(
Entry("bin", Tree(
Entry("parse-ansi-codes.rs"),
)),
Entry("Cargo.lock"),
Entry("Cargo.toml"),
Entry("README.md"),
Entry("src", Tree(
Entry("cursor.rs"),
Entry("lib.rs"),
Entry("style.rs"),
)),
Entry("target", Tree(
Entry("debug"),
)),
Entry("test"),
)
with a dsl like
tree {
entry("bin", tree {
entry("parse-ansi-codes.rs")
})
entry("Cargo.lock")
entry("Cargo.toml")
entry("README.md")
entry("src",
tree {
entry("cursor.rs")
entry("lib.rs")
entry("style.rs")
}
}
entry("target", tree {
entry("debug")
})
entry("test")
}
I'm not sure I'm a fan of forcing a title: String
on every entry. Also, I think it would be nice if tree
could be used without wrapping it in an entry
. Example:
data class Widget(val text: String)
sealed interface TreeNode
data class Leaf(val content: Widget) : TreeNode
data class Tree(val content: Widget? = null, val entries: List<TreeNode>) : TreeNode {
constructor(content: Widget? = null, vararg entries: TreeNode) : this(content, entries.toList())
data class Builder(val content: Widget? = null, var entries: MutableList<TreeNode> = mutableListOf<TreeNode>()) {
fun tree(content: Widget? = null, init: Tree.Builder.() -> Unit) {
entries.add(
Tree.Builder(content).apply { init() }.build()
)
}
fun leaf(init: () -> Widget) {
entries.add(Leaf(init()))
}
fun build(): Tree = Tree(content, entries)
}
}
fun tree(content: Widget? = null, init: Tree.Builder.() -> Unit): Tree =
Tree.Builder(content).apply { init() }.build()
fun main() {
val normalTree = Tree(
content = Widget("Hotel"),
Tree(
content = Widget("Floor 1"),
Leaf(Widget("Room 100")),
Leaf(Widget("Room 101")),
Leaf(Widget("Room 102"))
),
Tree(
content = Widget("Floor 2"),
Leaf(Widget("Room 200")),
Leaf(Widget("Room 201")),
Leaf(Widget("Room 202"))
)
)
val dslTree = tree(Widget("Hotel")) {
tree(Widget("Floor 1")) {
leaf {
Widget("Room 100")
}
leaf {
Widget("Room 101")
}
leaf {
Widget("Room 102")
}
}
tree(Widget("Floor 2")) {
leaf {
Widget("Room 200")
}
leaf {
Widget("Room 201")
}
leaf {
Widget("Room 202")
}
}
}
println(normalTree)
println(dslTree)
println(normalTree == dslTree)
}
Playground link: https://pl.kotl.in/o-95F5FZ1
Note that data class Widget(val text: String)
is a standin for mordant's Widget
class, allowing the above code to compile.
Thanks for all the feedback! I'll definitely have overloads for specifying entries as either a string or a widget, like I do for all the other widget builders.
The reason I think that a list of entries is preferable than a split content
/entires
, is because a tree might have only leaf nodes (or more than one root) like this:
├─ Cargo.lock
├─ Cargo.toml
└─ test
And a single separate content
doesn't fit well in that case.
Not sure I see the problem - wouldn't that just be
tree {
leaf { "Cargo.lock" }
leaf { "Cargo.toml" }
leaf { "test" }
}
?
EDIT
I.e.
[content]
├─ [entries[0]]
├─ [entries[1]]
└─ [entries[2]]
or
[root.content]
├── [root.entries[0].content]
│ ├── [root.entries[0].entries[0]]
│ ├── [root.entries[0].entries[1]]
│ └── [root.entries[0].entries[2]]
└── [root.entries[1].content]
├── [root.entries[1].entries[0]]
├── [root.entries[1].entries[1]]
└── [root.entries[1].entries[2]]
I guess index 0 could be made "special" to mean the current node's content but I think it makes sense to use explicit syntax.
EDIT2
Note that I imagine Tree.content
as optional. If there is no content, I'd imagine the "root" of the relevant subtree (or root tree) to render as an empty space or possibly ┐
or .
/similar.
How do you mean a tree could have more than one root?
I would like to render a tree view, similar to the linux
tree
command: