mirror of
https://github.com/nadrad/h-m-m.git
synced 2024-06-07 03:42:13 +02:00
ae185ceb02
The shutdown() function clears the screen which makes it very hard to debug what is wrong. Thus exiting with exit(1) which is also used in other error conditions.
2682 lines
52 KiB
PHP
Executable file
2682 lines
52 KiB
PHP
Executable file
#!/usr/bin/env php
|
||
<?php
|
||
|
||
/**
|
||
* h-m-m
|
||
*
|
||
* h-m-m (pronounced like the interjection "hmm") is a simple, fast, keyboard-centric terminal-based tool for working with mind maps.
|
||
*
|
||
* @author Nader K. Rad <me@nader.pm>
|
||
* @link https://github.com/nadrad/h-m-m
|
||
* @license https://www.gnu.org/licenses/gpl-3.0.en.html GPLv3
|
||
*/
|
||
|
||
|
||
// {{{ settings and defaults
|
||
|
||
mb_internal_encoding("UTF-8");
|
||
|
||
|
||
// $mm, short for "mind map", is an array that contains the
|
||
// 'nodes' array, as well as the other settings and parameters
|
||
// required for building and showing a mind map.
|
||
//
|
||
// escape codes for colors:
|
||
// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
||
|
||
$mm=[];
|
||
|
||
|
||
// parsing the command line arguments
|
||
|
||
$mm['arguments'] = [];
|
||
$alen = count($argv);
|
||
|
||
for ( $i = 1 ; $i < $alen ; $i++ )
|
||
if (substr($argv[$i],0,2) == '--')
|
||
{
|
||
$a = explode('=', substr($argv[$i],2));
|
||
$mm['arguments'][ str_replace('-','_',$a[0]) ] = ( $a[1] ?? true );
|
||
}
|
||
elseif (isset($mm['arguments']['filename']))
|
||
{
|
||
echo "Invalid arguments: more than one filename!\n";
|
||
exit(1);
|
||
}
|
||
else
|
||
$mm['arguments']['filename'] = $argv[$i];
|
||
|
||
|
||
// parsing the config file
|
||
|
||
$conf =
|
||
$mm['arguments']['config']
|
||
??
|
||
(
|
||
PHP_OS_FAMILY === 'Windows'
|
||
? $argv[0].'.conf'
|
||
:
|
||
(
|
||
PHP_OS_FAMILY === 'Darwin'
|
||
? getenv('HOME').'/Library/Preferences/h-m-m/h-m-m.conf'
|
||
: getenv('HOME').'/.config/h-m-m/h-m-m.conf'
|
||
)
|
||
)
|
||
;
|
||
|
||
if (file_exists($conf))
|
||
{
|
||
$handle = fopen($conf, "r");
|
||
if ($handle)
|
||
{
|
||
while (($line = fgets($handle)) !== false)
|
||
{
|
||
if (empty(trim($line))) continue;
|
||
$c = explode('=',trim($line));
|
||
$mm['config'][ str_replace('-','_',trim($c[0] ?? 'NA')) ] = trim($c[1] ?? 'NA');
|
||
}
|
||
fclose($handle);
|
||
}
|
||
}
|
||
|
||
|
||
// parsing the environment variables
|
||
|
||
$mm['env']=[];
|
||
$e = getenv();
|
||
foreach ($e as $k=>$v)
|
||
if (substr($k,0,4)=='hmm_')
|
||
$mm['env'][str_replace('-','_',substr($k,4))] = $v ?? '';
|
||
|
||
|
||
// and now, the settings!
|
||
// arguments > environment variables > config file > default
|
||
|
||
config($mm, 'max_parent_node_width', 25);
|
||
config($mm, 'max_leaf_node_width', 55);
|
||
config($mm, 'line_spacing', 1);
|
||
|
||
config($mm, 'initial_depth', 1);
|
||
config($mm, 'center_lock', false);
|
||
config($mm, 'focus_lock', false);
|
||
|
||
config($mm, 'max_undo_steps', 24);
|
||
|
||
config($mm, 'active_node_color', "\033[38;5;0m\033[48;5;172m\033[1m");
|
||
config($mm, 'message_color', "\033[38;5;0m\033[48;5;141m\033[1m");
|
||
config($mm, 'doubt_color', "\033[38;5;168m");
|
||
|
||
|
||
$mm['changes'] = [];
|
||
$mm['change_active_node'] = [];
|
||
$mm['change_index'] = 0;
|
||
|
||
$mm['top_left_column'] = 0;
|
||
$mm['top_left_row'] = 0;
|
||
|
||
$mm['terminal_width'] = (int)exec('tput cols');
|
||
$mm['terminal_height'] = (int)exec('tput lines');
|
||
|
||
$mm['viewport_left'] = 0;
|
||
$mm['viewport_top'] = 0;
|
||
|
||
$mm['root_id'] = 2;
|
||
|
||
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 left_padding = 1;
|
||
|
||
const conn_left_len = 6;
|
||
const conn_right_len = 4;
|
||
|
||
$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 BOM = "\xEF\xBB\xBF";
|
||
const default_color = "\033[0m";
|
||
const clear_screen = "\033[2J";
|
||
|
||
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";
|
||
|
||
const insert_sibling = 0;
|
||
const insert_child = 1;
|
||
|
||
|
||
function config(&$mm, $key, $default)
|
||
{
|
||
$mm[$key] =
|
||
$mm['arguments'][$key]
|
||
?? $mm['env'][$key]
|
||
?? $mm['config'][$key]
|
||
?? $default;
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ checking the requirements
|
||
|
||
function check_required_extensions()
|
||
{
|
||
if (!function_exists('mb_strlen'))
|
||
{
|
||
echo 'Required extension mbstring is not enabled; please check your php installation!';
|
||
echo PHP_EOL;
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
|
||
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";
|
||
exit(1);
|
||
}
|
||
|
||
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";
|
||
exit(1);
|
||
}
|
||
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ alternative screen buffer
|
||
|
||
function enable_alternate_screen()
|
||
{
|
||
// https://www.ibm.com/docs/en/aix/7.1?topic=s-stty-command
|
||
system('stty cbreak -echo -crterase intr undef');
|
||
|
||
echo
|
||
"\033[?1049h" // enabling the alternate screen buffer
|
||
."\033[?25l" // disabling the text cursor
|
||
."\033[?9;1000;1001;1002;1003;1004;1007;1005;1006;1015;1016l" // disabling the mouse
|
||
.clear_screen
|
||
;
|
||
}
|
||
|
||
|
||
function shutdown()
|
||
{
|
||
echo
|
||
clear_screen
|
||
."\033[?1049l" // enabling the default screen buffer
|
||
."\033[?25h" // enabling the text cursor
|
||
;
|
||
system("stty sane");
|
||
exit;
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ list to map converter
|
||
|
||
function list_to_map($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" ]
|
||
,[ " ", " " ]
|
||
,$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);
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ map to list converter
|
||
|
||
function map_to_list(&$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 .= map_to_list($mm, $cid, false, $base+1-$exclude_parent);
|
||
|
||
return $output;
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ 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 = list_to_map($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_id'] = 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_node_width']
|
||
+ !($node['is_leaf'] || ($node['collapsed'] ?? false)) * $mm['max_parent_node_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_id'])
|
||
* $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)
|
||
{
|
||
mmput
|
||
(
|
||
$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 );
|
||
|
||
mmput
|
||
(
|
||
$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++)
|
||
mmput
|
||
(
|
||
$mm
|
||
,$child['x'] - 2
|
||
,$yy
|
||
,'│'
|
||
);
|
||
|
||
mmput
|
||
(
|
||
$mm
|
||
,$child['x'] - 2
|
||
,$y2
|
||
,( $y2 > $y1 ? '╰' : '╭' )
|
||
);
|
||
|
||
mmput
|
||
(
|
||
$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);
|
||
|
||
mmput
|
||
(
|
||
$mm
|
||
,$mm['nodes'][$top_child]['x'] - conn_left_len - conn_right_len
|
||
,$middle
|
||
,$mm['conn_left']
|
||
);
|
||
|
||
for ( $i = $top ; $i < $bottom ; $i++ )
|
||
mmput
|
||
(
|
||
$mm
|
||
,$mm['nodes'][$top_child]['x'] - conn_right_len
|
||
,$i
|
||
,'│'
|
||
);
|
||
|
||
mmput
|
||
(
|
||
$mm
|
||
,$mm['nodes'][$top_child]['x'] - conn_right_len
|
||
,$top
|
||
,'╭'
|
||
.$mm['conn_right']
|
||
);
|
||
|
||
mmput
|
||
(
|
||
$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)
|
||
mmput
|
||
(
|
||
$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=='│')
|
||
mmput
|
||
(
|
||
$mm
|
||
,$mm['nodes'][$top_child]['x'] - conn_right_len
|
||
,$middle
|
||
,'┤'
|
||
);
|
||
|
||
if ($existing_char=='╭')
|
||
mmput
|
||
(
|
||
$mm
|
||
,$mm['nodes'][$top_child]['x'] - conn_right_len
|
||
,$middle
|
||
,'┬'
|
||
);
|
||
|
||
if ($existing_char=='├')
|
||
mmput
|
||
(
|
||
$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_node_width']
|
||
+ !($node['is_leaf'] || ($node['collapsed'] ?? false)) * $mm['max_parent_node_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++ )
|
||
mmput
|
||
(
|
||
$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_id']);
|
||
calculate_h($mm);
|
||
calculate_y($mm);
|
||
calculate_height_shift($mm, $mm['root_id']);
|
||
|
||
// 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_id']);
|
||
add_content_to_the_map($mm, $mm['root_id']);
|
||
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ toggle node
|
||
|
||
function toggle_node(&$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);
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ change line spacing
|
||
|
||
function change_line_spacing(&$mm, $amount)
|
||
{
|
||
$mm['line_spacing'] = max(0, $mm['line_spacing'] + $amount);
|
||
|
||
build_map($mm);
|
||
center_active_node($mm);
|
||
display($mm);
|
||
message($mm,'Spacing: '.$mm['line_spacing']);
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ change max node width
|
||
|
||
function change_max_node_width(&$mm, $amount)
|
||
{
|
||
$mx = $mm['terminal_width'] - max_width_padding;
|
||
|
||
$mm['max_parent_node_width'] = round(min($mx, max( width_min, $mm['max_parent_node_width'] * $amount )));
|
||
$mm['max_leaf_node_width'] = round(min($mx, max( width_min, $mm['max_leaf_node_width'] * $amount )));
|
||
|
||
build_map($mm);
|
||
center_active_node($mm);
|
||
display($mm);
|
||
|
||
message($mm,'Width: '.$mm['max_parent_node_width'].' / '.$mm['max_leaf_node_width']);
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ push node down
|
||
|
||
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_new_node(&$mm, $type)
|
||
{
|
||
if ($mm['active_node']==$mm['root_id'])
|
||
$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
|
||
);
|
||
|
||
echo
|
||
"\033["
|
||
.$mm['terminal_height'] //y
|
||
.";0f" // 0 -> x
|
||
.$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']==$mm['root_id'] && $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['viewport_left'] = max(0, round( $midx - $mm['terminal_width']/2 ) );
|
||
|
||
$mm['viewport_top'] = round( $midy - $mm['terminal_height']/2 );
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ goto's
|
||
|
||
function go_to_root(&$mm)
|
||
{
|
||
$mm['active_node']=$mm['root_id'];
|
||
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_id'] ]['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_id'])
|
||
.'</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>'
|
||
.map_to_list($mm,$mm['root_id'])
|
||
.'</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_id'])
|
||
{
|
||
$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']))
|
||
{
|
||
$path = getenv('PWD');
|
||
|
||
if (substr($path,-1,1) != '/')
|
||
$path .= '/';
|
||
|
||
if (empty($mm['filename']) && $mm['nodes'][ $mm['root_id'] ]['title'] != 'root')
|
||
$path .= preg_replace('/[^a-zA-Z0-9]/','-',$mm['nodes'][ $mm['root_id'] ]['title']);
|
||
|
||
$new_name = magic_readline($mm, $path. ( $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, map_to_list($mm, $mm['root_id']));
|
||
fclose($file);
|
||
|
||
display($mm);
|
||
message($mm, 'Saved '.$mm['filename']);
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ message
|
||
|
||
function message(&$mm, $text)
|
||
{
|
||
echo
|
||
"\033["
|
||
.$mm['terminal_height'] // y
|
||
.";"
|
||
.$mm['terminal_width'] - mb_strlen($text) - 1 //x
|
||
."f"
|
||
.$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['viewport_left'] = min( $mm['viewport_left'], $x1);
|
||
$mm['viewport_left'] = max( $mm['viewport_left'], $x2 - $mm['terminal_width']);
|
||
|
||
$mm['viewport_top'] = min( $mm['viewport_top'], $y1);
|
||
$mm['viewport_top'] = max( $mm['viewport_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['max_undo_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, $x, $y)
|
||
{
|
||
$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 ($x > 0)
|
||
{
|
||
if ($node['is_leaf']) return;
|
||
|
||
// auto-unfold node on right move
|
||
if ($node['collapsed'] ?? false) {
|
||
toggle_node($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 ($x < 0)
|
||
{
|
||
if ($mm['active_node']==$mm['root_id']) 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 ($y < 0)
|
||
{
|
||
$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 ($y > 0)
|
||
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 ( ($y>0 && $dy>0) || ($y<0 && $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);
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ 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_id']) 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 =
|
||
list_to_map
|
||
(
|
||
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'=> basename( $mm['filename'] ?? 'root' )
|
||
,'is_leaf'=>true
|
||
,'children'=>[]
|
||
,'collapsed'=>false
|
||
,'parent'=>0
|
||
]
|
||
;
|
||
|
||
$mm['active_node']=1;
|
||
$mm['root_id']=1;
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ yank
|
||
|
||
function yank_node(&$mm, $exclude_parent = false )
|
||
{
|
||
copy_to_clipboard($mm, map_to_list($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_id'])
|
||
$exclude_parent = true;
|
||
|
||
if (!$dont_copy_to_clipboard)
|
||
copy_to_clipboard($mm, map_to_list($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_id'] && !$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_id']) 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_id'])
|
||
return;
|
||
|
||
$branch = find_branch($mm, $mm['active_node']);
|
||
|
||
foreach ($mm['nodes'][ $mm['root_id'] ]['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_id'])
|
||
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_id'])
|
||
$mm['nodes'][$id]['collapsed'] = true;
|
||
|
||
$mm['active_node'] = $mm['root_id'];
|
||
|
||
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_id'], $level);
|
||
|
||
$id_collapsed = [];
|
||
$current = $mm['active_node'];
|
||
while ($current != $mm['root_id'])
|
||
{
|
||
$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'] = (int)exec('tput cols');
|
||
$mm['terminal_height'] = (int)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['viewport_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['viewport_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['viewport_top']]))
|
||
$line =
|
||
mb_substr(
|
||
$mm['map'][$y+$mm['viewport_top']],
|
||
$mm['viewport_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['doubt_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;
|
||
}
|
||
|
||
|
||
function mmput(&$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) );
|
||
}
|
||
|
||
|
||
// }}}
|
||
// {{{ 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, -1,0); break;
|
||
|
||
case 'i': edit_node($mm); break;
|
||
case 'I': edit_node($mm, true); break;
|
||
|
||
case 'j': change_active_node($mm, 0,1); break;
|
||
case 'J': move_active_node_down($mm); break;
|
||
|
||
case 'k': change_active_node($mm, 0,-1); break;
|
||
case 'K': move_active_node_up($mm); break;
|
||
|
||
case 'l': change_active_node($mm, 1,0); 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_new_node($mm, insert_sibling); break;
|
||
case 'O': insert_new_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 'v': collapse_all($mm); break;
|
||
|
||
case 'w': change_max_node_width($mm, width_change_factor); break;
|
||
case 'W': change_max_node_width($mm, 1/width_change_factor); break;
|
||
|
||
case 'x': export_html($mm); break;
|
||
|
||
case 'y': yank_node($mm); break;
|
||
case 'Y': yank_node($mm, true); break;
|
||
|
||
case 'Z': change_line_spacing($mm, +1); break;
|
||
case 'z': change_line_spacing($mm, -1); break;
|
||
|
||
case arr_down: change_active_node($mm, 0,+1); break;
|
||
case arr_right: change_active_node($mm, +1, 0); break;
|
||
case arr_up: change_active_node($mm, 0,-1); break;
|
||
case arr_left: change_active_node($mm, -1, 0); 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_node($mm); break;
|
||
|
||
case '/': search($mm); break;
|
||
case '?': search($mm); break;
|
||
case ctrl_f: search($mm); break;
|
||
|
||
case "\n": insert_new_node($mm, insert_sibling); break;
|
||
case "\t": insert_new_node($mm, insert_child); break;
|
||
}
|
||
|
||
// uncomment the following to discover the escape codes:
|
||
// mecho "\033[1;1f";
|
||
// for ($i=0; $i<strlen($in); $i++)
|
||
// echo base_convert(ord($in[$i]),10,8).' ';
|
||
// echo ' ';
|
||
}
|
||
}
|
||
|
||
// }}}
|
||
// {{{ main
|
||
|
||
check_required_extensions();
|
||
check_the_available_clipboard_tool($mm);
|
||
|
||
enable_alternate_screen();
|
||
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);
|
||
|
||
// }}}
|
||
|