h-m-m/h-m-m
2022-08-30 17:54:49 +02:00

2209 lines
47 KiB
PHP
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env php
<?php
// {{{ settings
$mm=[];
$mm['max_parent_width'] = 25;
$mm['max_leaf_width'] = 55;
$mm['line_spacing'] = 1;
$mm['active_node_color'] = "\033[38;5;0m\033[48;5;172m\033[1m";
$mm['message_color'] = "\033[38;5;0m\033[48;5;141m\033[1m";
$mm['center_lock'] = false;
$mm['focus_lock'] = false;
$mm['initial_depth'] = 1;
$mm['show_logo'] = 2;
function load_settings(&$mm){
global $argv;
$conf = $argv[0].'.conf';
if (!file_exists($conf)) return;
$handle = fopen($conf, "r");
if (!$handle) return;
while (($line = fgets($handle)) !== false) {
if (empty(trim($line))) continue;
$elements = explode('=',trim($line));
$setting = trim($elements[0] ?? '');
$value = trim($elements[1] ?? '');
switch ($setting){
case 'max_parent_width': $mm['max_parent_width'] = max( round($value), width_min ); break;
case 'max_leaf_width': $mm['max_leaf_width'] = max( round($value), width_min ); break;
case 'line_spacing': $mm['line_spacing'] = max( round($value), 0 ); break;
case 'initial_depth': $mm['initial_depth'] = max( round($value), 1 ); break;
case 'active_node_color': $mm['active_node_color'] = $value; break;
case 'message_color': $mm['message_color'] = $value; break;
case 'center_lock': $mm['center_lock'] = (bool)($value); break;
case 'focus_lock': $mm['focus_lock'] = (bool)($value); break;
}
}
fclose($handle);
}
// }}}
// {{{ constants and defaults
$mm['top_left_column'] = 0;
$mm['top_left_row'] = 0;
$mm['active_column'] = 0;
$mm['active_row'] = 0;
$mm['terminal_width'] = exec('tput cols');
$mm['terminal_height'] = exec('tput lines');
$mm['win_left'] = 0;
$mm['win_top'] = 0;
$mm['root'] = 2;
const logo_color = "\033[38;5;222m";
const default_color = "\033[0m";
const min_indentation = 2;
const width_tolerance = 1.3;
const width_min = 15;
const width_change_factor = 1.2;
const max_width_padding = 30;
const default_leaf_width = 55;
const default_parent_width = 25;
const default_spacing = 1;
const width_wider = 0;
const width_narrower = 1;
const width_default = 2;
const spacing_wider = 0;
const spacing_narrower = 1;
const spacing_default = 2;
const conn_left_len = 6;
const conn_right_len = 4;
const vertical_offset = 4;
const move_up = 0;
const move_down = 1;
const move_left = 2;
const move_right = 3;
const left_padding = 1;
const insert_sibling = 0;
const insert_child = 1;
$mm['conn_left'] = str_repeat('─', conn_left_len );
$mm['conn_right'] = str_repeat('─', conn_right_len - 2 );
$mm['conn_single'] = str_repeat('─', conn_left_len + conn_right_len - 1 );
// escape codes: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
const reset_page = "\033[2J\033[0;0f";
const reset_color = "\033[0m";
const invert_on = "\033[7m";
const invert_off = "\033[27m";
const dim_on = "\033[2m";
const dim_off = "\033[22m";
const line_on = "\033[0m\033[38;5;95m";
const line_off = "\033[0m";
const collapsed_symbol_on = "\033[1m\033[38;5;215m";
const collapsed_symbol_off = "\033[0m";
// }}}
// {{{ init
function shutdown(){
clear();
system("tput rmcup && tput cnorm && stty sane");
exit;
}
register_shutdown_function("shutdown");
declare(ticks = 1);
pcntl_signal(SIGINT,"shutdown");
// -------------------------------------------------------------
function set_up_screen(){
// https://www.computerhope.com/unix/ustty.htm
// https://www.ibm.com/docs/en/aix/7.1?topic=s-stty-command
//
// system('stty cbreak -echo '); // testing
system('stty cbreak -echo -crterase intr undef && tput smcup'); // production
// the first code disables the text cursor and the second one gets rid of the mouse!
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking
// 'l' disables and 'h' enables
echo "\033[?25l\033[?9;1000;1001;1002;1003;1004;1007;1005;1006;1015;1016l";
}
// -------------------------------------------------------------
function clear() { echo "\033[2J"; }
function move($x,$y) { echo "\033[{$y};{$x}f"; }
function put($x,$y,$t) { echo "\033[{$y};{$x}f{$t}"; }
// -------------------------------------------------------------
function mput(&$mm,$x,$y,$s){
$mm['map'][round($y)] =
mb_substr( $mm['map'][round($y)], 0, $x).
$s.
mb_substr( $mm['map'][round($y)], $x + mb_strlen($s) );
}
// }}}
// {{{ debug
function debug($nodes){
echo " id pr x w y yo h lh clh title\n";
$i = 0;
foreach ($nodes as $id=>$data)
echo
($i++ % 4 == 0 ? "\n" : '').
str_pad($id,3,' ',STR_PAD_LEFT).
str_pad($data['parent'],5,' ',STR_PAD_LEFT).
str_pad($data['x'] ?? 'x',5,' ',STR_PAD_LEFT).
str_pad($data['w'] ?? 'x',5,' ',STR_PAD_LEFT).
str_pad($data['y'] ?? 'x',5,' ',STR_PAD_LEFT).
str_pad($data['yo'] ?? 'x',5,' ',STR_PAD_LEFT).
str_pad($data['h'] ?? 'x',5,' ',STR_PAD_LEFT).
str_pad($data['lh'] ?? 'x',5,' ',STR_PAD_LEFT).
str_pad($data['clh'] ?? 'x',5,' ',STR_PAD_LEFT).
' '.
substr($data['title'],0,70).
"\n";
}
// }}}
// {{{ decode tree
function decode_tree($lines, $root_id, $start_id){
// calculating the indentation shift and cleaning up the special characters
$indentation_shift = 9999999;
foreach ($lines as $lid=>$line) {
$lines[$lid] =
str_replace(
[ "\t", "\n", "\r"],
[ " ", " ", " "],
$lines[$lid]
);
if (trim($lines[$lid])!='')
$indentation_shift =
min(
$indentation_shift,
mb_strlen($lines[$lid]) - mb_strlen(ltrim($lines[$lid]))
);
}
// building the tree
$nodes = [];
$id = $start_id;
$previous_level = 1;
$level = 1;
$previous_indentation = 0;
$level_parent[1] = $root_id;
$level_indentation[1] = 0;
foreach ($lines as $line)
if (trim($line)!='') {
// warming up for this round!
$indentation = mb_strlen($line) - mb_strlen(ltrim($line)) - $indentation_shift;
// going one level down
if ($indentation > $previous_indentation ){
$level = $previous_level + 1;
$level_indentation[$level] = $indentation;
}
// going one or more levels up
if ($indentation < $previous_indentation )
foreach ($level_indentation as $plevel=>$pindentation)
if ($pindentation == $indentation)
$level = $plevel;
// saving the latest level_parent
if ($level > $previous_level)
$level_parent[$level] = $id-1;
// done! saving the data
$nodes[$id] = [
'title' => trim($line),
'parent' => $level_parent[$level]
];
// getting ready for the next round!
$previous_indentation = $indentation;
$previous_level = $level;
$id++;
}
// setting a few properties that simplify future calculations
foreach ($nodes as $id=>$node) {
$nodes[$id]['collapsed'] = false;
$nodes[$id]['is_leaf'] = true;
$nodes[$id]['children'] = [];
}
foreach ($nodes as $id=>$node)
if (isset($nodes[ $node['parent'] ])) {
$nodes[ $node['parent'] ]['is_leaf'] = false;
$nodes[ $node['parent'] ]['children'][] = $id;
}
return($nodes);
}
// }}}
// {{{ load_file
function load_file(&$mm){
global $argv;
if (!isset($argv[1])){
load_empty_map($mm);
return;
}
$mm['filename']=$argv[1];
if (!file_exists($argv[1])) {
load_empty_map($mm);
return;
}
$lines = file($argv[1], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// starting from 2 instead of 1, in case the files doesn't have
// a single root and we have to inject one. leaving "1" empty
// won't cause any problems.
$new_nodes = decode_tree($lines, 0, 2);
// checking to see how many first-level nodes we have
$first_level_nodes = [];
foreach ($new_nodes as $id=>$node)
if ($id!=0 && $node['parent']==0)
$first_level_nodes[] = $id;
$mm['nodes'][0]['parent'] = -1;
$mm['nodes'][0]['title'] = 'X';
$mm['nodes'][0]['is_leaf'] = false;
if (count($first_level_nodes)>1) {
$mm['root'] = 1;
$mm['nodes'][1]['parent'] = 0;
$mm['nodes'][1]['title'] = 'root';
$mm['nodes'][1]['is_leaf'] = false;
$mm['nodes'][1]['children'] = $first_level_nodes;
$mm['nodes'][0]['children'] = [1];
foreach ($new_nodes as $id=>$node)
if ($node['parent']==0)
$new_nodes[$id]['parent'] = 1;
} else {
$mm['nodes'][0]['children'] = $first_level_nodes;
}
$mm['nodes'] = $mm['nodes'] + $new_nodes;
if (isset($mm['nodes'][1]))
$mm['active_node']=1;
else
$mm['active_node']=2;
}
// }}}
// {{{ calculate w, x, lh, and clh
function calculate_x_and_lh(&$mm, $id){
$node = $mm['nodes'][$id];
$mm['nodes'][$id]['x'] =
$mm['nodes'][ $node['parent'] ]['x'] +
$mm['nodes'][ $node['parent'] ]['w'] +
conn_left_len +
conn_right_len +
1 +
( $node['parent']==0 ? 1 - conn_right_len - conn_left_len : 0 )
;
$max_width =
($node['is_leaf'] || ($node['collapsed'] ?? false)) * $mm['max_leaf_width'] +
!($node['is_leaf'] || ($node['collapsed'] ?? false)) * $mm['max_parent_width'];
if ( mb_strlen($node['title']) > width_tolerance * $max_width ) {
$lines = explode(
"\n",
wordwrap(
$node['title'],
$max_width
)
);
$mm['nodes'][$id]['w'] = 0;
foreach ($lines as $line)
$mm['nodes'][$id]['w'] =
max($mm['nodes'][$id]['w'], trim(mb_strlen($line)));
$mm['nodes'][$id]['lh'] = count($lines);
} else {
$mm['nodes'][$id]['w'] = mb_strlen($node['title']);
$mm['nodes'][$id]['lh'] = 1;
}
$mm['map_width'] =
max(
$mm['map_width'],
$mm['nodes'][$id]['x'] +
$mm['nodes'][$id]['w']
);
$mm['nodes'][$id]['clh'] = 0;
if (($mm['nodes'][$id]['collapsed'] ?? false) || $mm['nodes'][$id]['is_leaf'])
$mm['nodes'][$id]['clh'] = $mm['nodes'][$id]['lh'];
foreach ($node['children'] as $cid) {
calculate_x_and_lh($mm, $cid);
$mm['nodes'][$id]['clh'] += $mm['nodes'][$cid]['clh'];
}
}
// }}}
// {{{ calculate h
function calculate_h(&$mm){
$unfinished = true;
while ($unfinished) {
$unfinished = false;
foreach ($mm['nodes'] as $id=>$node)
if ($node['is_leaf'] || ($node['collapsed'] ?? false))
$mm['nodes'][$id]['h'] =
$mm['line_spacing'] +
$mm['nodes'][$id]['lh'];
else {
$h = 0;
$unready = false;
foreach ($node['children'] as $cid)
if ($mm['nodes'][$cid]['h']>=0)
$h += $mm['nodes'][$cid]['h'];
else {
$unready = true;
break;
}
if ($unready)
$unfinished = true;
else
$mm['nodes'][$id]['h'] =
max( $h, $node['lh'] + $mm['line_spacing'] );
}
}
}
// }}}
// {{{ calculate y and yo
function calculate_y(&$mm){
$mm['map_top'] = 0;
$mm['map_bottom'] = 0;
$mm['map_height'] = $mm['nodes'][0]['h'];
$mm['nodes'][0]['y'] = 0;
calculate_children_y($mm, 0);
}
function calculate_children_y(&$mm,$pid){
$y = $mm['nodes'][$pid]['y'];
$mm['nodes'][$pid]['yo'] =
round(
(
$mm['nodes'][$pid]['h'] -
$mm['nodes'][$pid]['lh']
)/2
);
if (!($mm['nodes'][$pid]['collapsed'] ?? false))
foreach ($mm['nodes'][$pid]['children'] as $cid) {
$mm['nodes'][$cid]['y'] = $y;
$mm['map_bottom'] = max(
$mm['map_bottom'],
$mm['nodes'][$cid]['lh']+
$mm['line_spacing']+
$y
);
$mm['map_top'] = min($mm['map_top'],$y);
$y += $mm['nodes'][$cid]['h'];
calculate_children_y($mm,$cid);
}
}
// }}}
// {{{ calculate top-down height shift
function calculate_height_shift(&$mm, $id, $shift = 0){
$mm['nodes'][$id]['yo'] += $shift;
$shift += max(0, round( ($mm['nodes'][$id]['lh'] - $mm['nodes'][$id]['clh'])/2 - 0.5 ));
foreach ($mm['nodes'][$id]['children'] as $cid)
if (!$mm['nodes'][$id]['collapsed'])
calculate_height_shift($mm, $cid, $shift);
}
// }}}
// {{{ draw connections on the map
function draw_connections(&$mm, $id){
$node = $mm['nodes'][$id];
// if there's no child
if ($node['is_leaf']) return;
// if the node is collapsed
if ($node['collapsed'] ?? false) {
mput(
$mm,
$node['x'] + $node['w']+1,
$node['y'] + $node['yo'],
' [+]'
);
return;
}
// if there's only one child
if (count($node['children'])==1) {
$child = $mm['nodes'][ $node['children'][0] ];
$y1 = round($node['y']+$node['yo']) + round($node['lh']/2-0.6);
$y2 = round($child['y']+$child['yo']) + round($child['lh']/2-0.6);
mput(
$mm,
$mm['nodes'][ $node['children'][0] ]['x'] - conn_left_len - conn_right_len,
min($y1,$y2),
$mm['conn_single']
);
draw_connections($mm, $node['children'][0]);
return;
}
// if there's more than one child
$bottom = 0;
$bottom_child = 0;
$top = $mm['map_height'];
$top_child = 0;
foreach ($node['children'] as $cid) {
if ($mm['nodes'][$cid]['y'] + $mm['nodes'][$cid]['yo'] > $bottom) {
$bottom = $mm['nodes'][$cid]['y'] + $mm['nodes'][$cid]['yo'];
$bottom_child = $cid;
}
if ($mm['nodes'][$cid]['y'] + $mm['nodes'][$cid]['yo'] < $top) {
$top = $mm['nodes'][$cid]['y'] + $mm['nodes'][$cid]['yo'];
$top_child = $cid;
}
}
$middle = round($node['y']+$node['yo']) + round($node['lh']/2-0.6);
mput(
$mm,
$mm['nodes'][$top_child]['x'] - conn_left_len - conn_right_len,
$middle,
$mm['conn_left']
);
for ( $i = $top ; $i < $bottom ; $i++ )
mput(
$mm,
$mm['nodes'][$top_child]['x'] - conn_right_len,
$i,
'│');
mput(
$mm,
$mm['nodes'][$top_child]['x'] - conn_right_len,
$top,
'╭'.
$mm['conn_right']);
mput(
$mm,
$mm['nodes'][$top_child]['x']-conn_right_len,
$bottom,
'╰'.
$mm['conn_right']);
if (count($node['children'])>2)
foreach ($node['children'] as $cid)
if ($cid!=$top_child && $cid!=$bottom_child)
mput(
$mm,
$mm['nodes'][$cid]['x']-conn_right_len,
$mm['nodes'][$cid]['y']+
$mm['nodes'][$cid]['lh']/2-0.2+
$mm['nodes'][$cid]['yo'],
'├'.
$mm['conn_right']);
$existing_char =
mb_substr(
$mm['map'][$middle],
$mm['nodes'][$top_child]['x'] - conn_right_len,
1);
if ($existing_char=='│')
mput(
$mm,
$mm['nodes'][$top_child]['x'] - conn_right_len,
$middle,
'┤');
if ($existing_char=='╭')
mput(
$mm,
$mm['nodes'][$top_child]['x'] - conn_right_len,
$middle,
'┬');
if ($existing_char=='├')
mput(
$mm,
$mm['nodes'][$top_child]['x'] - conn_right_len,
$middle,
'┼');
foreach ($node['children'] as $cid)
draw_connections($mm, $cid);
}
// }}}
// {{{ add content to the map
function add_content_to_the_map(&$mm, $id){
$node = $mm['nodes'][$id];
$max_width =
($node['is_leaf'] || ($node['collapsed'] ?? false)) * $mm['max_leaf_width'] +
!($node['is_leaf'] || ($node['collapsed'] ?? false)) * $mm['max_parent_width'];
if ( mb_strlen($node['title']) > width_tolerance * $max_width)
$lines = explode("\n",wordwrap(
$node['title'],
$max_width
));
else
$lines = [$node['title']];
$num_lines = count($lines);
for ( $i=0 ; $i<$num_lines ; $i++ )
mput(
$mm,
$node['x'],
round($node['y']+$node['yo'])+$i,
$lines[$i].' '
);
if (!($node['collapsed'] ?? false))
foreach ($node['children'] as $cid)
add_content_to_the_map($mm,$cid);
}
// }}}
// {{{ build map
function build_map(&$mm){
// resetting the global values
$mm['map_width'] = 0;
$mm['map_height'] = 0;
$mm['map_top'] = 0;
$mm['map_bottom'] = 0;
// resetting the coordinates
foreach ($mm['nodes'] as $id=>$node) {
$mm['nodes'][$id]['x'] = -1;
$mm['nodes'][$id]['y'] = -1;
$mm['nodes'][$id]['h'] = -1;
$mm['nodes'][$id]['lh'] = -1;
}
$mm['nodes'][0]['x'] = 0;
$mm['nodes'][0]['w'] = left_padding;
$mm['nodes'][0]['lh'] = 1;
// resetting the map, 1/2
$mm['map']=[];
$mm['map_width']=0;
$mm['map_height']=0;
$mm['map_top']=0;
$mm['map_bottom']=0;
// calculating the new coordinates
calculate_x_and_lh($mm,$mm['root']);
calculate_h($mm);
calculate_y($mm);
calculate_height_shift($mm, $mm['root']);
// in case it was resized!
$mm['terminal_width'] = exec('tput cols');
$mm['terminal_height'] = exec('tput lines');
// resetting the map, 2/2
$height = max($mm['map_bottom'],$mm['terminal_height']);
$blank = str_repeat(' ', $mm['map_width']+$mm['terminal_width']);
for ($i=$mm['map_top'] ; $i<=$height ; $i++)
$mm['map'][$i] = $blank;
// building the new map
draw_connections($mm, $mm['root']);
add_content_to_the_map($mm, $mm['root']);
}
// }}}
// {{{ toggle
function toggle(&$mm){
if ($mm['nodes'][ $mm['active_node'] ]['is_leaf']) return;
$mm['nodes'][ $mm['active_node'] ]['collapsed'] =
!($mm['nodes'][ $mm['active_node'] ]['collapsed'] ?? false);
build_map($mm);
display($mm);
}
// }}}
// {{{ spacing adjuster
function adjust_spacing(&$mm, $task){
if ($task==spacing_wider) $mm['line_spacing']++;
if ($task==spacing_narrower) $mm['line_spacing'] = max(0, $mm['line_spacing']-1);
if ($task==spacing_default) $mm['line_spacing'] = default_spacing;
build_map($mm);
center_active_node($mm);
display($mm);
message($mm,'Spacing: '.$mm['line_spacing']);
}
// }}}
// {{{ width adjuster
function adjust_width(&$mm, $task){
if ($task==width_wider) {
$mx = $mm['terminal_width'] - max_width_padding;
$mm['max_parent_width'] = round(min($mx, max( width_min, $mm['max_parent_width'] * width_change_factor )));
$mm['max_leaf_width'] = round(min($mx, max( width_min, $mm['max_leaf_width'] * width_change_factor )));
}
if ($task==width_narrower) {
$mm['max_parent_width'] = round(max( width_min, $mm['max_parent_width'] / width_change_factor ));
$mm['max_leaf_width'] = round(max( width_min, $mm['max_leaf_width'] / width_change_factor ));
}
if ($task==width_default) {
$mm['max_parent_width'] = default_parent_width;
$mm['max_leaf_width'] = default_leaf_width;
}
build_map($mm);
center_active_node($mm);
display($mm);
message($mm,'Width: '.$mm['max_parent_width'].' / '.$mm['max_leaf_width']);
}
// }}}
// {{{ move nodes
function push_node_down(&$mm, $id){
if ($id==0) return;
$mm['modified'] = true;
if (isset($mm['nodes'][$id+1]))
push_node_down($mm,$id+1);
$mm['nodes'][$id+1] = $mm['nodes'][$id];
unset($mm['nodes'][$id]);
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'] =
array_diff(
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'],
[$id]
);
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'] =
array_push(
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'],
[$id+1]
);
foreach($mm['nodes'][$id+1]['children'] as $cid=>$cdata)
$mm['nodes'][$cid]['parent'] = $id+1;
}
// }}}
// {{{ insert node
function insert_node(&$mm, $type){
if ($mm['active_node']==$mm['root'])
$type=insert_child;
$mm['modified'] = true;
if ($type==insert_sibling)
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
else
$parent_id = $mm['active_node'];
$mm['nodes'][$parent_id]['is_leaf'] = false;
$mm['nodes'][$parent_id]['collapsed'] = false;
$new_id = max(array_keys($mm['nodes'])) + 1;
$mm['nodes'][$new_id] = [
'title' => 'NEW',
'is_leaf' => true,
'collapsed' => false,
'children' => [],
'parent' => $parent_id
];
if ($type==insert_sibling) {
$children = [];
foreach ($mm['nodes'][$parent_id]['children'] as $child) {
$children[] = $child;
if ($child==$mm['active_node'])
$children[] = $new_id;
}
$mm['nodes'][$parent_id]['children'] = $children;
} else {
$mm['nodes'][$parent_id]['children'][] = $new_id;
}
$mm['active_node'] = $new_id;
build_map($mm);
display($mm);
$mm['nodes'][ $mm['active_node'] ]['title']='';
edit_node($mm);
}
// }}}
// {{{ edit node
function show_line(&$mm, $title, $cursor, $shift){
$output =
str_pad(
substr($title,$shift,$mm['terminal_width']-1),
$mm['terminal_width']
);
$output = substr_replace( $output, invert_off, $cursor-$shift , 0);
$output = substr_replace( $output, invert_on, $cursor-$shift-1, 0);
put(0,$mm['terminal_height'],$mm['active_node_color'].$output);
}
function edit_node(&$mm, $rewrite = false){
$title = $rewrite ? '' : $mm['nodes'][ $mm['active_node'] ]['title'];
if ($mm['active_node']==0 && $title=='root') $title='';
$in = '';
$cursor = strlen($title)+1;
$shift = max( 0, $cursor - $mm['terminal_width'] );
show_line($mm, $title, $cursor, $shift);
while(true){
usleep(10000);
$in = fread(STDIN, 9);
if ($in != '') {
// Esc.
if ($in=="\033") {
display($mm);
message($mm, 'Editing cancelled');
return;
}
// up arrow and home
elseif ($in=="\033\133\101" || $in=="\033\133\110") $cursor = 1;
// right arrow
elseif ($in=="\033\133\103") $cursor = min( strlen($title)+1, $cursor+1);
// down arrow and end
elseif ($in=="\033\133\102" || $in=="\033\133\106") $cursor = strlen($title)+1;
// left arrow
elseif ($in=="\033\133\104") $cursor = max(1, $cursor-1);
// ctrl+left and shift+left
elseif ($in=="\033\133\061\073\065\104" || $in=="\033\133\061\073\062\104")
$cursor =
$cursor < 3
? 1
: max(
1,
(
strrpos($title,' ',$cursor-strlen($title)-3) !== false
? strrpos($title,' ',$cursor-strlen($title)-3) + 2
: 1
)
);
// ctrl+right and shift+right
elseif ($in=="\033\133\061\073\065\103" || $in=="\033\133\061\073\062\103")
$cursor =
$cursor > strlen($title) -2
? strlen($title) + 1
: min(
strlen($title)+1,
(
strpos($title,' ',$cursor+1) !== false
? strpos($title,' ',$cursor+1) + 2
: strlen($title) + 1
)
);
// ctrl+backspace
elseif ($in=="\010") {
$from = mb_strrpos($title, ' ', min(1,$cursor-mb_strlen($title)-3));
if ($from === false) $from=1;
$title = mb_substr($title, 0, $from) . mb_substr($title,$cursor-1);
$cursor = $from+1;
}
// backspace
elseif ($in=="\177") {
if ($cursor>1) {
$title = substr_replace($title, '', $cursor-2, 1);
$cursor--;
}
}
// ctrl+delete
elseif ($in=="\033\133\63\073\065\176"){
$title = '';
$cursor = 1;
}
// delete
elseif ($in=="\033\133\63\176") {
$title = substr_replace($title, '', $cursor-1, 1);
}
// enter
elseif ($in=="\012") {
$title = trim($title);
$mm['nodes'][ $mm['active_node'] ]['title'] = $title;
$original['nodes'][ $mm['active_node'] ]['title'] = $title;
$mm['modified'] = true;
build_map($mm);
display($mm);
return;
}
// normal characters
elseif (strlen($in)==1) {
if ($in=="\011") $in=' ';
$title = substr_replace($title, $in, $cursor-1, 0);
$cursor++;
}
// adjusting the position and shift
$shift = max( 0, $shift, $cursor - $mm['terminal_width'] );
$shift = min( $shift, $cursor-1 );
show_line($mm, $title, $cursor, $shift);
}
}
}
// }}}
// {{{ center active node
function center_active_node(&$mm, $only_vertically = false){
$node = $mm['nodes'][ $mm['active_node'] ];
$midx = $node['w']/2 + $node['x'];
$midy = $node['lh']/2 + $node['y'] + $node['yo'];
if (!$only_vertically)
$mm['win_left'] = max(0, round( $midx - $mm['terminal_width']/2 ) );
$mm['win_top'] = round( $midy - $mm['terminal_height']/2 );
}
// }}}
// {{{ goto's
function go_to_root(&$mm){
$mm['active_node']=$mm['root'];
display($mm, true);
}
function go_to_top(&$mm){
$yid = 0;
$y = $mm['map_height'];
foreach ($mm['nodes'] as $id=>$node)
if ($node['y']>=0 && $node['y']+$node['yo'] < $y) {
$y = $node['y']+$node['yo'];
$yid = $id;
}
$mm['active_node'] = $yid;
display($mm, true);
}
function go_to_bottom(&$mm){
$yid = 0;
$y = 0;
foreach ($mm['nodes'] as $id=>$node)
if ($node['y']>=0 && $node['y']+$node['yo'] > $y) {
$y = $node['y']+$node['yo']+$node['lh'];
$yid = $id;
}
$mm['active_node'] = $yid;
display($mm, true);
}
// }}}
// {{{ search
function search(&$mm){
put(0,$mm['terminal_height'],$mm['active_node_color'].str_repeat(' ',$mm['terminal_width']));
move(0,$mm['terminal_height']);
system("stty sane");
$mm['query'] = readline('Search for: ');
system('stty cbreak -echo');
if (empty($mm['query'])) {
display($mm);
return;
}
next_search_result($mm);
}
function previous_search_result(&$mm){
$cy = $mm['nodes'][ $mm['active_node'] ]['y'] +
$mm['nodes'][ $mm['active_node'] ]['yo'];
$ny = -1;
$nid = -1;
foreach ($mm['nodes'] as $id=>$node)
if (
$id != 0 &&
$node['y'] > -1 &&
$node['y']+$node['yo'] < $cy &&
$node['y']+$node['yo'] > $ny &&
mb_stripos($node['title'],$mm['query'])!==false
)
{
$ny = $node['y'] + $node['yo'];
$nid = $id;
}
if ($nid<0) {
display($mm);
return;
}
$mm['active_node'] = $nid;
display($mm);
}
function next_search_result(&$mm){
$cy = $mm['nodes'][ $mm['active_node'] ]['y'] +
$mm['nodes'][ $mm['active_node'] ]['yo'];
$ny = $mm['map_height']+1;
$nid = -1;
foreach ($mm['nodes'] as $id=>$node)
if (
$id != 0 &&
$node['y'] > -1 &&
$node['y']+$node['yo'] > $cy &&
$node['y']+$node['yo'] < $ny &&
mb_stripos($node['title'],$mm['query'])!==false
)
{
$ny = $node['y'] + $node['yo'];
$nid = $id;
}
if ($nid<0) {
display($mm);
return;
}
$mm['active_node'] = $nid;
display($mm);
}
// }}}
// {{{ move active node
function move_active_node_down(&$mm){
if ($mm['active_node']==0) return;
$mm['modified'] = true;
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
$children = [];
$now = false;
foreach ($mm['nodes'][ $parent_id ]['children'] as $child) {
if ($child!=$mm['active_node'])
$children[] = $child;
if ($now) {
$children[] = $mm['active_node'];
$now = false;
}
if ($child==$mm['active_node'])
$now = true;
}
if ($now)
$children[] = $mm['active_node'];
$mm['nodes'][ $parent_id ]['children'] = $children;
build_map($mm);
display($mm);
}
function move_active_node_up(&$mm){
if ($mm['active_node']==0) return;
$mm['modified'] = true;
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
$children = [];
$now = false;
$rev_children = array_reverse($mm['nodes'][$parent_id]['children']);
foreach ($rev_children as $child) {
if ($child!=$mm['active_node'])
$children[] = $child;
if ($now) {
$children[] = $mm['active_node'];
$now = false;
}
if ($child==$mm['active_node'])
$now = true;
}
if ($now)
$children[] = $mm['active_node'];
$mm['nodes'][ $parent_id ]['children'] = array_reverse($children);
build_map($mm);
display($mm);
}
// }}}
// {{{ save
function save(&$mm, $new_name = false){
if (empty($mm['filename']))
$new_name = true;
if ($new_name) {
$path = exec('pwd');
put(0,$mm['terminal_height'],$mm['active_node_color'].str_repeat(' ',$mm['terminal_width']));
put(0,$mm['terminal_height']," $path -- new path and file name: ");
system("stty sane");
$mm['filename'] = trim(readline());
system('stty cbreak -echo');
if ($mm['filename']=='') {
display($mm);
message($mm, 'Saving cancelled');
return;
}
$ext = mb_substr( $mm['filename'], mb_strrpos($mm['filename'],'.') + 1);
if ($ext!='hmm')
$mm['filename'] .= '.hmm';
display($mm);
}
if (file_exists($mm['filename']) && !is_writable($mm['filename'])) {
message($mm, "ERROR! I don't have access to write into \"$mm[filename]\". Use shift+s and set another path and filename.");
sleep(1);
return;
}
$file = fopen($mm['filename'], "w");
$mm['modified'] = false;
if ($file===false) {
message($mm, 'ERROR! Could not save the file');
$mm['modified'] = true;
return;
}
fwrite($file, encode_tree($mm, $mm['root']));
fclose($file);
display($mm);
message($mm, 'Saved '.$mm['filename']);
}
// }}}
// {{{ message
function message(&$mm, $text){
put(
$mm['terminal_width'] - strlen($text) - 1,
$mm['terminal_height'],
$mm['message_color'].' '.$text.' '.reset_color
);
usleep(200000);
}
// }}}
// {{{ quit
function quit(&$mm){
if (($mm['modified'] ?? false) === false) exit;
message($mm, "You have unsaved changes. Save them, or use shift+Q to quit without saving.");
}
// }}}
// {{{ move_window
function move_window(&$mm){
$node = $mm['nodes'][ $mm['active_node'] ];
$x1 = max(0, $node['x'] - conn_right_len - 2);
$x2 = $node['x'] + $node['w'] + 2;
$y1 = max(0, $node['y'] + $node['yo'] - vertical_offset);
$y2 = $y1 + $node['lh'] + vertical_offset*2;
$mm['win_left'] = min( $mm['win_left'], $x1);
$mm['win_left'] = max( $mm['win_left'], $x2 - $mm['terminal_width']);
$mm['win_top'] = min( $mm['win_top'], $y1);
$mm['win_top'] = max( $mm['win_top'], $y2 - $mm['terminal_height']);
}
// }}}
// {{{ change active node
function change_active_node(&$mm, $direction) {
$node = $mm['nodes'][ $mm['active_node'] ];
// we go to the child that is closest to the parent node;
// i.e., closest to the middle.
if ($direction==move_right){
if ($node['is_leaf'] || ($node['collapsed'] ?? false) ) return;
$distance = [];
foreach ($node['children'] as $cid)
$distance[$cid] =
abs(
+ $node['y']
+ $node['yo']
+ $node['lh']/2
- $mm['nodes'][$cid]['y']
- $mm['nodes'][$cid]['yo']
- $mm['nodes'][$cid]['lh']/2
);
asort($distance);
$mm['active_node'] = array_keys($distance)[0];
display($mm);
return;
}
// no other movement applies to the root element
if ( $mm['active_node']==0 ) return;
// it can't be easier than moving to the left!
if ($direction==move_left) {
if ($mm['active_node']==$mm['root']) return;
$mm['active_node'] = $node['parent'];
display($mm);
return;
}
// for up and down, we'll try to move between siblings first,
// considering that their order is set based on their list
// in the parent item.
if ($direction==move_up){
$rchildren = array_reverse($mm['nodes'][ $node['parent'] ]['children']);
foreach ($rchildren as $cid)
if ($mm['nodes'][$cid]['y']+$mm['nodes'][$cid]['yo'] < $node['y']+$node['yo']) {
$mm['active_node'] = $cid;
display($mm);
return;
}
}
if ($direction==move_down){
foreach ($mm['nodes'][ $node['parent'] ]['children'] as $cid)
if ($mm['nodes'][$cid]['y']+$mm['nodes'][$cid]['yo'] > $node['y']+$node['yo']) {
$mm['active_node'] = $cid;
display($mm);
return;
}
}
// if it's not possible to move up or down between siblings,
// we'll measure distance and move to the nearest node.
// because the goal is to move vertically, and also because
// the aspect ratio of characters is not 1, there's a factor
// for y to give it more importance. I've set the amount by
// trial and erros and doesn't follow an exact logic. It may
// need refinements in the future.
$distance = [];
foreach ($mm['nodes'] as $id=>$nd)
if ($id != $mm['active_node'] && $nd['y']!=-1) {
$dy = $nd['y'] + $nd['yo'] + $nd['lh']/2 - $node['y'] - $node['yo'] - $node['lh']/2;
if ( ($direction==move_down && $dy>0) || ($direction==move_up && $dy<0) ) {
$dx = $nd['x'] + $nd['w']/2 - $node['x'] - $node['w']/2;
$distance[$id] = pow($dy*15,2) + pow($dx,2);
}
}
if ($distance==[]) return;
asort($distance);
$mm['active_node'] = array_keys($distance)[0];
display($mm);
return;
}
// }}}
// {{{ expand
function expand(&$mm, $id){
$mm['nodes'][$id]['collapsed'] = false;
foreach ($mm['nodes'][$id]['children'] as $cid)
expand($mm, $cid);
}
function expand_all(&$mm){
foreach ($mm['nodes'] as $id=>$node)
$mm['nodes'][$id]['collapsed'] = false;
$mm['show_logo'] = 0;
build_map($mm);
center_active_node($mm);
display($mm);
}
// }}}
// {{{ encode tree
function encode_tree(&$mm, $id, $exclude_parent = false, $base = 0){
if (!$exclude_parent)
$output = str_repeat("\t",$base).$mm['nodes'][$id]['title']."\n";
else
$output = '';
foreach ($mm['nodes'][$id]['children'] as $cid)
$output .= encode_tree($mm, $cid, false, $base+1-$exclude_parent);
return $output;
}
// }}}
// {{{ paste sub-tree
function paste_sub_tree(&$mm, $as_sibling ){
if ($as_sibling && $mm['active_node']==$mm['root']) return;
$mm['modified'] = true;
if ($as_sibling)
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
else
$parent_id = $mm['active_node'];
$mm['nodes'][$parent_id]['collapsed'] = false;
$mm['nodes'][ $parent_id ]['is_leaf'] = false;
$new_id = 1 + max(array_keys($mm['nodes']));
$st =
decode_tree(
explode("\n",get_from_clipboard()),
$parent_id,
$new_id
);
$mm['nodes'] += $st;
// doing it like this, in case the sub-tree has more than
// one top-level element.
foreach ($st as $cid=>$cdata)
if ($cdata['parent'] == $parent_id)
$mm['nodes'][ $parent_id ]['children'][] = $cid;
// rearranging the items only if they are pasted as siblings
// (for pasting as children, it makes sense to have them
// at the end)
if ($as_sibling) {
$sub_roots = [];
foreach ($st as $cid=>$cdata)
if ($cdata['parent']==$parent_id)
$sub_roots[] = $cid;
$children = [];
foreach ($mm['nodes'][ $parent_id ]['children'] as $child_id) {
if (!in_array($child_id, $sub_roots))
$children[] = $child_id;
if ($child_id == $mm['active_node'])
$children = array_merge($children, $sub_roots);
}
$mm['nodes'][ $parent_id ]['children'] = $children;
}
$mm['active_node'] = $new_id;
build_map($mm);
display($mm);
}
// }}}
// {{{ clipboard
function copy_to_clipboard($text){
if (PHP_OS_FAMILY === "Linux") $clip = popen('xclip -selection clipboard','wb');
elseif (PHP_OS_FAMILY === "Windows") $clip = popen("clip","wb");
elseif (PHP_OS_FAMILY === "Darwin") $clip = popen('pbcopy','wb');
if (!isset($clip)) return;
fwrite($clip,$text);
pclose($clip);
}
function get_from_clipboard(){
if (PHP_OS_FAMILY === "Linux")
return shell_exec('xclip -out -selection clipboard');
elseif (PHP_OS_FAMILY === "Windows")
return shell_exec('powershell -sta "add-type -as System.Windows.Forms; [windows.forms.clipboard]::GetText()"');
elseif (PHP_OS_FAMILY === "Darwin")
return shell_exec('pbpaste');
}
// }}}
// {{{ load empty map
function load_empty_map(&$mm){
if (isset($mm['nodes']))
unset($mm['nodes']);
$mm['nodes'][0] = [ 'title'=>'X', 'is_leaf'=>false, 'children'=>[1], 'collapsed'=>false, 'parent'=>-1 ];
$mm['nodes'][1] = [ 'title'=>'root', 'is_leaf'=>true, 'children'=>[], 'collapsed'=>false, 'parent'=>0 ];
$mm['active_node']=1;
$mm['root']=1;
}
// }}}
// {{{ yank
function yank_node(&$mm, $exclude_parent = false ){
copy_to_clipboard(encode_tree($mm, $mm['active_node'], $exclude_parent));
message($mm, 'Item(s) are copied to the clipboard.');
}
// }}}
// {{{ delete
function delete_node(&$mm, $exclude_parent = false ){
if ($mm['active_node']==$mm['root']) $exclude_parent = true;
copy_to_clipboard( encode_tree($mm, $mm['active_node'], $exclude_parent) );
$mm['modified'] = true;
delete_node_internal($mm, $mm['active_node'], $exclude_parent);
build_map($mm);
display($mm);
message($mm, 'Item(s) are cut and placed into the clipboard.');
}
function delete_node_internal(&$mm, $active_node, $exclude_parent = false ){
// taking a shorter approach if it's for the whole tree
if ($active_node==$mm['root'] && !$exclude_parent) {
load_empty_map($mm);
display($mm, true);
return;
}
// if it's for a sub-tree, then...
delete_children($mm, $active_node);
if ($exclude_parent) {
$mm['nodes'][ $active_node ]['is_leaf'] = true;
$mm['nodes'][ $active_node ]['children'] = [];
} else {
$parent_id = $mm['nodes'][ $active_node ]['parent'];
$previous_sibling = 0;
$passed = false;
foreach ($mm['nodes'][$parent_id]['children'] as $cid)
if ($cid==$active_node){
if ($previous_sibling!=0) break;
$passed = true;
}
else {
$previous_sibling = $cid;
if ($passed) break;
}
$mm['nodes'][$parent_id]['children'] =
array_diff(
$mm['nodes'][$parent_id]['children'],
[$active_node]
);
if (count($mm['nodes'][$parent_id]['children'])==0)
$mm['nodes'][$parent_id]['is_leaf'] = true;
unset($mm['nodes'][ $active_node ]);
if ($mm['nodes'][$parent_id]['is_leaf'])
$mm['active_node'] = $parent_id;
else
$mm['active_node'] = $previous_sibling;
}
}
function delete_children(&$mm,$id){
foreach (($mm['nodes'][$id]['children'] ?? []) as $cid) {
delete_children($mm, $cid);
unset($mm['nodes'][$cid]);
}
}
// }}}
// {{{ focus
function toggle_focus(&$mm){
$mm['focus_lock'] = !$mm['focus_lock'];
message($mm, $mm['focus_lock'] ? 'Focus locked' : 'Focus unlocked');
build_map($mm);
display($mm);
}
function focus(&$mm){
collapse_siblings($mm, $mm['active_node']);
expand_siblings($mm, $mm['active_node']);
}
function collapse_siblings(&$mm, $id){
if ($id <= $mm['root']) return;
$parent_id = $mm['nodes'][$id]['parent'];
foreach ($mm['nodes'][$parent_id]['children'] as $cid)
if ($cid!=$id)
$mm['nodes'][$cid]['collapsed'] = true;
collapse_siblings($mm, $parent_id);
}
function expand_siblings(&$mm, $id){
if ($mm['nodes'][$id]['is_leaf']) return;
$mm['nodes'][$id]['collapsed'] = false;
foreach ($mm['nodes'][$id]['children'] as $cid)
expand_siblings($mm, $cid);
}
// }}}
// {{{ collapse
function collapse_all(&$mm){
foreach ($mm['nodes'] as $id=>$node)
if (!$node['is_leaf'] && $id!=0 && $id!=$mm['root'])
$mm['nodes'][$id]['collapsed'] = true;
$mm['active_node'] = $mm['root'];
build_map($mm);
center_active_node($mm);
display($mm);
}
function collapse(&$mm, $id, $keep){
if ($mm['nodes'][$id]['is_leaf']) return;
if ($keep<=0) {
$mm['nodes'][$id]['collapsed'] = true;
} else {
$mm['nodes'][$id]['collapsed'] = false;
foreach ($mm['nodes'][$id]['children'] as $cid)
collapse($mm, $cid, $keep-1);
}
}
function collapse_level(&$mm, $level, $no_display = false){
collapse($mm, $mm['root'], $level);
$id_collapsed = [];
$current = $mm['active_node'];
while ($current != $mm['root']) {
$id_collapsed[$current] = $mm['nodes'][$current]['collapsed'];
$current = $mm['nodes'][$current]['parent'];
}
$id_collapsed = array_reverse( $id_collapsed, true);
foreach ($id_collapsed as $id=>$collapsed)
if ($collapsed) {
$mm['active_node'] = $id;
break;
}
if ($no_display) return;
build_map($mm);
center_active_node($mm);
display($mm);
}
// }}}
// {{{ display
function display(&$mm, $force_center = false){
if ($mm['focus_lock']) {
focus($mm);
build_map($mm);
}
if ($mm['center_lock'] || $force_center)
center_active_node($mm);
else
move_window($mm);
$mm['terminal_width'] = exec('tput cols');
$mm['terminal_height'] = exec('tput lines');
$blank = str_repeat(' ', $mm['terminal_width']);
// calculating the coordinates of the active node
$x1 =
max(
0,
$mm['nodes'][ $mm['active_node'] ]['x'] -
1 -
$mm['win_left']
);
$x2 =
$mm['nodes'][ $mm['active_node'] ]['w'] +
$x1 +
2 -
($mm['active_node']==0 && left_padding==0);
$y1 =
max(
0,
$mm['nodes'][ $mm['active_node'] ]['y'] +
$mm['nodes'][ $mm['active_node'] ]['yo'] -
$mm['win_top']
);
$y2 =
$mm['nodes'][ $mm['active_node'] ]['lh'] +
$y1;
// building the output
$output = '';
for ( $y = 0 ; $y < $mm['terminal_height'] ; $y++ ) {
if (isset($mm['map'][$y+$mm['win_top']]))
$line =
mb_substr(
$mm['map'][$y+$mm['win_top']],
$mm['win_left'],
$mm['terminal_width']
);
else
$line = $blank;
// this one really depends on (x,y), but after this, the
// coordinates are not reliable anymore because of the
// added escape codes.
if ( $y >= $y1 && $y < $y2 )
$line =
mb_substr($line, 0, $x1).
$mm['active_node_color'].
mb_substr($line, $x1, $x2-$x1).
reset_color.
mb_substr($line, $x2);
// this is here, because it doesn't need coordinates.
// only inverting instead of giving another color,
// because, otherwise, it would conflict with the active
// node's highlight and it's a little complicated to fix.
if ($mm['query'] ?? '' != '')
$line =
str_ireplace(
$mm['query'],
invert_on.$mm['query'].invert_off,
$line);
// styling the collapsed symbol
$line =
str_replace(
' [+]',
' '.
collapsed_symbol_on.
'[+]'.
collapsed_symbol_off,
$line
);
// styling the lines
mb_regex_encoding("UTF-8");
mb_internal_encoding("UTF-8");
$line =
mb_ereg_replace(
'([─-]+)',
line_on.'\\1'.line_off,
$line
);
// adding the logo
if ($mm['show_logo'] > 0) {
if ($y==2) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' ╭────────────╮ '.default_color;
elseif ($y==3) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' │ ┏━ m │ '.default_color;
elseif ($y==4) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' │ h ━━┫ │ '.default_color;
elseif ($y==5) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' │ ┗━━━ m │ '.default_color;
elseif ($y==6) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' ╰────────────╯ '.default_color;
elseif ($y==7) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' '.default_color;
elseif ($y==8) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' hackers '.default_color;
elseif ($y==9) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' mind '.default_color;
elseif ($y==10) $line = mb_substr($line, 0, $mm['terminal_width']-19).logo_color. ' map '.default_color;
}
// done!
$output .= $line;
}
echo reset_page.reset_color.$output;
$mm['show_logo']--;
}
// }}}
// {{{ monitor key presses
function monitor_key_presses(&$mm){
stream_set_blocking(STDIN,false);
while(true){
usleep(20000);
$in = fread(STDIN, 16);
if (empty($in)) continue;
// --- easier than :wq? ;)
if ($in=='q') quit($mm);
if ($in=="\003") quit($mm); // ctrl+c
if ($in=='Q') exit;
// --- only changing the view ---
if ($in=='m') go_to_root($mm);
if ($in=='~') go_to_root($mm);
if ($in=='g') go_to_top($mm);
if ($in=='G') go_to_bottom($mm);
if ($in=="\033\133\102") change_active_node($mm, move_down);
if ($in=="\033\133\103") change_active_node($mm, move_right);
if ($in=="\033\133\101") change_active_node($mm, move_up);
if ($in=="\033\133\104") change_active_node($mm, move_left);
if ($in=='j') change_active_node($mm, move_down);
if ($in=='l') change_active_node($mm, move_right);
if ($in=='k') change_active_node($mm, move_up);
if ($in=='h') change_active_node($mm, move_left);
if ($in=='w') adjust_width($mm, width_wider);
if ($in=='W') adjust_width($mm, width_narrower);
if ($in=='Z') adjust_spacing($mm, spacing_wider);
if ($in=='z') adjust_spacing($mm, spacing_narrower);
if ($in=='y') yank_node($mm);
if ($in=='Y') yank_node($mm, true);
if ($in=='f') { focus($mm); build_map($mm); display($mm,true); }
if ($in=='F') toggle_focus($mm);
if ($in=='c') { center_active_node($mm); display($mm); }
if ($in=='C') { $mm['center_lock'] = !$mm['center_lock']; display($mm); }
if ($in=='/') search($mm);
if ($in=='n') next_search_result($mm);
if ($in=='N') previous_search_result($mm);
if ($in=='s') save($mm);
if ($in=='S') save($mm, true);
if ($in==' ') toggle($mm);
if ($in=='b') expand_all($mm);
if ($in=='v') collapse_all($mm);
if ($in=='1') collapse_level($mm, 1);
if ($in=='2') collapse_level($mm, 2);
if ($in=='3') collapse_level($mm, 3);
if ($in=='4') collapse_level($mm, 4);
if ($in=='5') collapse_level($mm, 5);
if ($in=='6') collapse_level($mm, 6);
if ($in=='7') collapse_level($mm, 7);
if ($in=='8') collapse_level($mm, 8);
if ($in=='9') collapse_level($mm, 9);
if ($in=='d') delete_node($mm);
if ($in=='D') delete_node($mm, true);
if ($in=='p') paste_sub_tree($mm, false);
if ($in=='P') paste_sub_tree($mm, true);
if ($in=='J') move_active_node_down($mm);
if ($in=='K') move_active_node_up($mm);
if ($in=='i' || $in=="\n") insert_node($mm, insert_sibling);
if ($in=='I' || $in=="\t") insert_node($mm, insert_child);
if ($in=='e') edit_node($mm);
if ($in=='E') edit_node($mm, true);
if ($in=='U') debug($mm['nodes']);
// move(1,1);
// for ($i=0; $i<strlen($in); $i++)
// echo ord($in[$i]).' ';
// echo ' ';
}
}
// }}}
// {{{ main
set_up_screen();
load_settings($mm);
clear();
load_file($mm);
collapse_level($mm, $mm['initial_depth'], true);
build_map($mm);
center_active_node($mm);
display($mm);
monitor_key_presses($mm);
// }}}