h-m-m/h-m-m
2022-09-18 13:48:51 +02:00

2663 lines
53 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['initial_depth'] = 1;
$mm['center_lock'] = false;
$mm['focus_lock'] = false;
$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['question_color'] = "\033[38;5;168m";
$mm['changes'] = [];
$mm['change_active_node'] = [];
$mm['change_index'] = 0;
$mm['change_max_steps'] = 24;
mb_internal_encoding("UTF-8");
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 'undo_steps': $mm['change_max_steps'] = max( round($value), 0 ); 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
// escape codes: https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
$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 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 BOM = "\xEF\xBB\xBF";
$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 );
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;
const ctrl_p = "\020";
const ctrl_c = "\003";
const ctrl_r = "\022";
const ctrl_f = "\006";
const ctrl_v = "\026";
const arr_down = "\033\133\102";
const arr_right = "\033\133\103";
const arr_up = "\033\133\101";
const arr_left = "\033\133\104";
const del = "\033\133\063\176";
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[38;5;215m";
const collapsed_symbol_off = "\033[0m";
// }}}
// {{{ checking the requirements
function check_required_extensions(): bool
{
if (!function_exists('mb_strlen'))
{
echo 'Required extension mbstring is not enabled; please check your php installation!';
echo PHP_EOL;
return false;
}
return true;
}
function check_the_available_clipboard_tool(&$mm)
{
if (PHP_OS_FAMILY === "Windows")
{
$mm['clipboard']['write'] = "clip";
$mm['clipboard']['read'] = 'powershell -sta "add-type -as System.Windows.Forms; [windows.forms.clipboard]::GetText()"';
return;
}
if (PHP_OS_FAMILY === "Darwin")
{
$mm['clipboard']['write'] = "pbcopy";
$mm['clipboard']['read'] = 'pbpaste';
return;
}
// now, the main OS ;)
exec('command -v xclip xsel wl-copy', $result);
$tool = basename($result[0] ?? '');
if (trim($tool)==='')
{
echo "Can't find your clipboard tool! I expected to find xclip, xsel, or wl-clipboard.\n";
shutdown();
}
switch ($tool)
{
case 'xclip':
$mm['clipboard']['write'] = 'xclip -selection clipboard';
$mm['clipboard']['read'] = 'xclip -out -selection clipboard';
break;
case 'xsel':
$mm['clipboard']['write'] = 'xsel --clipboard';
$mm['clipboard']['read'] = 'xsel --clipboard';
break;
case 'wl-copy':
$mm['clipboard']['write'] = 'wl-copy';
$mm['clipboard']['read'] = 'wl-paste';
break;
default:
echo "I can't find your clipboard tool!\n";
shutdown();
}
}
// }}}
// {{{ alternative screen
function shutdown()
{
clear();
system("tput rmcup && tput cnorm && stty sane");
exit;
}
if (false === check_required_extensions())
return 1;
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)
{
$y = round($y);
$mm['map'][$y]
= mb_substr( $mm['map'][$y], 0, $x)
. $s
. mb_substr( $mm['map'][$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"
;
echo " id pr x w y yo h lh clh title\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] =
mb_ereg_replace
(
"[\000-\010\013-\037\177]|".BOM
,''
,str_replace
(
[ "\t", "\n", "\r" ]
,[ " ", " ", " " ]
,$lines[$lid]
)
)
;
$indentation = mb_strlen($lines[$lid]) - mb_strlen(ltrim($lines[$lid]));
$start = mb_substr($lines[$lid], $indentation, 2);
if ($start=='* ' || $start=='- ')
{
$lines[$lid][$indentation] = ' ';
$indentation += 2;
}
if (trim($lines[$lid])!='')
$indentation_shift =
min
(
$indentation_shift
,$indentation
)
;
}
// 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(trim($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']
)
+ ($mm['nodes'][$id]['parent'] == $mm['root'])
* $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 in the same y coordinate
if (count($node['children'] ?? [])==1)
{
$child_id = $node['children'][ array_key_first( $node['children'] ) ];
$child = $mm['nodes'][ $child_id ];
$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
,$child['x'] - conn_left_len - conn_right_len
,min($y1,$y2)
,$mm['conn_single']
);
if (abs(min($y1,$y2)-$y2)>0)
{
for ($yy=min($y1,$y2); $yy<max($y1,$y2); $yy++)
mput
(
$mm
,$child['x'] - 2
,$yy
,'│'
);
mput
(
$mm
,$child['x'] - 2
,$y2
,( $y2 > $y1 ? '╰' : '╭' )
);
mput
(
$mm
,$child['x'] - 2
,min($y1,$y2)
,( $y2 > $y1 ? '╮' : '╯' )
);
}
draw_connections($mm, $child_id );
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'],
$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']);
// 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'])
$mm['active_node'] = $mm['nodes'][ $mm['active_node'] ]['parent'];
$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;
push_change($mm);
$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;
push_change($mm);
$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);
}
// }}}
// {{{ magic readline!
function magic_readline(&$mm, $title)
{
$in = '';
$cursor = mb_strlen($title)+1;
$shift = max( 0, $cursor - $mm['terminal_width'] );
show_line($mm, $title, $cursor, $shift);
while(true)
{
usleep(5000);
$in = fread(STDIN, 66666);
// normally, the longest sequence we have is 13 bytes,
// but if ctrl+shift+v is used, the whole text will be passed!
// In other words, we don't receive a ctrl+shift+v input,
// but the actual content. Was that a confusing behavior? Of course!!!
if ($in != '')
{
// Esc.
if ($in=="\033")
{
display($mm);
message($mm, 'Editing cancelled');
return false;
}
// up arrow and home
elseif ($in=="\033\133\101" || $in=="\033\133\110")
$cursor = 1;
// right arrow
elseif ($in=="\033\133\103")
$cursor = min( mb_strlen($title)+1, $cursor+1);
// down arrow and end
elseif ($in=="\033\133\102" || $in=="\033\133\106")
$cursor = mb_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,
(
mb_strrpos($title,' ',$cursor-mb_strlen($title)-3) !== false
? mb_strrpos($title,' ',$cursor-mb_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 > mb_strlen($title) -2
? mb_strlen($title) + 1
: min
(
mb_strlen($title)+1,
(
mb_strpos($title,' ',$cursor+1) !== false
? mb_strpos($title,' ',$cursor+1) + 2
: mb_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 =
mb_substr
(
$title
,0
,$cursor-2
)
.mb_substr
(
$title
,$cursor-1
);
$cursor--;
}
}
// ctrl+delete
elseif ($in=="\033\133\63\073\065\176")
{
$title = '';
$cursor = 1;
}
// delete
elseif ($in=="\033\133\63\176")
{
$title =
mb_substr
(
$title
,0
,$cursor-1
)
.mb_substr
(
$title
,$cursor
);
}
// enter
elseif ($in=="\012")
return trim($title);
// ctrl+v
elseif ($in==ctrl_v)
{
$content =
trim
(
str_replace
(
["\n", "\r", "\t"]
,[" ", "", " " ]
,get_from_clipboard($mm)
)
);
$title =
mb_substr
(
$title
,0
,$cursor-1
)
.$content
.mb_substr
(
$title
,$cursor-1
);
$cursor += mb_strlen($content);
}
// normal characters
else
{
if ($in=="\011") $in=' ';
$title =
mb_substr
(
$title
,0
,$cursor-1
)
.$in
.mb_substr
(
$title
,$cursor-1
)
;
$title = mb_ereg_replace("[\000-\010\013-\037\177]|".BOM,'',$title);
$cursor += mb_strlen($in);
// the input content can be longer than one character if
// the user uses ctrl+shift+v to paste.
}
// adjusting the position and shift
$shift = max( 0, $shift, $cursor - $mm['terminal_width'] );
$shift = min( $shift, $cursor-1 );
show_line($mm, $title, $cursor, $shift);
}
}
}
function show_line(&$mm, $title, $cursor, $shift)
{
$output = mb_substr($title,$shift,$mm['terminal_width']-1);
$output .= str_repeat( ' ' ,$mm['terminal_width'] - mb_strlen($output) );
// showing the cursor
$output =
mb_substr
(
$output
,0
,$cursor-$shift-1
)
.invert_on
.mb_substr
(
$output
,$cursor-$shift-1
,1
)
.invert_off
.mb_substr
(
$output
,$cursor-$shift
);
put(0,$mm['terminal_height'],$mm['active_node_color'].$output);
}
// }}}
// {{{ edit node
function edit_node(&$mm, $rewrite = false)
{
$title = $rewrite ? '' : $mm['nodes'][ $mm['active_node'] ]['title'];
if ($mm['active_node']==0 && $title=='root') $title='';
$output = magic_readline($mm, $title);
if ($output === false)
{
display($mm);
message($mm, 'Editing cancelled');
return;
}
$mm['nodes'][ $mm['active_node'] ]['title'] = $output;
push_change($mm);
$mm['modified'] = true;
build_map($mm);
display($mm);
}
// }}}
// {{{ 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)
{
$mm['query'] = magic_readline($mm,'');
if (empty($mm['query']))
{
display($mm);
return;
}
if (!next_search_result($mm))
previous_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 false;
}
$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)
return false;
$mm['active_node'] = $nid;
display($mm);
}
// }}}
// {{{ move active node
function move_active_node_down(&$mm)
{
if ($mm['active_node']==0) return;
push_change($mm);
$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;
push_change($mm);
$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);
}
// }}}
// {{{ export html
function export_html(&$mm)
{
if (empty($mm['filename']))
{
message($mm, "Can't export the map when it doesn't have a file name yet. Save it first.");
return;
}
$file = fopen($mm['filename'].'.html', "w");
if ($file===false)
{
message($mm, 'ERROR! Could not save the file');
return;
}
fwrite
(
$file
,'<!DOCTYPE html>'
.'<html lang=en>'
.'<head>'
.'<title>'
.$mm['nodes'][ $mm['root'] ]['title']
.'</title>'
.'<meta charset="UTF-8">'
.'<meta name=viewport content="width=device-width,initial-scale=1,user-scalable=yes">'
.'<style>'
.'body { background-color: #222; color: #ddd; font-family: monospace; padding: 0; font-size: 16px; }'
.'#root {margin:10px 0}'
.'p:before { content: "━ "; }'
.'p, summary { padding: 8px; margin: 0; font-size: 16px; }'
.'details, p { padding-left: 29px; border-left: 3px solid #444; font-size:16px; }'
.'summary { margin-left: -10px; cursor: pointer; }'
.'summary:hover, p:hover { color: #fbc531; }'
.'details:hover, p:hover { border-color: #e1b12c; }'
.'#source { position: absolute; bottom: 0; left: 0; padding: 5px 15px 8px 15px; margin: 100px 0 0 0; }'
.'#source { background-color: #333; border: none; box-sizing: border-box;}'
.'#source > summary { list-style: none; }'
.'#source[open] { position: static; margin: 100px 0 0 0; font-size: 16px; }'
.'#map { margin: 40px 30px; }'
.'</style>'
.'</head>'
.'<body>'
.'<div id=map>'
.export_html_node($mm, $mm['root'])
.'</div>'
.'<details id=source>'
.'<summary>This is a limited, read-only version of a mind-map created in h-m-m | view source!</summary>'
.'<pre>'
.encode_tree($mm,$mm['root'])
.'</pre>'
.'</details>'
.'</body>'
.'</html>'
);
fclose($file);
message($mm, 'Exported as '.$mm['filename'].'.html');
copy_to_clipboard($mm, $mm['filename'].'.html');
}
function export_html_node(&$mm, $parent_id)
{
if ($mm['nodes'][$parent_id]['children']==[])
{
$output =
"<p>"
.$mm['nodes'][$parent_id]['title']
."</p>";
}
elseif ($parent_id==$mm['root'])
{
$output =
"<div id=root>"
.$mm['nodes'][$parent_id]['title']
."</div>";
foreach ($mm['nodes'][$parent_id]['children'] as $cid)
$output .= export_html_node($mm, $cid);
}
else
{
$output =
"<details>"
."<summary>"
.$mm['nodes'][$parent_id]['title']
."</summary>";
foreach ($mm['nodes'][$parent_id]['children'] as $cid)
$output .= export_html_node($mm, $cid);
$output .=
"</details>";
}
return $output;
}
// }}}
// {{{ save
function save(&$mm, $new_name = false)
{
if ($new_name || empty($mm['filename']))
{
$new_name = magic_readline($mm, empty($mm['filename']) ? exec('pwd') : $mm['filename']);
if ($new_name === false)
{
display($mm);
message($mm, 'Saving cancelled');
return;
}
$mm['filename'] = $new_name;
$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'] - mb_strlen($text) - 1,
$mm['terminal_height'],
$mm['message_color'].' '.$text.' '.reset_color
);
usleep(200000);
}
// }}}
// {{{ quit
function quit(&$mm)
{
if (($mm['modified'] ?? false) === false) shutdown();
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']);
}
// }}}
// {{{ changes
function push_change(&$mm)
{
// flush any redo chain
while(count($mm['changes']) > $mm['change_index'])
{
array_pop($mm['changes']);
array_pop($mm['change_active_node']);
}
// removing the old history if it's getting bigger than the maximum
if (count($mm['changes']) >= $mm['change_max_steps'])
{
array_shift($mm['changes']);
array_shift($mm['change_active_node']);
$mm['change_index']--;
}
array_push($mm['changes'], $mm['nodes']);
array_push($mm['change_active_node'], $mm['active_node']);
$mm['change_index']++;
}
function undo(&$mm)
{
if($mm['change_index'] == 0)
return;
$mm['nodes'] = $mm['changes'][$mm['change_index'] - 1];
$mm['active_node'] = $mm['change_active_node'][$mm['change_index'] - 1];
$mm['change_index']--;
build_map($mm);
display($mm);
}
function redo(&$mm)
{
if(count($mm['changes']) == $mm['change_index'])
return;
$mm['nodes'] = $mm['changes'][$mm['change_index']];
$mm['active_node'] = $mm['change_active_node'][$mm['change_index']];
$mm['change_index']++;
build_map($mm);
display($mm);
}
// }}}
// {{{ 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']) return;
// auto-unfold node on right move
if ($node['collapsed'] ?? false) {
toggle($mm);
$node = $mm['nodes'][ $mm['active_node'] ];
}
$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;
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;
}
// }}}
// {{{ append
function append(&$mm)
{
$mm['nodes'][ $mm['active_node'] ]['title'] .=
' '
.str_replace
(
["\n","\r","\t"]
,[' ','', ' ']
,trim(get_from_clipboard($mm))
)
;
build_map($mm);
display($mm);
}
// }}}
// {{{ paste sub-tree
function paste_sub_tree(&$mm, $as_sibling )
{
if ($as_sibling && $mm['active_node']==$mm['root']) return;
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($mm)),
$parent_id,
$new_id
)
;
if ($st==[]) return;
push_change($mm);
$mm['modified'] = true;
$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(&$mm, $text)
{
$clip = popen($mm['clipboard']['write'],'wb');
if (!isset($clip)) return;
fwrite($clip,$text);
pclose($clip);
}
function get_from_clipboard(&$mm)
{
return
mb_ereg_replace
(
"[\000-\010\013-\037\177]|".BOM
,''
,shell_exec($mm['clipboard']['read'])
)
;
}
// }}}
// {{{ 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($mm, 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, $dont_copy_to_clipboard = false )
{
if ($mm['active_node']==$mm['root'])
$exclude_parent = true;
if (!$dont_copy_to_clipboard)
copy_to_clipboard($mm, encode_tree($mm, $mm['active_node'], $exclude_parent) );
push_change($mm);
$mm['modified'] = true;
delete_node_internal($mm, $mm['active_node'], $exclude_parent);
build_map($mm);
display($mm);
if (!$dont_copy_to_clipboard)
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_other_branches(&$mm)
{
if ($mm['active_node'] == $mm['root'])
return;
$branch = find_branch($mm, $mm['active_node']);
foreach ($mm['nodes'][ $mm['root'] ]['children'] as $bid)
if ($bid != $branch)
$mm['nodes'][$bid]['collapsed'] = true;
build_map($mm);
center_active_node($mm);
display($mm);
}
function collapse_inner(&$mm)
{
foreach ($mm['nodes'][ $mm['active_node'] ]['children'] as $cid)
$mm['nodes'][$cid]['collapsed'] = true;
$mm['nodes'][ $mm['active_node'] ]['collapsed'] = false;
build_map($mm);
center_active_node($mm);
display($mm);
}
function find_branch(&$mm, $cid)
{
if ($mm['nodes'][$cid]['parent'] == $mm['root'])
return $cid;
else
return find_branch($mm, $mm['nodes'][$cid]['parent']);
}
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)
;
else // styling the codes when the node is not active
$line =
mb_ereg_replace
(
'\b(.\d)\. '
,dim_on.'\\1. '.dim_off
,$line
)
;
// styling the search results
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
$line =
mb_ereg_replace
(
'([─-]+)'
,line_on.'\\1'.line_off
,$line
)
;
// styling "???"
$line =
str_replace
(
'???'
,$mm['question_color'].'???'.default_color
,$line
);
// dimming {meta}s
$line =
str_replace
(
'{'
,dim_on.'{'
,$line
);
$line =
str_replace
(
'}'
,'}'.dim_off
,$line
);
// done!
$output .= $line;
}
echo reset_page.reset_color.$output;
}
// }}}
// {{{ 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;
switch ($in)
{
case 'a': edit_node($mm); break;
case 'A': edit_node($mm, true); break;
case 'b': expand_all($mm); break;
case 'c': { center_active_node($mm); display($mm); } break;
case 'C': { $mm['center_lock'] = !$mm['center_lock']; display($mm); } break;
case ctrl_c: quit($mm); break;
case 'd': delete_node($mm); break;
case 'D': delete_node($mm, true); break;
case del: delete_node($mm, false, true); break;
case 'e': edit_node($mm); break;
case 'E': edit_node($mm, true); break;
case 'f': { focus($mm); build_map($mm); display($mm,true); } break;
case 'F': toggle_focus($mm); break;
case 'g': go_to_top($mm); break;
case 'G': go_to_bottom($mm); break;
case 'h': change_active_node($mm, move_left); break;
case 'i': edit_node($mm); break;
case 'I': edit_node($mm, true); break;
case 'j': change_active_node($mm, move_down); break;
case 'J': move_active_node_down($mm); break;
case 'k': change_active_node($mm, move_up); break;
case 'K': move_active_node_up($mm); break;
case 'l': change_active_node($mm, move_right); break;
case 'm': go_to_root($mm); break;
case 'n': next_search_result($mm); break;
case 'N': previous_search_result($mm); break;
case 'o': insert_node($mm, insert_sibling); break;
case 'O': insert_node($mm, insert_child); break;
case 'p': paste_sub_tree($mm, false); break;
case 'P': paste_sub_tree($mm, true); break;
case ctrl_p: append($mm); break;
case 'q': quit($mm); break;
case 'Q': shutdown(); break;
case ctrl_r: redo($mm); break;
case 'r': collapse_other_branches($mm); break;
case 'R': collapse_inner($mm); break;
case 's': save($mm); break;
case 'S': save($mm, true); break;
case 'u': undo($mm); break;
case 'U': debug($mm['nodes']); break;
case 'v': collapse_all($mm); break;
case 'w': adjust_width($mm, width_wider); break;
case 'W': adjust_width($mm, width_narrower); break;
case 'x': export_html($mm); break;
case 'y': yank_node($mm); break;
case 'Y': yank_node($mm, true); break;
case 'Z': adjust_spacing($mm, spacing_wider); break;
case 'z': adjust_spacing($mm, spacing_narrower); break;
case arr_down: change_active_node($mm, move_down); break;
case arr_right: change_active_node($mm, move_right); break;
case arr_up: change_active_node($mm, move_up); break;
case arr_left: change_active_node($mm, move_left); break;
case '1': collapse_level($mm, 1); break;
case '2': collapse_level($mm, 2); break;
case '3': collapse_level($mm, 3); break;
case '4': collapse_level($mm, 4); break;
case '5': collapse_level($mm, 5); break;
case '6': collapse_level($mm, 6); break;
case '7': collapse_level($mm, 7); break;
case '8': collapse_level($mm, 8); break;
case '9': collapse_level($mm, 9); break;
case '~': go_to_root($mm); break;
case ' ': toggle($mm); break;
case '/': search($mm); break;
case '?': search($mm); break;
case ctrl_f: search($mm); break;
case "\n": insert_node($mm, insert_sibling); break;
case "\t": insert_node($mm, insert_child); break;
}
// uncomment the following to discover the escape codes:
// move(1,1);
// for ($i=0; $i<strlen($in); $i++)
// echo base_convert(ord($in[$i]),10,8).' ';
// echo ' ';
}
}
// }}}
// {{{ main
check_the_available_clipboard_tool($mm);
set_up_screen();
load_settings($mm);
clear();
load_file($mm);
collapse_all($mm);
collapse_level($mm, $mm['initial_depth'], true);
build_map($mm);
center_active_node($mm);
display($mm);
monitor_key_presses($mm);
// }}}