mdbub 0.3.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mdbub/__init__.py +63 -0
- mdbub/cli.py +122 -0
- mdbub/commands/__init__.py +0 -0
- mdbub/commands/about.py +99 -0
- mdbub/commands/export.py +9 -0
- mdbub/commands/print_kv.py +59 -0
- mdbub/commands/print_links.py +24 -0
- mdbub/commands/print_tags.py +40 -0
- mdbub/commands/quick.py +1471 -0
- mdbub/commands/quickmode_config.py +141 -0
- mdbub/commands/tag_utils.py +0 -0
- mdbub/commands/version.py +67 -0
- mdbub/commands/view.py +9 -0
- mdbub/core/__init__.py +0 -0
- mdbub/core/mindmap.py +241 -0
- mdbub/core/mindmap_utils.py +33 -0
- mdbub/symbols.py +68 -0
- mdbub-0.3.7.dist-info/LICENSE +201 -0
- mdbub-0.3.7.dist-info/METADATA +182 -0
- mdbub-0.3.7.dist-info/RECORD +22 -0
- mdbub-0.3.7.dist-info/WHEEL +4 -0
- mdbub-0.3.7.dist-info/entry_points.txt +4 -0
mdbub/__init__.py
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
"""
|
2
|
+
mdbub - Terminal-first interactive mindmap CLI tool with extended markdown support
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import subprocess
|
7
|
+
from datetime import datetime
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
9
|
+
|
10
|
+
__version__ = "0.3.0"
|
11
|
+
|
12
|
+
|
13
|
+
def get_version() -> str:
|
14
|
+
try:
|
15
|
+
__version__ = version("mdbub")
|
16
|
+
except PackageNotFoundError:
|
17
|
+
__version__ = "unknown"
|
18
|
+
return __version__
|
19
|
+
|
20
|
+
|
21
|
+
def _get_build_info() -> str:
|
22
|
+
"""Get comprehensive build information."""
|
23
|
+
info_parts = []
|
24
|
+
|
25
|
+
# Git commit hash
|
26
|
+
try:
|
27
|
+
commit = (
|
28
|
+
subprocess.check_output(
|
29
|
+
["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL
|
30
|
+
)
|
31
|
+
.decode("ascii")
|
32
|
+
.strip()
|
33
|
+
)
|
34
|
+
info_parts.append(f"git:{commit}")
|
35
|
+
except Exception:
|
36
|
+
info_parts.append("git:unknown")
|
37
|
+
|
38
|
+
# Build date (from environment variable if available, otherwise current time)
|
39
|
+
build_date = os.environ.get("BUILD_DATE")
|
40
|
+
if not build_date:
|
41
|
+
try:
|
42
|
+
# Try to get git commit date
|
43
|
+
build_date = (
|
44
|
+
subprocess.check_output(
|
45
|
+
["git", "log", "-1", "--format=%ci"], stderr=subprocess.DEVNULL
|
46
|
+
)
|
47
|
+
.decode("ascii")
|
48
|
+
.strip()
|
49
|
+
.split()[0] # Just the date part
|
50
|
+
)
|
51
|
+
except Exception:
|
52
|
+
build_date = datetime.now().strftime("%Y-%m-%d")
|
53
|
+
|
54
|
+
info_parts.append(f"date:{build_date}")
|
55
|
+
|
56
|
+
# Build environment
|
57
|
+
build_env = os.environ.get("BUILD_ENV", "dev")
|
58
|
+
info_parts.append(f"env:{build_env}")
|
59
|
+
|
60
|
+
return " ".join(info_parts)
|
61
|
+
|
62
|
+
|
63
|
+
BUILD_INFO = _get_build_info()
|
mdbub/cli.py
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
import typer
|
2
|
+
|
3
|
+
from mdbub import BUILD_INFO
|
4
|
+
from mdbub import __version__ as VERSION
|
5
|
+
from mdbub.commands import about, version
|
6
|
+
from mdbub.commands.quick import main as quick_main
|
7
|
+
|
8
|
+
app = typer.Typer(
|
9
|
+
help="""
|
10
|
+
mdbub: Interactive mindmap CLI tool.
|
11
|
+
|
12
|
+
Usage examples:
|
13
|
+
poetry run mdbub FILE.md
|
14
|
+
poetry run mdbub --print-tags FILE.md
|
15
|
+
poetry run mdbub --version
|
16
|
+
poetry run mdbub --about
|
17
|
+
|
18
|
+
[bold yellow]Note:[/bold yellow] Options like --print-tags must come before the filename.
|
19
|
+
If you do not provide a filename, the last session will be restored (if available).
|
20
|
+
""",
|
21
|
+
add_completion=False,
|
22
|
+
invoke_without_command=True,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
@app.callback(invoke_without_command=True) # type: ignore
|
27
|
+
def main(
|
28
|
+
ctx: typer.Context,
|
29
|
+
file: str = typer.Argument(
|
30
|
+
None,
|
31
|
+
help="""
|
32
|
+
Markdown mindmap file to open or create (append #path to open to a node with [id:path]).
|
33
|
+
[bold yellow]Options like --print-tags must come before the filename.[/bold yellow]
|
34
|
+
""",
|
35
|
+
),
|
36
|
+
print_tags: bool = typer.Option(
|
37
|
+
False, "--print-tags", help="Print all #tags in the [FILE] as a table and exit."
|
38
|
+
),
|
39
|
+
print_kv: bool = typer.Option(
|
40
|
+
False,
|
41
|
+
"--print-kv",
|
42
|
+
help="Print all @key:value metadata in the [FILE] as a table and exit.",
|
43
|
+
),
|
44
|
+
print_links: bool = typer.Option(
|
45
|
+
False,
|
46
|
+
"--print-links",
|
47
|
+
help="Print all [id:...] anchors in the [FILE] as a table and exit.",
|
48
|
+
),
|
49
|
+
version_flag: bool = typer.Option(
|
50
|
+
False, "--version", help="Show version, build info, and config path."
|
51
|
+
),
|
52
|
+
about_flag: bool = typer.Option(False, "--about", help="Show about info."),
|
53
|
+
) -> None:
|
54
|
+
"""
|
55
|
+
Quick mode: mini interactive shell (default).
|
56
|
+
Use --version for version info, --config for config management.
|
57
|
+
"""
|
58
|
+
if version_flag:
|
59
|
+
version.main()
|
60
|
+
raise typer.Exit()
|
61
|
+
if about_flag:
|
62
|
+
about.main()
|
63
|
+
raise typer.Exit()
|
64
|
+
if print_links:
|
65
|
+
from mdbub.commands.print_links import main as print_links_main
|
66
|
+
|
67
|
+
print_links_main(file)
|
68
|
+
raise typer.Exit()
|
69
|
+
|
70
|
+
if print_tags:
|
71
|
+
from mdbub.commands.print_tags import main as print_tags_main
|
72
|
+
|
73
|
+
print_tags_main(file)
|
74
|
+
raise typer.Exit()
|
75
|
+
|
76
|
+
if print_kv:
|
77
|
+
from mdbub.commands.print_kv import main as print_kv_main
|
78
|
+
|
79
|
+
print_kv_main(file)
|
80
|
+
raise typer.Exit()
|
81
|
+
|
82
|
+
if ctx.invoked_subcommand is None:
|
83
|
+
try:
|
84
|
+
# Support deep links: filename.md#path/to/node
|
85
|
+
if file is not None and "#" in file:
|
86
|
+
filename, deep_link = file.split("#", 1)
|
87
|
+
deep_link_path = deep_link.strip("/") if deep_link.strip("/") else None
|
88
|
+
file = filename
|
89
|
+
else:
|
90
|
+
deep_link_path = None
|
91
|
+
if file is not None:
|
92
|
+
file_obj = open(file, "r+")
|
93
|
+
else:
|
94
|
+
file_obj = None
|
95
|
+
if print_tags:
|
96
|
+
from mdbub.commands.print_tags import main as print_tags_main
|
97
|
+
|
98
|
+
print_tags_main(file)
|
99
|
+
raise typer.Exit()
|
100
|
+
if print_kv:
|
101
|
+
from mdbub.commands.print_kv import main as print_kv_main
|
102
|
+
|
103
|
+
print_kv_main(file)
|
104
|
+
raise typer.Exit()
|
105
|
+
if print_links:
|
106
|
+
from mdbub.commands.links import main as links_main
|
107
|
+
|
108
|
+
links_main(file)
|
109
|
+
raise typer.Exit()
|
110
|
+
quick_main(
|
111
|
+
file_obj,
|
112
|
+
VERSION,
|
113
|
+
BUILD_INFO,
|
114
|
+
deep_link_path=deep_link_path,
|
115
|
+
)
|
116
|
+
except Exception as e:
|
117
|
+
typer.echo(f"Error: {e}")
|
118
|
+
raise typer.Exit(1)
|
119
|
+
|
120
|
+
|
121
|
+
if __name__ == "__main__":
|
122
|
+
app()
|
File without changes
|
mdbub/commands/about.py
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
from rich.console import Console
|
2
|
+
|
3
|
+
|
4
|
+
def main() -> None:
|
5
|
+
"""Print about info."""
|
6
|
+
console = Console()
|
7
|
+
console.print("mdbub: Interactive mindmap CLI tool", style="bold yellow")
|
8
|
+
console.print(
|
9
|
+
"It’s built for fast thinkers who love their keyboard. Whether you're capturing a flurry of ideas, organizing research, or mapping out your next big project, `mdbub` is your expressive outlet. It's not just functional, it should feel *fluid*.",
|
10
|
+
style="cyan",
|
11
|
+
)
|
12
|
+
console.print(
|
13
|
+
"Designed to be delightful to use entirely from the keyboard, this ecosystem lets you think at the speed of thought. And because it’s CLI-native, it’s composable in the best UNIX spirit—ready to interoperate with everything else you do in the shell.",
|
14
|
+
style="cyan",
|
15
|
+
)
|
16
|
+
console.print(
|
17
|
+
"This is for the chaos-to-clarity thinkers. We’re building it because no other tool feels this good under your fingers.",
|
18
|
+
style="cyan",
|
19
|
+
)
|
20
|
+
console.print("")
|
21
|
+
|
22
|
+
# Usage Section
|
23
|
+
console.print("[b]USAGE[/b]", style="bold yellow")
|
24
|
+
console.print(
|
25
|
+
"[b cyan]Basic:[/b cyan] [dim]mdbub <file.md>[/dim] — Open or create a mindmap from a Markdown file."
|
26
|
+
)
|
27
|
+
console.print(
|
28
|
+
"[b cyan]Deeplinks:[/b cyan] [dim]mdbub <file.md>#<nodeid>[/dim] — Jump directly to a specific node by its link ID."
|
29
|
+
)
|
30
|
+
console.print(
|
31
|
+
"[b cyan] --> Link IDs: Add \[id:something] for referencing or deep linking.",
|
32
|
+
style="dim",
|
33
|
+
)
|
34
|
+
console.print(
|
35
|
+
"[b cyan]Session Restore:[/b cyan] [dim]mdbub[/dim] — No file? Your last session is auto-restored (file & node)."
|
36
|
+
)
|
37
|
+
console.print(
|
38
|
+
"[b cyan]Tags:[/b cyan] Add #tags to node text for fast filtering and organizing."
|
39
|
+
)
|
40
|
+
console.print(
|
41
|
+
"[b cyan] --> See tag metrics with:[/b cyan] mdbub --print-tags <file.md>.",
|
42
|
+
style="dim",
|
43
|
+
)
|
44
|
+
console.print(
|
45
|
+
"[b cyan]KV Metadata:[/b cyan] Add @key:value to node text for structured metadata."
|
46
|
+
)
|
47
|
+
console.print(
|
48
|
+
"[b cyan] --> See all metadata with:[/b cyan] mdbub --print-kv <file.md>.",
|
49
|
+
style="dim",
|
50
|
+
)
|
51
|
+
console.print(
|
52
|
+
"[b cyan]Search:[/b cyan] Press [b]/[/b] to search instantly by text or tag. Navigate results with "
|
53
|
+
"arrows, Enter to jump."
|
54
|
+
)
|
55
|
+
console.print(
|
56
|
+
"[b cyan]Keyboard-First:[/b cyan] Navigate, edit, and manage your mindmap without ever leaving the "
|
57
|
+
"keyboard."
|
58
|
+
)
|
59
|
+
console.print("")
|
60
|
+
console.print(
|
61
|
+
"Note: Options like --print-tags and --print-kv must come before the filename."
|
62
|
+
)
|
63
|
+
console.print("")
|
64
|
+
console.print("[yellow]EXAMPLES[/yellow]")
|
65
|
+
console.print(
|
66
|
+
"mdbub --print-tags FILE.md | mdbub --print-kv FILE.md | mdbub FILE.md"
|
67
|
+
)
|
68
|
+
|
69
|
+
console.print("---", style="dim white")
|
70
|
+
console.print(
|
71
|
+
"This app was ideated with help from [ChatGPT-4o](https://chatgpt.com/),\n and then vibe coded with a "
|
72
|
+
"mix of [Claude 4](https://www.anthropic.com/claude) and [GPT-4.1](https://openai.com/blog/gpt-4-1)",
|
73
|
+
style="dim white",
|
74
|
+
)
|
75
|
+
console.print("---", style="dim white")
|
76
|
+
|
77
|
+
console.print("[bold blue]Version:[/bold blue] 0.3.0", style="dim white")
|
78
|
+
console.print("[bold blue]Author:[/bold blue] Collabinator Team", style="dim white")
|
79
|
+
console.print("[bold blue]License:[/bold blue] Apache 2.0", style="dim white")
|
80
|
+
console.print(
|
81
|
+
"[bold blue]Repository:[/bold blue] https://github.com/collabinator/mdbubbles",
|
82
|
+
style="dim white",
|
83
|
+
)
|
84
|
+
console.print(
|
85
|
+
"[bold blue]Documentation:[/bold blue] https://collabinator.github.io/mdbubbles/",
|
86
|
+
style="dim white",
|
87
|
+
)
|
88
|
+
console.print(
|
89
|
+
"[bold blue]PyPI:[/bold blue] https://pypi.org/project/mdbub/",
|
90
|
+
style="dim white",
|
91
|
+
)
|
92
|
+
console.print(
|
93
|
+
"[bold blue]Homebrew Tap:[/bold blue] https://github.com/collabinator/homebrew-tap",
|
94
|
+
style="dim white",
|
95
|
+
)
|
96
|
+
console.print(
|
97
|
+
"[bold blue]Changelog:[/bold blue] https://github.com/collabinator/mdbubbles/blob/main/CHANGELOG.md",
|
98
|
+
style="dim white",
|
99
|
+
)
|
mdbub/commands/export.py
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
import re
|
2
|
+
from collections import defaultdict
|
3
|
+
from typing import Dict, Tuple
|
4
|
+
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table
|
7
|
+
|
8
|
+
|
9
|
+
def extract_kv_metadata_from_file(
|
10
|
+
filename: str,
|
11
|
+
) -> Tuple[list[dict[str, object]], set[str], Dict[str, Dict[str, int]]]:
|
12
|
+
kv_pattern = re.compile(r"@(\w+):([^\s]+)")
|
13
|
+
node_kvs = [] # List of dicts: {line, label, kvs: {k:v}}
|
14
|
+
all_keys = set()
|
15
|
+
value_counts: Dict[str, Dict[str, int]] = defaultdict(
|
16
|
+
lambda: defaultdict(int)
|
17
|
+
) # key -> value -> count
|
18
|
+
with open(filename, "r", encoding="utf-8") as f:
|
19
|
+
for lineno, line in enumerate(f, 1):
|
20
|
+
kvs = {}
|
21
|
+
for match in kv_pattern.finditer(line):
|
22
|
+
key = match.group(1)
|
23
|
+
value = match.group(2)
|
24
|
+
kvs[key] = value
|
25
|
+
all_keys.add(key)
|
26
|
+
value_counts[key][value] += 1
|
27
|
+
if kvs:
|
28
|
+
label = line.strip().split("@", 1)[0].strip()
|
29
|
+
node_kvs.append({"line": lineno, "label": label, "kvs": kvs})
|
30
|
+
return node_kvs, set(all_keys), value_counts
|
31
|
+
|
32
|
+
|
33
|
+
def main(filename: str) -> None:
|
34
|
+
console = Console()
|
35
|
+
node_kvs, all_keys, value_counts = extract_kv_metadata_from_file(filename)
|
36
|
+
if not node_kvs:
|
37
|
+
console.print(
|
38
|
+
f"[yellow]No @key:value metadata found in:[/yellow] [italic]{filename}[/italic]"
|
39
|
+
)
|
40
|
+
return
|
41
|
+
table = Table(title=f"@key:value metadata in {filename}", show_lines=True)
|
42
|
+
table.add_column("Line", style="dim", justify="right")
|
43
|
+
table.add_column("Node Label", style="cyan")
|
44
|
+
for key in all_keys:
|
45
|
+
table.add_column(f"@{key}", style="magenta")
|
46
|
+
for node in node_kvs:
|
47
|
+
row = [str(node["line"]), node["label"]]
|
48
|
+
for key in all_keys:
|
49
|
+
row.append(node["kvs"].get(key, "") if hasattr(node["kvs"], "get") else "")
|
50
|
+
table.add_row(*row)
|
51
|
+
console.print(table)
|
52
|
+
# Print summary
|
53
|
+
console.print("\n[bold yellow]Summary of value counts per key:[/bold yellow]")
|
54
|
+
for key in all_keys:
|
55
|
+
vc = value_counts[key]
|
56
|
+
summary = ", ".join(
|
57
|
+
f"{v} ({c})" for v, c in sorted(vc.items(), key=lambda x: (-x[1], x[0]))
|
58
|
+
)
|
59
|
+
console.print(f" [cyan]@{key}[/cyan]: {summary}")
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
from rich.console import Console
|
4
|
+
from rich.table import Table
|
5
|
+
|
6
|
+
|
7
|
+
def main(filename: str) -> None:
|
8
|
+
console = Console()
|
9
|
+
anchor_pattern = re.compile(r"\[id:([^]]+)\]")
|
10
|
+
rows = []
|
11
|
+
with open(filename, "r", encoding="utf-8") as f:
|
12
|
+
for lineno, line in enumerate(f, 1):
|
13
|
+
m = anchor_pattern.search(line)
|
14
|
+
if m:
|
15
|
+
anchor = m.group(1)
|
16
|
+
label = line.strip().split("[id:", 1)[0].strip()
|
17
|
+
rows.append((lineno, label, anchor))
|
18
|
+
table = Table(title=f"[id:...] anchors in {filename}")
|
19
|
+
table.add_column("Line", justify="right")
|
20
|
+
table.add_column("Node Label")
|
21
|
+
table.add_column("Anchor")
|
22
|
+
for row in rows:
|
23
|
+
table.add_row(str(row[0]), row[1], row[2])
|
24
|
+
console.print(table)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import re
|
2
|
+
from collections import defaultdict
|
3
|
+
from typing import Dict, Tuple
|
4
|
+
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table
|
7
|
+
|
8
|
+
|
9
|
+
def extract_tags_from_file(
|
10
|
+
filename: str,
|
11
|
+
) -> Tuple[Dict[str, int], Dict[str, list[int]]]:
|
12
|
+
tag_pattern = re.compile(r"#(\w+)")
|
13
|
+
tag_counts: Dict[str, int] = defaultdict(int)
|
14
|
+
tag_lines = defaultdict(list)
|
15
|
+
with open(filename, "r", encoding="utf-8") as f:
|
16
|
+
for lineno, line in enumerate(f, 1):
|
17
|
+
for match in tag_pattern.finditer(line):
|
18
|
+
tag = match.group(1)
|
19
|
+
tag_counts[tag] += 1
|
20
|
+
tag_lines[tag].append(lineno)
|
21
|
+
return tag_counts, tag_lines
|
22
|
+
|
23
|
+
|
24
|
+
def main(filename: str) -> None:
|
25
|
+
console = Console()
|
26
|
+
tag_counts, tag_lines = extract_tags_from_file(filename)
|
27
|
+
if not tag_counts:
|
28
|
+
console.print(
|
29
|
+
f"[bold red]No tags found in:[/bold red] [italic]{filename}[/italic]"
|
30
|
+
)
|
31
|
+
return
|
32
|
+
table = Table(title=f"Tags in {filename}", show_lines=True)
|
33
|
+
table.add_column("Tag", style="cyan", no_wrap=True)
|
34
|
+
table.add_column("Count", style="magenta", justify="right")
|
35
|
+
table.add_column("Lines", style="dim")
|
36
|
+
for tag in sorted(tag_counts):
|
37
|
+
unique_lines = sorted(set(tag_lines[tag]))
|
38
|
+
lines = ", ".join(str(n) for n in unique_lines)
|
39
|
+
table.add_row(f"#{tag}", str(tag_counts[tag]), lines)
|
40
|
+
console.print(table)
|