h-m-m/h-m-m

3685 lines
71 KiB
Plaintext
Raw Normal View History

2022-08-30 17:54:49 +02:00
#!/usr/bin/env php
<?php
/**
* h-m-m
*
2023-02-05 08:23:29 +01:00
* 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
*/
2022-08-30 17:54:49 +02:00
2022-09-18 13:27:37 +02:00
// {{{ settings and defaults
2022-09-18 13:27:37 +02:00
mb_internal_encoding("UTF-8");
2022-09-18 13:27:37 +02:00
2022-09-16 02:50:35 +02:00
// $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=[];
2022-08-30 17:54:49 +02:00
// parsing the command line arguments
2022-08-30 17:54:49 +02:00
$mm['arguments'] = [];
2023-10-20 09:57:33 +02:00
$mm['config'] = [];
$mm['env'] = [];
$alen = count($argv);
2022-08-30 17:54:49 +02:00
for ( $i = 1 ; $i < $alen ; $i++ )
if (substr($argv[$i],0,2) == '--')
2022-09-04 19:05:06 +02:00
{
$a = explode('=', substr($argv[$i],2));
$mm['arguments'][ str_replace('-','_',$a[0]) ] = trim( $a[1] ?? true, '"' );
}
elseif (isset($mm['arguments']['filename']))
{
2022-09-20 20:49:23 +02:00
echo "Invalid arguments: more than one filename!".PHP_EOL;
exit(1);
}
else
$mm['arguments']['filename'] = $argv[$i];
2022-08-30 17:54:49 +02:00
// parsing the config file
2022-09-16 19:33:55 +02:00
$conf =
$mm['arguments']['config']
??
(
PHP_OS_FAMILY === 'Windows'
? $argv[0].'.conf'
:
(
PHP_OS_FAMILY === 'Darwin'
2022-09-19 20:53:38 +02:00
? getenv('HOME').'/Library/Preferences/h-m-m/h-m-m.conf'
: getenv('HOME').'/.config/h-m-m/h-m-m.conf'
)
)
;
2022-08-30 17:54:49 +02:00
if (file_exists($conf))
{
$handle = fopen($conf, "r");
if ($handle)
{
while (($line = fgets($handle)) !== false)
{
if (empty(trim($line))) continue;
$c = explode('=',trim($line));
2022-09-19 20:53:38 +02:00
$mm['config'][ str_replace('-','_',trim($c[0] ?? 'NA')) ] = trim($c[1] ?? 'NA');
2022-09-04 19:05:06 +02:00
}
fclose($handle);
2022-09-04 19:05:06 +02:00
}
2022-08-30 17:54:49 +02:00
}
elseif (isset($mm['arguments']['config']))
die("ERROR: the custom config file ($conf) doesn't exit!\n");
2022-08-30 17:54:49 +02:00
// parsing the environment variables
$mm['env']=[];
$e = getenv();
foreach ($e as $k=>$v)
if (substr($k,0,4)=='hmm_')
2022-09-19 20:53:38 +02:00
$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);
2023-03-15 09:09:17 +01:00
config($mm, 'align_levels', 0);
2022-08-30 17:54:49 +02:00
2023-03-15 09:24:48 +01:00
config($mm, 'symbol1', "✓");
config($mm, 'symbol2', "✗");
2023-03-17 20:04:49 +01:00
config($mm, 'show_hidden', 0);
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");
2022-09-29 10:14:11 +02:00
config($mm, 'post_export_command', "");
config($mm, 'clipboard', "os");
config($mm, 'clipboard_file', "/tmp/h-m-m");
config($mm, 'clipboard_in_command', "");
config($mm, 'clipboard_out_command', "");
2023-07-23 11:22:48 +02:00
config($mm, 'auto_save', false);
2023-07-25 09:09:07 +02:00
config($mm, 'echo_keys', false);
2023-07-23 11:22:48 +02:00
2022-11-25 13:38:07 +01:00
if (isset($mm['arguments']['debug_config']))
{
echo "1. the config arguments:\n";
print_r($mm['arguments']);
echo "2. the environment variables:\n";
print_r($mm['env']);
echo "3. the config file:\n";
print_r($mm['config']);
exit;
}
$clipboard = '';
$mm['show_logo'] = true;
$mm['changes'] = [];
$mm['change_active_node'] = [];
$mm['change_index'] = 0;
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
$mm['top_left_column'] = 0;
$mm['top_left_row'] = 0;
2022-08-30 17:54:49 +02:00
$mm['terminal_width'] = (int)exec('tput cols');
$mm['terminal_height'] = (int)exec('tput lines');
2022-08-30 17:54:49 +02:00
$mm['viewport_left'] = 0;
$mm['viewport_top'] = 0;
2022-08-30 17:54:49 +02:00
$mm['root_id'] = 2;
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
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;
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
const conn_left_len = 6;
const conn_right_len = 4;
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
$mm['conn_right'] = str_repeat('─', conn_right_len - 2 );
2023-03-17 20:04:49 +01:00
$mm['conn_left'] = str_repeat('─', conn_left_len -2 );
$mm['conn_single'] = str_repeat('─', conn_left_len + conn_right_len - 3 );
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
const vertical_offset = 4;
2022-08-30 17:54:49 +02:00
const BOM = "\xEF\xBB\xBF";
const default_color = "\033[0m";
const clear_screen = "\033[2J";
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
const special_keys =
[
'ctrl_a' => "\001",
'ctrl_b' => "\002",
'ctrl_c' => "\003",
'ctrl_d' => "\004",
'ctrl_e' => "\005",
'ctrl_f' => "\006",
'ctrl_g' => "\007",
2023-07-23 11:09:17 +02:00
'ctrl_back_space' => "\010",
2023-07-23 10:45:10 +02:00
'ctrl_h' => "\010",
'ctrl_i' => "\011",
'ctrl_j' => "\012",
'ctrl_k' => "\013",
'ctrl_l' => "\014",
'ctrl_m' => "\015",
'ctrl_n' => "\016",
'ctrl_o' => "\017",
'ctrl_p' => "\020",
'ctrl_q' => "\021",
'ctrl_r' => "\022",
'ctrl_s' => "\023",
'ctrl_t' => "\024",
'ctrl_u' => "\025",
'ctrl_v' => "\026",
'ctrl_w' => "\027",
'ctrl_x' => "\030",
'ctrl_y' => "\031",
'ctrl_z' => "\032",
'alt_a' => "\033\141",
'alt_b' => "\033\142",
'alt_c' => "\033\143",
'alt_d' => "\033\144",
'alt_e' => "\033\145",
'alt_f' => "\033\146",
'alt_g' => "\033\147",
'alt_h' => "\033\150",
'alt_i' => "\033\151",
'alt_j' => "\033\152",
'alt_k' => "\033\153",
'alt_l' => "\033\154",
'alt_m' => "\033\155",
'alt_n' => "\033\156",
'alt_o' => "\033\157",
'alt_p' => "\033\160",
'alt_q' => "\033\161",
'alt_r' => "\033\162",
'alt_s' => "\033\163",
'alt_t' => "\033\164",
'alt_u' => "\033\165",
'alt_v' => "\033\166",
'alt_w' => "\033\167",
'alt_x' => "\033\170",
'alt_y' => "\033\171",
'alt_z' => "\033\172",
'arr_down' => "\033\133\102",
'arr_right'=> "\033\133\103",
'arr_up' => "\033\133\101",
'arr_left' => "\033\133\104",
'ctrl_arr_left' => "\033\133\061\073\065\104",
'shift_arr_left' => "\033\133\061\073\062\104",
'ctrl_arr_right' => "\033\133\061\073\065\103",
'shift_arr_right' => "\033\133\061\073\062\103",
'home' => "\033\133\110",
'end' => "\033\133\106",
'home_alternative' => "\033\133\061\176",
'end_alternative' => "\033\133\064\176",
2023-07-23 10:45:10 +02:00
'del' => "\033\133\063\176",
'ctrl_del' => "\033\133\63\073\065\176",
'back_space' => "\177",
'enter' => "\012",
'space' => "\040",
'tab' => "\011",
'esc' => "\033",
'equal' => "="
];
2022-09-16 21:03:12 +02:00
2022-09-18 13:39:49 +02:00
const reset_page = "\033[2J\033[0;0f";
const reset_color = "\033[0m";
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
const invert_on = "\033[7m";
const invert_off = "\033[27m";
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
const dim_on = "\033[2m";
const dim_off = "\033[22m";
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
const line_on = "\033[0m\033[38;5;95m";
const line_off = "\033[0m";
2022-08-30 17:54:49 +02:00
2022-09-18 13:39:49 +02:00
const collapsed_symbol_on = "\033[38;5;215m";
const collapsed_symbol_off = "\033[0m";
2022-08-30 17:54:49 +02:00
const insert_sibling = 0;
const insert_child = 1;
2023-07-23 10:45:10 +02:00
$keybindings = [];
$keybindings['a'] = 'edit_node_append';
$keybindings['A'] = 'edit_node_replace';
$keybindings['b'] = 'expand_all';
$keybindings['c'] = 'center_active_node';
$keybindings['C'] = 'toggle_center_lock';
$keybindings[special_keys['ctrl_c']] = 'quit';
$keybindings['d'] = 'delete_node';
$keybindings['D'] = 'delete_children';
$keybindings[special_keys['del']] = 'delete_node_without_clipboard';
$keybindings['e'] = 'edit_node_append';
$keybindings['E'] = 'edit_node_replace';
$keybindings['f'] = 'focus';
$keybindings['F'] = 'toggle_focus_lock';
$keybindings['g'] = 'go_to_top';
$keybindings['G'] = 'go_to_bottom';
$keybindings['h'] = 'go_left';
$keybindings['H'] = 'toggle_hide';
$keybindings[special_keys['ctrl_h']] = 'toggle_show_hidden';
$keybindings['i'] = 'edit_node_append';
$keybindings['I'] = 'edit_node_replace';
$keybindings['j'] = 'go_down';
$keybindings['J'] = 'move_node_down';
$keybindings['k'] = 'go_up';
$keybindings['K'] = 'move_node_up';
$keybindings['l'] = 'go_right';
$keybindings['m'] = 'go_to_root';
$keybindings['~'] = 'go_to_root';
$keybindings['n'] = 'next_search_result';
$keybindings['N'] = 'previous_search_result';
$keybindings['o'] = 'insert_new_sibling';
$keybindings['O'] = 'insert_new_child';
$keybindings[special_keys['ctrl_o']] = 'open_link';
$keybindings['p'] = 'paste_as_children';
$keybindings['P'] = 'paste_as_siblings';
$keybindings[special_keys['ctrl_p']] = 'append';
$keybindings['q'] = 'quit';
$keybindings['Q'] = 'shutdown';
2024-03-30 19:06:41 +01:00
$keybindings[special_keys['ctrl_q']] = 'quit_with_debug';
2023-07-23 10:45:10 +02:00
$keybindings['r'] = 'collapse_other_branches';
$keybindings['R'] = 'collapse_inner';
$keybindings['s'] = 'save';
$keybindings['S'] = 'save_as';
$keybindings['t'] = 'toggle_symbol';
$keybindings['T'] = 'sort_siblings';
$keybindings['#'] = 'toggle_numbers';
$keybindings['u'] = 'undo';
$keybindings['v'] = 'collapse_all';
$keybindings['V'] = 'collapse_children';
$keybindings['w'] = 'increase_text_width';
$keybindings['W'] = 'decrease_text_width';
$keybindings['x'] = 'export_html';
$keybindings['X'] = 'export_text';
$keybindings['y'] = 'yank_node';
$keybindings['Y'] = 'yank_children';
$keybindings['z'] = 'decrease_line_spacing';
$keybindings['Z'] = 'increase_line_spacing';
$keybindings["\n"] = 'insert_new_sibling';
$keybindings["\t"] = 'insert_new_child';
$keybindings[" "] = 'toggle_node';
$keybindings[special_keys['arr_down']] = 'go_down';
$keybindings[special_keys['arr_up']] = 'go_up';
$keybindings[special_keys['arr_right']] = 'go_right';
$keybindings[special_keys['arr_left']] = 'go_left';
$keybindings['1'] = 'collapse_level_1';
$keybindings['2'] = 'collapse_level_2';
$keybindings['3'] = 'collapse_level_3';
$keybindings['4'] = 'collapse_level_4';
$keybindings['5'] = 'collapse_level_5';
$keybindings['6'] = 'collapse_level_6';
$keybindings['7'] = 'collapse_level_7';
$keybindings['8'] = 'collapse_level_8';
$keybindings['9'] = 'collapse_level_9';
$keybindings['|'] = 'toggle_align';
2023-07-23 11:09:17 +02:00
$keybindings['?'] = 'help';
2023-07-23 10:45:10 +02:00
$keybindings['/'] = 'search';
$keybindings[special_keys['ctrl_f']] = 'search';
$keybindings['='] = 'increase_positive_rank';
$keybindings['+'] = 'decrease_positive_rank';
$keybindings['-'] = 'increase_negative_rank';
$keybindings['_'] = 'decrease_negative_rank';
foreach ($mm['config'] as $key=>$value)
if (substr($key,0,4) == 'bind')
{
$key = trim(substr($key,4));
if (isset(special_keys[$key]))
$key = special_keys[$key];
if (!is_callable($value))
{
echo 'Config error! "'.$value.'" is an unknown command.'."\n";
exit;
}
$keybindings[$key] = trim($value);
}
function config(&$mm, $key, $default)
{
$mm[$key] =
2022-11-25 14:03:22 +01:00
str_replace
(
'\033'
,"\033"
,
$mm['arguments'][$key]
?? $mm['env'][$key]
?? $mm['config'][$key]
?? $default
);
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ checking the requirements
2022-09-14 22:25:16 +02:00
function check_required_extensions()
2022-09-14 22:25:16 +02:00
{
if (!function_exists('mb_strlen'))
2022-09-14 22:25:16 +02:00
{
2022-09-20 20:49:23 +02:00
echo 'Required extension mbstring is not enabled; please check your php installation!'.PHP_EOL;
exit(1);
2022-09-14 22:25:16 +02:00
}
}
function check_the_available_clipboard_tool(&$mm)
{
if ($mm['clipboard'] == 'file' && !file_exists($mm['clipboard_file']))
file_put_contents($mm['clipboard_file'],'');
if ($mm['clipboard'] != 'os')
return;
if (PHP_OS_FAMILY === "Windows")
{
$mm['os_clipboard']['write'] = "clip";
$mm['os_clipboard']['read'] = 'powershell -sta "add-type -as System.Windows.Forms; [windows.forms.clipboard]::GetText()"';
return;
}
if (PHP_OS_FAMILY === "Darwin")
{
$mm['os_clipboard']['write'] = "pbcopy";
$mm['os_clipboard']['read'] = 'pbpaste';
return;
}
// now, the main OS ;)
2023-12-04 06:43:00 +01:00
exec('command -v xclip xsel wl-copy klipper', $result);
$tool = basename($result[0] ?? '');
if (trim($tool)==='')
{
2023-12-04 06:43:00 +01:00
echo "Can't find your clipboard tool! I expected to find xclip, xsel, wl-clipboard, or Klipper.".PHP_EOL;
exit(1);
}
switch ($tool)
{
case 'xclip':
$mm['os_clipboard']['write'] = 'xclip -selection clipboard';
$mm['os_clipboard']['read'] = 'xclip -out -selection clipboard';
break;
case 'xsel':
$mm['os_clipboard']['write'] = 'xsel --clipboard';
$mm['os_clipboard']['read'] = 'xsel --clipboard';
break;
case 'wl-copy':
$mm['os_clipboard']['write'] = 'wl-copy';
$mm['os_clipboard']['read'] = 'wl-paste';
break;
2023-12-04 06:43:00 +01:00
case 'klipper':
$mm['clipboard'] = 'command';
$mm['clipboard_in_command'] = 'qdbus org.kde.klipper /klipper setClipboardContents %text%';
$mm['clipboard_out_command'] = 'qdbus org.kde.klipper /klipper getClipboardContents';
break;
default:
2022-09-20 20:49:23 +02:00
echo "I can't find your clipboard tool!".PHP_EOL;
exit(1);
}
}
2022-09-14 22:25:16 +02:00
// }}}
// {{{ alternative screen buffer
2022-08-30 17:54:49 +02:00
function enable_alternate_screen()
2022-09-04 19:05:06 +02:00
{
// https://www.ibm.com/docs/en/aix/7.1?topic=s-stty-command
system('stty cbreak -echo -crterase intr undef');
2022-08-30 17:54:49 +02:00
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
;
2022-08-30 17:54:49 +02:00
}
function shutdown()
2022-09-04 19:05:06 +02:00
{
echo
clear_screen
."\033[?1049l" // enabling the default screen buffer
."\033[?25h" // enabling the text cursor
;
system("stty sane");
change_window_title('');
exit;
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ list to map converter
2022-08-30 17:54:49 +02:00
function list_to_map($lines, $root_id, $start_id)
2022-09-04 19:05:06 +02:00
{
// calculating the indentation shift and cleaning up the special characters
2022-09-04 19:05:06 +02:00
$indentation_shift = 9999999;
2022-09-04 19:05:06 +02:00
foreach ($lines as $lid=>$line)
{
$lines[$lid] =
mb_ereg_replace
2022-09-04 19:05:06 +02:00
(
"^( *)•"
,'\1*'
,
mb_ereg_replace
(
"[\000-\010\013-\037\177]|".BOM
,''
,str_replace
(
[ "\t", "\n" ]
,[ " ", " " ]
,$lines[$lid]
)
)
2022-09-04 19:05:06 +02:00
)
;
$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);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ map to list converter
function map_to_list(&$mm, $id, $exclude_parent = false, $base = 0)
{
if (!$exclude_parent)
2022-09-20 20:49:23 +02:00
$output = str_repeat("\t",$base).$mm['nodes'][$id]['title'].PHP_EOL;
else
$output = '';
foreach ($mm['nodes'][$id]['children'] as $cid)
$output .= map_to_list($mm, $cid, false, $base+1-$exclude_parent);
return $output;
}
// }}}
// {{{ load file
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
function load_file(&$mm)
{
$mm['filename'] = $mm['arguments']['filename'] ?? '';
2022-09-04 19:05:06 +02:00
if (isset($mm['arguments']['filename']) && file_exists($mm['filename']))
$lines = file($mm['filename'], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
else
2022-09-04 19:05:06 +02:00
{
load_empty_map($mm);
return;
}
// 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);
2022-09-04 19:05:06 +02:00
if (empty($new_nodes))
{
load_empty_map($mm);
return;
}
2022-09-04 19:05:06 +02:00
2023-02-07 20:12:50 +01:00
change_window_title($mm['filename']);
2022-09-04 19:05:06 +02:00
// 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;
2022-09-04 19:05:06 +02:00
$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;
2022-08-30 17:54:49 +02:00
}
2023-03-17 20:04:49 +01:00
// }}}
// {{{ list visible children
function list_visible_children(&$mm, $id)
{
$mm['nodes'][$id]['visible_children'] = [];
foreach ($mm['nodes'][$id]['children'] as $cid)
if (substr($mm['nodes'][$cid]['title'],0,9) != '[HIDDEN] ')
$mm['nodes'][$id]['visible_children'][] = $cid;
foreach ($mm['nodes'][$id]['children'] as $cid)
list_visible_children($mm, $cid);
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ calculate w, x, lh, and clh
2022-09-04 19:05:06 +02:00
function calculate_x_and_lh(&$mm, $id)
{
$node = $mm['nodes'][$id];
2023-03-15 09:09:17 +01:00
$mm['nodes'][$id]['x'] =
$mm['nodes'][ $node['parent'] ]['x']
2022-09-04 19:05:06 +02:00
+ $mm['nodes'][ $node['parent'] ]['w']
+ conn_left_len
+ conn_right_len
+ 1
+
(
$node['parent']==0
? 1 - conn_right_len - conn_left_len
: 0
)
;
2023-03-17 20:04:49 +01:00
$at_the_end =
$node['is_leaf']
|| ($node['collapsed'] ?? false)
|| count($node['visible_children']) == 0
;
$max_width =
$at_the_end
? $mm['max_leaf_node_width']
: $mm['max_parent_node_width']
;
2022-09-04 19:05:06 +02:00
if ( mb_strlen($node['title']) > width_tolerance * $max_width )
{
2022-09-20 20:49:23 +02:00
$lines = explode(PHP_EOL ,wordwrap($node['title'] ,$max_width, PHP_EOL));
2022-09-04 19:05:06 +02:00
$mm['nodes'][$id]['w'] = 0;
foreach ($lines as $line)
$mm['nodes'][$id]['w'] =
2024-04-04 10:55:44 +02:00
max($mm['nodes'][$id]['w'], trim(mb_strwidth($line)));
2022-09-04 19:05:06 +02:00
$mm['nodes'][$id]['lh'] = count($lines);
}
else
{
2024-04-04 10:55:44 +02:00
$mm['nodes'][$id]['w'] = mb_strwidth(trim($node['title']));
2022-09-04 19:05:06 +02:00
$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;
2023-03-17 20:04:49 +01:00
if ($at_the_end)
2022-09-04 19:05:06 +02:00
$mm['nodes'][$id]['clh'] = $mm['nodes'][$id]['lh'];
2023-03-17 20:04:49 +01:00
foreach ($node['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
{
calculate_x_and_lh($mm, $cid);
$mm['nodes'][$id]['clh'] += $mm['nodes'][$cid]['clh'];
}
2022-08-30 17:54:49 +02:00
}
2023-03-15 09:09:17 +01:00
// }}}
// {{{ calculate aligned x
function calculate_aligned_x(&$mm,$id,$x)
{
$mm['map_width'] =
max
(
$mm['map_width']
, $mm['nodes'][$id]['x']
+ $mm['nodes'][$id]['w']
)
;
$max_width = 0;
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'][$id]['visible_children'] as $cid)
2023-03-15 09:09:17 +01:00
{
$max_width = max( $max_width, $mm['nodes'][$cid]['w'] );
$mm['nodes'][$cid]['x'] = $x;
}
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'][$id]['visible_children'] as $cid)
2023-03-15 09:09:17 +01:00
calculate_aligned_x
(
$mm
,$cid
,$max_width
+$x
+conn_left_len
+conn_right_len
+1
)
;
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ calculate h
2022-09-04 19:05:06 +02:00
function calculate_h(&$mm)
{
$unfinished = true;
while ($unfinished)
{
$unfinished = false;
foreach ($mm['nodes'] as $id=>$node)
2023-04-12 11:09:46 +02:00
if ($node['is_leaf'] || $node['visible_children']==0 || ($node['collapsed'] ?? false))
2022-09-04 19:05:06 +02:00
$mm['nodes'][$id]['h']
2022-09-04 19:05:06 +02:00
= $mm['line_spacing']
+ $mm['nodes'][$id]['lh']
;
else
{
$h = 0;
$unready = false;
2023-03-17 20:04:49 +01:00
foreach ($node['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
if ($mm['nodes'][$cid]['h']>=0)
$h += $mm['nodes'][$cid]['h'];
else
{
$unready = true;
break;
}
if ($unready)
$unfinished = true;
else
$mm['nodes'][$id]['h']
2022-09-04 19:05:06 +02:00
= max
(
2022-09-04 19:05:06 +02:00
$h
, $node['lh']
+ $mm['line_spacing']
2022-09-04 19:05:06 +02:00
)
;
}
}
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ calculate y and yo
2022-09-04 19:05:06 +02:00
function calculate_y(&$mm)
{
$mm['map_top'] = 0;
$mm['map_bottom'] = 0;
$mm['map_height'] = $mm['nodes'][0]['h'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][0]['y'] = 0;
calculate_children_y($mm, 0);
2022-08-30 17:54:49 +02:00
}
2022-09-04 19:05:06 +02:00
function calculate_children_y(&$mm,$pid)
{
2022-09-09 21:35:23 +02:00
$y = $mm['nodes'][$pid]['y'];
2022-09-04 19:05:06 +02:00
2022-09-09 21:35:23 +02:00
$mm['nodes'][$pid]['yo'] =
round(
(
+ $mm['nodes'][$pid]['h']
- $mm['nodes'][$pid]['lh']
)/2
)
;
2022-09-04 19:05:06 +02:00
2022-09-09 21:35:23 +02:00
if (!($mm['nodes'][$pid]['collapsed'] ?? false))
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'][$pid]['visible_children'] as $cid)
2022-09-09 21:35:23 +02:00
{
$mm['nodes'][$cid]['y'] = $y;
2022-09-04 19:05:06 +02:00
2022-09-09 21:35:23 +02:00
$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);
}
2022-08-30 17:54:49 +02:00
}
2022-09-18 13:48:51 +02:00
2022-08-30 17:54:49 +02:00
// }}}
// {{{ calculate top-down height shift
2022-09-04 19:05:06 +02:00
function calculate_height_shift(&$mm, $id, $shift = 0)
{
$mm['nodes'][$id]['yo'] += $shift;
$shift += max(0, floor( (($mm['nodes'][$id]['lh'] - $mm['nodes'][$id]['clh']))/2 - 0.9 ));
2022-08-30 17:54:49 +02:00
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'][$id]['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
if (!$mm['nodes'][$id]['collapsed'])
calculate_height_shift($mm, $cid, $shift);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ draw connections on the map
2022-09-04 19:05:06 +02:00
function draw_connections(&$mm, $id)
{
$node = $mm['nodes'][$id];
2023-03-17 20:04:49 +01:00
$num_visible_children = count($mm['nodes'][$id]['visible_children']);
$num_children = count($mm['nodes'][$id]['children']);
$has_hidden_children = $num_visible_children != $num_children;
2022-09-04 19:05:06 +02:00
// if the node is collapsed
if ( ($node['collapsed'] ?? false) && ($num_children > 0) )
2022-09-04 19:05:06 +02:00
{
if ($num_visible_children == $num_children)
2023-03-17 20:04:49 +01:00
mmput
(
$mm
,$node['x'] + $node['w']+1
,$node['y'] + $node['yo']
,' [+]'
);
else
mmput
(
$mm
,$node['x'] + $node['w']+1
,$node['y'] + $node['yo']
,'─╫─ [+]'
2023-03-17 20:04:49 +01:00
);
return;
}
// the easy part...
if ($num_visible_children==0)
{
if ($num_children>0)
mmput
(
$mm
,$node['x'] + $node['w'] + 1
,round( $node['y'] + $node['yo'] ) + round( ($node['lh'] ) / 2 - 0.6 )
,'─╫─'
);
2022-09-04 19:05:06 +02:00
return;
}
2023-03-17 20:04:49 +01:00
// if there's only one child: the same y coordinate
if ($num_visible_children==1)
2022-09-04 19:05:06 +02:00
{
2023-03-17 20:04:49 +01:00
$child_id = $node['visible_children'][ array_key_first( $node['visible_children'] ) ];
2022-09-04 19:05:06 +02:00
$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 );
2023-03-15 09:09:17 +01:00
$x =
$mm['align_levels']
? $node['x'] + $node['w'] - 2
: $child['x'] - conn_left_len - conn_right_len
;
$line =
2023-03-17 20:04:49 +01:00
(
$has_hidden_children
? '─╫'
: '──'
)
.
(
$mm['align_levels']
? str_repeat('─', $child['x'] - $node['x'] - $node['w'] - 1)
2023-03-17 20:04:49 +01:00
: $mm['conn_single']
)
2023-03-15 09:09:17 +01:00
;
mmput
2022-09-04 19:05:06 +02:00
(
$mm
2023-03-15 09:09:17 +01:00
,$x
2022-09-04 19:05:06 +02:00
,min($y1,$y2)
2023-03-15 09:09:17 +01:00
,$line
2022-09-04 19:05:06 +02:00
);
if (abs(min($y1,$y2)-$y2)>0)
{
for ($yy=min($y1,$y2); $yy<max($y1,$y2); $yy++)
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$child['x'] - 2
,$yy
,'│'
);
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$child['x'] - 2
,$y2
,( $y2 > $y1 ? '╰' : '╭' )
);
mmput
2022-09-04 19:05:06 +02:00
(
$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;
2023-03-17 20:04:49 +01:00
foreach ($node['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
{
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);
2023-03-15 09:09:17 +01:00
$x =
$mm['align_levels']
? $node['x'] + $node['w'] - 2
: $mm['nodes'][$top_child]['x'] - conn_left_len - conn_right_len
;
$line =
2023-03-17 20:04:49 +01:00
(
$has_hidden_children
? '─╫'
: '──'
)
.
(
$mm['align_levels']
? str_repeat('─', $mm['nodes'][$top_child]['x'] - $node['x'] - $node['w'] - 3 )
2023-03-17 20:04:49 +01:00
: $mm['conn_left']
)
2023-03-15 09:09:17 +01:00
;
mmput
2022-09-04 19:05:06 +02:00
(
$mm
2023-03-15 09:09:17 +01:00
,$x
2022-09-04 19:05:06 +02:00
,$middle
2023-03-15 09:09:17 +01:00
,$line
2022-09-04 19:05:06 +02:00
);
for ( $i = $top ; $i < $bottom ; $i++ )
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$mm['nodes'][$top_child]['x'] - conn_right_len
,$i
,'│'
);
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$mm['nodes'][$top_child]['x'] - conn_right_len
,$top
,'╭'
.$mm['conn_right']
);
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$mm['nodes'][$top_child]['x']-conn_right_len
,$bottom
,'╰'
.$mm['conn_right']
);
2023-03-17 20:04:49 +01:00
if ($num_visible_children>2)
foreach ($node['visible_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']
);
2022-09-04 19:05:06 +02:00
$existing_char =
mb_substr
(
$mm['map'][$middle]
,$mm['nodes'][$top_child]['x'] - conn_right_len
,1
);
if ($existing_char=='│')
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$mm['nodes'][$top_child]['x'] - conn_right_len
,$middle
,'┤'
);
if ($existing_char=='╭')
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$mm['nodes'][$top_child]['x'] - conn_right_len
,$middle
,'┬'
);
if ($existing_char=='├')
mmput
2022-09-04 19:05:06 +02:00
(
$mm
,$mm['nodes'][$top_child]['x'] - conn_right_len
,$middle
,'┼'
);
2023-03-17 20:04:49 +01:00
foreach ($node['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
draw_connections($mm, $cid);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ add content to the map
2022-09-04 19:05:06 +02:00
function add_content_to_the_map(&$mm, $id)
{
$node = $mm['nodes'][$id];
2023-03-17 20:04:49 +01:00
$at_the_end =
$node['is_leaf']
|| ($node['collapsed'] ?? false)
|| count($node['visible_children']) == 0
;
$max_width =
$at_the_end
? $mm['max_leaf_node_width']
: $mm['max_parent_node_width']
2022-09-04 19:05:06 +02:00
;
if ( mb_strlen($node['title']) > width_tolerance * $max_width)
$lines =
explode
(
2022-09-20 20:49:23 +02:00
PHP_EOL,
2022-09-04 19:05:06 +02:00
wordwrap
(
2022-09-20 20:49:23 +02:00
$node['title']
,$max_width
,PHP_EOL
2022-09-04 19:05:06 +02:00
)
)
;
else
$lines = [$node['title']];
$num_lines = count($lines);
for ( $i=0 ; $i<$num_lines ; $i++ )
2024-04-04 10:55:44 +02:00
mmputcontent
2022-09-04 19:05:06 +02:00
(
$mm,
$node['x'],
$node['y']+$node['yo']+$i,
2022-09-04 19:05:06 +02:00
$lines[$i].' '
);
if (!($node['collapsed'] ?? false))
2023-03-17 20:04:49 +01:00
foreach ($node['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
add_content_to_the_map($mm,$cid);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ build map
2022-09-04 19:05:06 +02:00
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;
2023-03-17 20:04:49 +01:00
$mm['nodes'][$id]['visible_children'] = $mm['nodes'][$id]['children'];
2022-09-04 19:05:06 +02:00
}
$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
2023-03-17 20:04:49 +01:00
if (!$mm['show_hidden'])
list_visible_children($mm, $mm['root_id']);
calculate_x_and_lh($mm,$mm['root_id']);
2023-03-15 09:09:17 +01:00
if ($mm['align_levels'])
calculate_aligned_x($mm,$mm['root_id'],$mm['nodes'][ $mm['root_id'] ]['w'] + conn_left_len + conn_right_len + 1);
2022-09-04 19:05:06 +02:00
calculate_h($mm);
calculate_y($mm);
calculate_height_shift($mm, $mm['root_id']);
2022-09-04 19:05:06 +02:00
// resetting the map, 2/2
$height = max($mm['map_bottom'],$mm['terminal_height']);
2024-04-04 10:55:44 +02:00
$blank = str_repeat(' ', max($mm['map_width'],$mm['terminal_width']));
2022-09-04 19:05:06 +02:00
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']);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ toggle numbers
2023-07-23 10:45:10 +02:00
function toggle_numbers()
{
2023-07-23 10:45:10 +02:00
global $mm;
if ($mm['active_node'] <= $mm['root_id'])
return;
2023-01-07 12:29:42 +01:00
$padlen = count($mm['nodes'][ $mm['nodes'][$mm['active_node']]['parent'] ]['children']) > 9 ? 2 : 1;
$ordered= false;
$i=1;
foreach ($mm['nodes'][ $mm['nodes'][$mm['active_node']]['parent'] ]['children'] as $cid)
$ordered = $ordered || mb_ereg('^\d+\.',$mm['nodes'][$cid]['title']);
$i=1;
foreach ($mm['nodes'][ $mm['nodes'][$mm['active_node']]['parent'] ]['children'] as $cid)
if ($ordered)
$mm['nodes'][$cid]['title'] = mb_ereg_replace('^\d+\. *','',$mm['nodes'][$cid]['title']);
else
2023-01-07 12:29:42 +01:00
$mm['nodes'][$cid]['title'] = str_pad($i++, $padlen, '0', STR_PAD_LEFT).'. '.$mm['nodes'][$cid]['title'];
2023-07-23 11:22:48 +02:00
push_change($mm);
build_map($mm);
display($mm);
}
// }}}
// {{{ toggle symbol
2023-07-23 10:45:10 +02:00
function toggle_symbol()
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-03-15 09:24:48 +01:00
$len1 = mb_strlen($mm['symbol1'])+1;
$len2 = mb_strlen($mm['symbol2'])+1;
$sym1 = mb_substr($mm['nodes'][ $mm['active_node'] ]['title'],0,$len1);
$sym2 = mb_substr($mm['nodes'][ $mm['active_node'] ]['title'],0,$len2);
if ($sym1==$mm['symbol1'].' ')
$mm['nodes'][ $mm['active_node'] ]['title'] =
$mm['symbol2']
.' '
.mb_substr($mm['nodes'][ $mm['active_node'] ]['title'],$len1);
elseif ($sym2==$mm['symbol2'].' ')
$mm['nodes'][ $mm['active_node'] ]['title'] =
mb_substr($mm['nodes'][ $mm['active_node'] ]['title'],$len2);
else
2023-03-15 09:24:48 +01:00
$mm['nodes'][ $mm['active_node'] ]['title'] =
$mm['symbol1']
.' '
.$mm['nodes'][ $mm['active_node'] ]['title'];
2023-07-23 11:22:48 +02:00
push_change($mm);
build_map($mm);
display($mm);
}
2023-03-17 20:04:49 +01:00
// }}}
// {{{ toggle hide
2023-07-23 10:45:10 +02:00
function toggle_hide()
2023-03-17 20:04:49 +01:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-03-17 20:04:49 +01:00
if ($mm['active_node'] == $mm['root_id'])
return;
push_change($mm);
$is_hidden = false;
if ( substr($mm['nodes'][ $mm['active_node'] ]['title'], 0, 9) == '[HIDDEN] ')
$mm['nodes'][ $mm['active_node'] ]['title'] = mb_substr( $mm['nodes'][ $mm['active_node'] ]['title'], 9 );
else
{
$mm['nodes'][ $mm['active_node'] ]['title'] = '[HIDDEN] ' . $mm['nodes'][ $mm['active_node'] ]['title'];
$is_hidden = true;
move_active_node_to_sibling_or_parent($mm);
2023-03-17 20:04:49 +01:00
}
build_map($mm);
display($mm);
message($mm, 'Hidden attribute is turned '.($is_hidden ? 'on' : 'off').' for the node.');
}
2023-07-23 10:45:10 +02:00
function toggle_show_hidden()
2023-03-17 20:04:49 +01:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-03-17 20:04:49 +01:00
$mm['show_hidden'] = !$mm['show_hidden'];
move_active_node_to_sibling_or_parent($mm);
2023-03-17 20:04:49 +01:00
build_map($mm);
display($mm);
message($mm, 'Hidden nodes will '.($mm['show_hidden'] ? '' : 'not ').'be shown.');
}
function move_active_node_to_sibling_or_parent(&$mm)
{
if ($mm['show_hidden'] || substr($mm['nodes'][ $mm['active_node'] ]['title'],0,9)!='[HIDDEN] ' )
return;
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
if (count($mm['nodes'][$parent_id]['visible_children'])<=1)
$mm['active_node'] = $parent_id;
else
{
$passed = false;
// getting the next sibling
foreach ($mm['nodes'][$parent_id]['children'] as $cid)
if ($cid==$mm['active_node'])
$passed = true;
elseif ($passed && substr($mm['nodes'][$cid]['title'],0,9)!='[HIDDEN] ')
{
$mm['active_node'] = $cid;
return;
}
// so, there's no item after it!
2023-03-18 08:34:00 +01:00
$mm['active_node'] = $parent_id;
}
}
2023-03-17 20:04:49 +01:00
2023-03-15 09:09:17 +01:00
// }}}
// {{{ toggle align
2023-07-23 10:45:10 +02:00
function toggle_align()
2023-03-15 09:09:17 +01:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-03-15 09:09:17 +01:00
$mm['align_levels'] = !$mm['align_levels'];
build_map($mm);
display($mm);
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ toggle node
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
function toggle_node()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
if ($mm['nodes'][ $mm['active_node'] ]['is_leaf'])
$mm['active_node'] = $mm['nodes'][ $mm['active_node'] ]['parent'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][ $mm['active_node'] ]['collapsed'] =
!($mm['nodes'][ $mm['active_node'] ]['collapsed'] ?? false);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
display($mm);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ change line spacing
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
function increase_line_spacing()
{
global $mm;
change_line_spacing($mm, +1);
}
function decrease_line_spacing()
{
global $mm;
change_line_spacing($mm, -1);
}
function change_line_spacing(&$mm, $amount)
2022-09-04 19:05:06 +02:00
{
$mm['line_spacing'] = max(0, $mm['line_spacing'] + $amount);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-09-04 19:05:06 +02:00
display($mm);
message($mm,'Spacing: '.$mm['line_spacing']);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ change max node width
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
function increase_text_width()
{
global $mm;
change_max_node_width($mm, width_change_factor);
}
function decrease_text_width()
{
global $mm;
change_max_node_width($mm, 1/width_change_factor);
}
function change_max_node_width(&$mm, $amount)
2022-09-04 19:05:06 +02:00
{
$mx = $mm['terminal_width'] - max_width_padding;
2022-09-04 19:05:06 +02:00
$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 )));
2022-09-04 19:05:06 +02:00
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-09-04 19:05:06 +02:00
display($mm);
message($mm,'Width: '.$mm['max_parent_node_width'].' / '.$mm['max_leaf_node_width']);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ push node down
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
function push_node_down(&$mm, $id)
{
2023-03-17 20:04:49 +01:00
if ($id==$mm['root_id'])
return;
2022-08-30 17:54:49 +02:00
2022-09-16 02:50:35 +02:00
push_change($mm);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if (isset($mm['nodes'][$id+1]))
push_node_down($mm,$id+1);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][$id+1] = $mm['nodes'][$id];
unset($mm['nodes'][$id]);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'] =
array_diff
(
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'],
[$id]
);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'] =
array_push
(
$mm['nodes'][ $mm['nodes'][$id+1]['parent'] ]['children'],
[$id+1]
);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach($mm['nodes'][$id+1]['children'] as $cid=>$cdata)
$mm['nodes'][$cid]['parent'] = $id+1;
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ insert node
2023-07-23 10:45:10 +02:00
function insert_new_child()
{
insert_new_node(insert_child);
}
function insert_new_sibling()
{
insert_new_node(insert_sibling);
}
function insert_new_node($type)
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
if ($mm['active_node']==$mm['root_id'])
2022-09-04 19:05:06 +02:00
$type=insert_child;
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);
2022-10-24 10:49:26 +02:00
push_change($mm);
2022-09-04 19:05:06 +02:00
$mm['nodes'][ $mm['active_node'] ]['title']='';
edit_node($mm);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ magic readline!
2022-08-30 17:54:49 +02:00
function magic_readline(&$mm, $title)
2022-09-04 19:05:06 +02:00
{
$in = '';
2022-09-15 18:37:21 +02:00
$cursor = mb_strlen($title)+1;
2022-09-04 19:05:06 +02:00
$shift = max( 0, $cursor - $mm['terminal_width'] );
show_line($mm, $title, $cursor, $shift);
while(true)
{
usleep(5000);
2022-09-16 20:21:40 +02:00
$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!!!
2022-09-04 19:05:06 +02:00
if ($in != '')
{
2023-07-23 10:45:10 +02:00
if ($in==special_keys['esc'])
2022-09-04 19:05:06 +02:00
{
display($mm);
return false;
2022-09-04 19:05:06 +02:00
}
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['arr_up'] || $in==special_keys['home'])
2022-09-15 18:37:21 +02:00
$cursor = 1;
2022-09-04 19:05:06 +02:00
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['arr_right'])
2022-09-15 18:37:21 +02:00
$cursor = min( mb_strlen($title)+1, $cursor+1);
2022-09-04 19:05:06 +02:00
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['arr_down'] || $in==special_keys['end'])
2022-09-15 18:37:21 +02:00
$cursor = mb_strlen($title)+1;
2022-09-04 19:05:06 +02:00
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['arr_left'])
2022-09-15 18:37:21 +02:00
$cursor = max(1, $cursor-1);
2022-09-04 19:05:06 +02:00
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['ctrl_arr_left'] || $in==special_keys['shift_arr_left'])
2022-09-04 19:05:06 +02:00
$cursor =
$cursor < 3
? 1
: max
(
1,
(
2022-09-15 18:37:21 +02:00
mb_strrpos($title,' ',$cursor-mb_strlen($title)-3) !== false
? mb_strrpos($title,' ',$cursor-mb_strlen($title)-3) + 2
2022-09-04 19:05:06 +02:00
: 1
)
);
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['ctrl_arr_right'] || $in==special_keys['shift_arr_right'])
2022-09-04 19:05:06 +02:00
$cursor =
2022-09-15 18:37:21 +02:00
$cursor > mb_strlen($title) -2
? mb_strlen($title) + 1
2022-09-04 19:05:06 +02:00
: min
(
2022-09-15 18:37:21 +02:00
mb_strlen($title)+1,
2022-09-04 19:05:06 +02:00
(
2022-09-15 18:37:21 +02:00
mb_strpos($title,' ',$cursor+1) !== false
? mb_strpos($title,' ',$cursor+1) + 2
: mb_strlen($title) + 1
2022-09-04 19:05:06 +02:00
)
);
// ctrl+backspace
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['ctrl_back_space'])
2022-09-04 19:05:06 +02:00
{
2022-09-25 08:58:19 +02:00
$from =
max
(
0
,mb_strrpos
(
$title
,' '
,max
(
-mb_strlen($title)
,$cursor-mb_strlen($title)-3
)
)
)
;
$title =
mb_substr($title, 0, $from + ($from>0) )
2022-09-25 09:14:33 +02:00
.mb_substr($title, $cursor-1)
;
2022-09-25 08:58:19 +02:00
$cursor = $from+1+($from>0);
2022-09-04 19:05:06 +02:00
}
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['back_space'])
2022-09-04 19:05:06 +02:00
{
if ($cursor>1)
{
2022-09-15 18:37:21 +02:00
$title =
mb_substr
(
$title
,0
,$cursor-2
)
.mb_substr
(
$title
,$cursor-1
);
2022-09-04 19:05:06 +02:00
$cursor--;
}
}
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['ctrl_del'])
2022-09-04 19:05:06 +02:00
{
2022-09-25 09:14:33 +02:00
$len = mb_strlen($title);
$from =
mb_strpos
(
$title
,' '
,min
(
$cursor+1
,$len
)
)
;
if ($from===false)
$from=$len;
$title =
mb_substr($title, 0, $cursor-1)
.mb_substr($title, $from+1 )
;
2022-09-04 19:05:06 +02:00
}
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['del'])
2022-09-04 19:05:06 +02:00
{
2022-09-15 18:37:21 +02:00
$title =
mb_substr
(
$title
,0
,$cursor-1
)
.mb_substr
(
$title
,$cursor
);
2022-09-04 19:05:06 +02:00
}
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['enter'])
return trim($title);
2022-09-04 19:05:06 +02:00
2023-07-23 10:45:10 +02:00
elseif ($in==special_keys['ctrl_v'])
2022-09-15 17:29:44 +02:00
{
$content =
2022-09-15 17:29:44 +02:00
trim
(
str_replace
2022-09-15 17:29:44 +02:00
(
["\n", "\r", "\t"]
,[" ", "", " " ]
,get_from_clipboard($mm)
2022-09-15 17:29:44 +02:00
)
);
2022-09-15 18:37:21 +02:00
$title =
mb_substr
(
$title
,0
,$cursor-1
)
.$content
.mb_substr
(
$title
,$cursor-1
);
2022-09-15 17:29:44 +02:00
$cursor += mb_strlen($content);
}
2022-09-04 19:05:06 +02:00
// normal characters
2022-09-15 18:37:21 +02:00
else
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
if ($in==special_keys['tab'])
$in=' ';
2022-09-15 18:37:21 +02:00
$title =
mb_substr
(
$title
,0
,$cursor-1
)
.$in
.mb_substr
(
$title
,$cursor-1
)
;
$title = str_replace(["\n","\r","\t"],[" ",""," "],$title);
2022-09-17 00:41:45 +02:00
$title = mb_ereg_replace("[\000-\010\013-\037\177]|".BOM,'',$title);
2022-09-16 20:21:40 +02:00
$cursor += mb_strlen($in);
// the input content can be longer than one character if
// the user uses ctrl+shift+v to paste.
2022-09-04 19:05:06 +02:00
}
// adjusting the position and shift
$shift = max( 0, $shift, $cursor - $mm['terminal_width'] );
$shift = min( $shift, $cursor-1 );
show_line($mm, $title, $cursor, $shift);
}
}
2022-08-30 17:54:49 +02:00
}
function show_line(&$mm, $title, $cursor, $shift)
{
$output = mb_substr($title,$shift,$mm['terminal_width']-1);
2024-04-04 10:55:44 +02:00
$output .= str_repeat( ' ' ,$mm['terminal_width'] - mb_strwidth($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
2023-07-23 10:45:10 +02:00
function edit_node_append()
{
edit_node(false);
}
function edit_node_replace()
{
2023-07-23 10:45:10 +02:00
edit_node(true);
}
function edit_node($rewrite = false)
{
global $mm;
$title = $rewrite ? '' : $mm['nodes'][ $mm['active_node'] ]['title'];
2022-10-24 10:49:26 +02:00
if
(
(
2022-09-25 08:58:19 +02:00
$mm['active_node']==$mm['root_id']
&& $title=='root'
)
or $title=='NEW'
)
2022-10-24 10:49:26 +02:00
$title = '';
$output = magic_readline($mm, $title);
2022-10-24 10:49:26 +02:00
if
(
(
$output === false
or $output === ''
)
&& $mm['nodes'][ $mm['active_node'] ]['title'] === ''
2022-10-24 10:49:26 +02:00
&& $mm['nodes'][ $mm['active_node'] ]['is_leaf']
)
{
2023-07-23 10:45:10 +02:00
delete_node_vh($mm, false, true);
2022-10-24 10:49:26 +02:00
push_change($mm);
build_map($mm);
display($mm);
return;
}
if ($output === false)
{
display($mm);
message($mm, 'Editing cancelled');
return;
}
$mm['nodes'][ $mm['active_node'] ]['title'] = $output;
push_change($mm);
build_map($mm);
display($mm);
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ center active node
2023-07-23 10:45:10 +02:00
function center_active_node()
{
global $mm;
center_active_node_vh(false);
display($mm);
}
function toggle_center_lock()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
$mm['center_lock'] = !$mm['center_lock'];
display($mm);
}
function center_active_node_vh($only_vertically = false)
{
global $mm;
2022-09-04 19:05:06 +02:00
$node = $mm['nodes'][ $mm['active_node'] ];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$midx = $node['w']/2 + $node['x'];
$midy = $node['lh']/2 + $node['y'] + $node['yo'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if (!$only_vertically)
$mm['viewport_left'] = max(0, round( $midx - $mm['terminal_width']/2 ) );
2022-08-30 17:54:49 +02:00
$mm['viewport_top'] = round( $midy - $mm['terminal_height']/2 );
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ goto's
2023-07-23 10:45:10 +02:00
function go_to_root()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
$mm['active_node']=$mm['root_id'];
2022-09-04 19:05:06 +02:00
display($mm, true);
2022-08-30 17:54:49 +02:00
}
2023-07-23 10:45:10 +02:00
function go_up()
{
global $mm;
change_active_node($mm,0,-1);
}
function go_down()
{
global $mm;
change_active_node($mm,0,1);
}
function go_left()
{
global $mm;
change_active_node($mm,-1,0);
}
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
function go_to_top()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
$yid = 0;
$y = $mm['map_height'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach ($mm['nodes'] as $id=>$node)
if ($node['y']>=0 && $node['y']+$node['yo'] < $y)
{
$y = $node['y']+$node['yo'];
$yid = $id;
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['active_node'] = $yid;
display($mm, true);
2022-08-30 17:54:49 +02:00
}
2023-07-23 10:45:10 +02:00
function go_right()
{
global $mm;
change_active_node($mm,1,0);
}
function go_to_bottom()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
$yid = 0;
$y = 0;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach ($mm['nodes'] as $id=>$node)
if ($node['y']>=0 && $node['y']+$node['yo'] > $y)
{
$y = $node['y']+$node['yo']+$node['lh'];
$yid = $id;
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['active_node'] = $yid;
display($mm, true);
2022-08-30 17:54:49 +02:00
}
2023-07-23 11:09:17 +02:00
// }}}
// {{{ help
function help()
{
global $mm;
global $keybindings;
$keynames = array_flip(special_keys);
$commands = [];
foreach ($keybindings as $key => $command)
$commands[$command][] = ( $keynames[$key] ?? $key );
$output = [];
foreach ($commands as $command => $keys)
$output[] =
str_pad($command.' ',32,'.',STR_PAD_RIGHT)
.' '
.implode(', ',$keys);
sort($output);
$breakpoint = floor((count($output)-1)/2);
for ($i=0 ; $i<=$breakpoint ; $i++)
echo
' '
.str_pad($output[$i],56,' ',STR_PAD_RIGHT)
.
($output[$i+$breakpoint+1] ?? '')
."\n"
;
echo "\n";
message($mm, "Press any key to exit this help screen.");
while (true)
{
usleep(20000);
$in = fread(STDIN, 16);
if ($in != '')
break;
}
display($mm);
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ search
2023-07-23 10:45:10 +02:00
function search()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
$mm['query'] = magic_readline($mm,'');
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if (empty($mm['query']))
{
display($mm);
return;
}
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
if (!next_search_result())
previous_search_result();
2022-08-30 17:54:49 +02:00
}
2022-09-18 13:48:51 +02:00
2023-07-23 10:45:10 +02:00
function previous_search_result()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
$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 &&
2023-03-17 20:04:49 +01:00
(
$mm['show_hidden'] ||
substr($node['title'],0,9) != '[HIDDEN] '
) &&
2022-09-04 19:05:06 +02:00
$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)
2022-09-16 14:47:47 +02:00
{
display($mm);
return false;
2022-09-16 14:47:47 +02:00
}
2022-09-04 19:05:06 +02:00
$mm['active_node'] = $nid;
display($mm);
2022-08-30 17:54:49 +02:00
}
2022-09-18 13:48:51 +02:00
2023-07-23 10:45:10 +02:00
function next_search_result()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
$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 &&
2023-03-17 20:04:49 +01:00
(
$mm['show_hidden'] ||
substr($node['title'],0,9) != '[HIDDEN] '
) &&
2022-09-04 19:05:06 +02:00
$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;
2022-09-04 19:05:06 +02:00
$mm['active_node'] = $nid;
display($mm);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ move active node
2023-07-23 10:45:10 +02:00
function move_node_down()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
if ($mm['active_node']==0) return;
2022-08-30 17:54:49 +02:00
2022-09-16 02:50:35 +02:00
push_change($mm);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
$children = [];
$just_passed_active = false;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach ($mm['nodes'][ $parent_id ]['children'] as $child)
{
if ($child!=$mm['active_node'])
$children[] = $child;
2022-08-30 17:54:49 +02:00
if ($just_passed_active && ($mm['show_hidden'] || substr($mm['nodes'][$child]['title'],0,9)!='[HIDDEN] ' ) )
2022-09-04 19:05:06 +02:00
{
$children[] = $mm['active_node'];
$just_passed_active = false;
2022-09-04 19:05:06 +02:00
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($child==$mm['active_node'])
$just_passed_active = true;
2022-09-04 19:05:06 +02:00
}
2022-08-30 17:54:49 +02:00
if ($just_passed_active)
2022-09-04 19:05:06 +02:00
$children[] = $mm['active_node'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][ $parent_id ]['children'] = $children;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
display($mm);
2022-08-30 17:54:49 +02:00
}
2023-07-23 10:45:10 +02:00
function move_node_up()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
if ($mm['active_node']==0) return;
2022-08-30 17:54:49 +02:00
2022-09-16 02:50:35 +02:00
push_change($mm);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
$children = [];
$just_passed_active = false;
2022-09-04 19:05:06 +02:00
$rev_children = array_reverse($mm['nodes'][$parent_id]['children']);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach ($rev_children as $child)
{
if ($child!=$mm['active_node'])
$children[] = $child;
2022-08-30 17:54:49 +02:00
if ($just_passed_active && ($mm['show_hidden'] || substr($mm['nodes'][$child]['title'],0,9)!='[HIDDEN] ' ) )
2022-09-04 19:05:06 +02:00
{
$children[] = $mm['active_node'];
$just_passed_active = false;
2022-09-04 19:05:06 +02:00
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($child==$mm['active_node'])
$just_passed_active = true;
2022-09-04 19:05:06 +02:00
}
2022-08-30 17:54:49 +02:00
if ($just_passed_active)
2022-09-04 19:05:06 +02:00
$children[] = $mm['active_node'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][ $parent_id ]['children'] = array_reverse($children);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
display($mm);
2022-08-30 17:54:49 +02:00
}
2022-09-07 15:11:13 +02:00
// }}}
// {{{ export html
2023-07-23 10:45:10 +02:00
function export_html()
2022-09-07 15:11:13 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-07 15:11:13 +02:00
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>'
2022-09-09 21:35:23 +02:00
.'<html lang=en>'
2022-09-07 15:11:13 +02:00
.'<head>'
2022-09-09 21:35:23 +02:00
.'<title>'
.$mm['nodes'][ $mm['root_id'] ]['title']
2022-09-09 21:35:23 +02:00
.'</title>'
.'<meta charset="UTF-8">'
2022-09-18 13:39:49 +02:00
.'<meta name=viewport content="width=device-width,initial-scale=1,user-scalable=yes">'
2022-09-07 15:11:13 +02:00
.'<style>'
2022-09-07 15:15:02 +02:00
.'body { background-color: #222; color: #ddd; font-family: monospace; padding: 0; font-size: 16px; }'
2022-09-07 15:11:13 +02:00
.'#root {margin:10px 0}'
.'p:before { content: "━ "; }'
2022-09-09 21:35:23 +02:00
.'p, summary { padding: 8px; margin: 0; font-size: 16px; }'
.'details, p { padding-left: 29px; border-left: 3px solid #444; font-size:16px; }'
2022-09-07 15:11:13 +02:00
.'summary { margin-left: -10px; cursor: pointer; }'
2022-09-07 15:15:02 +02:00
.'summary:hover, p:hover { color: #fbc531; }'
2022-09-07 15:11:13 +02:00
.'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; }'
2022-09-09 21:35:23 +02:00
.'#source[open] { position: static; margin: 100px 0 0 0; font-size: 16px; }'
2022-09-07 15:11:13 +02:00
.'#map { margin: 40px 30px; }'
.'</style>'
.'</head>'
.'<body>'
.'<div id=map>'
.export_html_node($mm, $mm['root_id'])
2022-09-07 15:11:13 +02:00
.'</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'])
2022-09-07 15:11:13 +02:00
.'</pre>'
.'</details>'
.'</body>'
.'</html>'
);
fclose($file);
message($mm, 'Exported as '.$mm['filename'].'.html');
copy_to_clipboard($mm, $mm['filename'].'.html');
2022-09-29 10:14:11 +02:00
$filename = getenv('PWD').'/'.basename($mm['filename']).'.html';
if ($mm['post_export_command']!='')
{
message($mm, 'Running the "'.$mm['post_export_command'].'" command');
exec(str_replace('%filename%', $filename, $mm['post_export_command']), $output, $result);
display($mm);
message($mm, 'Post-export command ended ('.$result.')');
}
2022-09-07 15:11:13 +02:00
}
function export_html_node(&$mm, $parent_id)
{
2023-03-17 20:04:49 +01:00
if ($mm['nodes'][$parent_id]['visible_children']==[])
2022-09-07 15:11:13 +02:00
{
$output =
"<p>"
.$mm['nodes'][$parent_id]['title']
."</p>";
}
elseif ($parent_id==$mm['root_id'])
2022-09-07 15:11:13 +02:00
{
$output =
2022-09-07 15:11:13 +02:00
"<div id=root>"
.$mm['nodes'][$parent_id]['title']
."</div>";
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'][$parent_id]['visible_children'] as $cid)
2022-09-07 15:11:13 +02:00
$output .= export_html_node($mm, $cid);
}
else
{
$output =
"<details>"
."<summary>"
.$mm['nodes'][$parent_id]['title']
2022-09-07 15:11:13 +02:00
."</summary>";
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'][$parent_id]['visible_children'] as $cid)
2022-09-07 15:11:13 +02:00
$output .= export_html_node($mm, $cid);
$output .=
"</details>";
}
return $output;
}
2023-02-05 08:23:29 +01:00
// }}}
// {{{ export text
2023-07-23 10:45:10 +02:00
function export_text()
2023-02-05 08:23:29 +01:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-03-18 14:38:26 +01:00
$output = '';
foreach ($mm['map'] as $line)
if (rtrim($line) != '')
$output .= substr(rtrim($line),3)."\n";
2023-02-05 08:23:29 +01:00
2023-03-18 14:38:26 +01:00
copy_to_clipboard($mm, $output);
message($mm, 'Exported the map to clipboard.');
2023-02-05 08:23:29 +01:00
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ save
2023-07-23 10:45:10 +02:00
function save()
{
global $mm;
2023-07-23 11:22:48 +02:00
if ($mm['auto_save'])
message($mm,'auto_save is enabled and you don\'t need to save manually.');
else
save_vh($mm, false);
2023-07-23 10:45:10 +02:00
}
function save_as()
{
global $mm;
save_vh($mm, true);
}
function save_vh(&$mm, $new_name = false)
2022-09-04 19:05:06 +02:00
{
if ($new_name || empty($mm['filename']))
2022-09-04 19:05:06 +02:00
{
2022-09-20 11:38:06 +02:00
$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']);
2023-02-07 20:16:39 +01:00
$new_name = magic_readline($mm, (empty($mm['filename']) ? $path : $mm['filename'] ) );
2022-08-30 17:54:49 +02:00
if ($new_name === false)
2022-09-04 19:05:06 +02:00
{
display($mm);
message($mm, 'Saving cancelled');
return;
}
2022-08-30 17:54:49 +02:00
$mm['filename'] = $new_name;
2022-09-04 19:05:06 +02:00
$ext = mb_substr( $mm['filename'], mb_strrpos($mm['filename'],'.') + 1);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($ext!='hmm')
$mm['filename'] .= '.hmm';
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
display($mm);
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
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;
}
2022-08-30 17:54:49 +02:00
2023-02-07 20:12:50 +01:00
change_window_title($mm['filename']);
2022-09-04 19:05:06 +02:00
$file = fopen($mm['filename'], "w");
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['modified'] = false;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($file===false)
{
message($mm, 'ERROR! Could not save the file');
$mm['modified'] = true;
return;
}
2022-08-30 17:54:49 +02:00
fwrite($file, map_to_list($mm, $mm['root_id']) . "\n");
2022-09-04 19:05:06 +02:00
fclose($file);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
display($mm);
2023-07-23 11:22:48 +02:00
if (!$mm['auto_save'])
message($mm, 'Saved '.$mm['filename']);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ message
2022-09-04 19:05:06 +02:00
function message(&$mm, $text)
{
2022-09-23 11:11:16 +02:00
$mm['terminal_width'] = (int)exec('tput cols');
$mm['terminal_height'] = (int)exec('tput lines');
echo
"\033["
.$mm['terminal_height'] // y
.";"
2022-09-23 11:11:16 +02:00
.max(0,$mm['terminal_width'] - mb_strlen($text) - 1) //x
."f"
.$mm['message_color']
.' '
.$text
.' '
.reset_color
;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
usleep(200000);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ quit
2023-07-23 10:45:10 +02:00
function quit()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-07-23 11:22:48 +02:00
if (($mm['modified'] ?? false) === false)
shutdown();
2022-09-04 19:05:06 +02:00
message($mm, "You have unsaved changes. Save them, or use shift+Q to quit without saving.");
2022-08-30 17:54:49 +02:00
}
2024-03-30 19:06:41 +01:00
function quit_with_debug()
{
global $mm;
$file = fopen('./h-m-m--debug.txt', "w");
if ($file!==false)
{
fwrite($file, serialize($mm));
fclose($file);
echo "Debug information is written to h-m-m--debug.txt file.";
}
shutdown();
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ move_window
2022-09-04 19:05:06 +02:00
function move_window(&$mm)
{
$node = $mm['nodes'][ $mm['active_node'] ];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$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;
2022-08-30 17:54:49 +02:00
$mm['viewport_left'] = min( $mm['viewport_left'], $x1);
$mm['viewport_left'] = max( $mm['viewport_left'], $x2 - $mm['terminal_width']);
2022-08-30 17:54:49 +02:00
$mm['viewport_top'] = min( $mm['viewport_top'], $y1);
$mm['viewport_top'] = max( $mm['viewport_top'], $y2 - $mm['terminal_height']);
2022-08-30 17:54:49 +02:00
}
2022-09-16 02:50:35 +02:00
// }}}
// {{{ changes
function push_change(&$mm)
{
// flush any redo chain
while(count($mm['changes']) > $mm['change_index'])
2022-09-16 19:33:55 +02:00
{
2022-09-16 02:50:35 +02:00
array_pop($mm['changes']);
2022-09-16 19:33:55 +02:00
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'])
2022-09-16 19:33:55 +02:00
{
array_shift($mm['changes']);
array_shift($mm['change_active_node']);
$mm['change_index']--;
}
2022-09-16 02:50:35 +02:00
array_push($mm['changes'], $mm['nodes']);
array_push($mm['change_active_node'], $mm['active_node']);
$mm['change_index']++;
2023-07-23 11:22:48 +02:00
$mm['modified'] = true;
if ($mm['auto_save'])
save_vh($mm);
2022-09-16 02:50:35 +02:00
}
2022-09-18 13:48:51 +02:00
2023-07-23 10:45:10 +02:00
function undo()
2022-09-16 02:50:35 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-16 02:50:35 +02:00
if($mm['change_index'] == 0)
return;
$mm['change_index']--;
2022-09-21 16:20:01 +02:00
$mm['nodes'] = $mm['changes'][$mm['change_index']];
$mm['active_node'] = $mm['change_active_node'][$mm['change_index']];
2022-09-16 02:50:35 +02:00
build_map($mm);
display($mm);
}
2022-09-18 13:48:51 +02:00
2023-07-23 10:45:10 +02:00
function redo()
2022-09-16 02:50:35 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-16 02:50:35 +02:00
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);
}
2022-09-18 13:48:51 +02:00
2022-08-30 17:54:49 +02:00
// }}}
// {{{ change active node
function change_active_node(&$mm, $x, $y)
2022-09-04 19:05:06 +02:00
{
$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)
2022-09-04 19:05:06 +02:00
{
2023-03-18 08:44:43 +01:00
if (count($node['visible_children'])==0)
return;
2022-09-16 12:49:26 +02:00
// auto-unfold node on right move
if ($node['collapsed'] ?? false) {
toggle_node($mm);
2022-09-16 12:49:26 +02:00
$node = $mm['nodes'][ $mm['active_node'] ];
}
2022-09-04 19:05:06 +02:00
$distance = [];
2023-03-17 20:04:49 +01:00
foreach ($node['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
$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)
2022-09-04 19:05:06 +02:00
{
if ($mm['active_node']==$mm['root_id']) return;
2022-09-04 19:05:06 +02:00
$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)
2022-09-04 19:05:06 +02:00
{
2023-03-17 20:04:49 +01:00
$rchildren = array_reverse($mm['nodes'][ $node['parent'] ]['visible_children']);
2022-09-04 19:05:06 +02:00
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)
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'][ $node['parent'] ]['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
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) )
2022-09-04 19:05:06 +02:00
{
$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;
2022-08-30 17:54:49 +02:00
}
2022-09-02 09:16:02 +02:00
// }}}
// {{{ append
2023-07-23 10:45:10 +02:00
function append()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
$mm['nodes'][ $mm['active_node'] ]['title'] .=
' '
.str_replace
(
["\n","\r","\t"]
,[' ','', ' ']
,trim(get_from_clipboard($mm))
)
;
2022-09-04 19:05:06 +02:00
build_map($mm);
display($mm);
2022-09-02 09:16:02 +02:00
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ paste sub-tree
2023-07-23 10:45:10 +02:00
function paste_as_children()
{
global $mm;
paste_sub_tree($mm, false);
}
function paste_as_siblings()
{
global $mm;
paste_sub_tree($mm, true);
}
2022-09-04 19:05:06 +02:00
function paste_sub_tree(&$mm, $as_sibling )
{
if ($as_sibling && $mm['active_node']==$mm['root_id'])
return;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($as_sibling)
$parent_id = $mm['nodes'][ $mm['active_node'] ]['parent'];
else
$parent_id = $mm['active_node'];
2022-08-30 17:54:49 +02:00
2023-03-17 20:04:49 +01:00
$mm['nodes'][ $parent_id ]['collapsed'] = false;
$mm['nodes'][ $parent_id ]['is_leaf'] = false;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$new_id = 1 + max(array_keys($mm['nodes']));
2022-08-30 17:54:49 +02:00
2022-09-20 20:49:23 +02:00
// \n instead of PHP_EOL, just to be sure.
// existing \r's will be removed later.
2022-09-04 19:05:06 +02:00
$st =
list_to_map
2022-09-04 19:05:06 +02:00
(
explode("\n",get_from_clipboard($mm)),
2022-09-04 19:05:06 +02:00
$parent_id,
$new_id
)
;
2022-08-30 17:54:49 +02:00
if ($st==[])
return;
2022-08-30 17:54:49 +02:00
push_change($mm);
$mm['nodes'] += $st;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
// doing it like this, in case the sub-tree has more than
// one top-level element.
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach ($st as $cid=>$cdata)
if ($cdata['parent'] == $parent_id)
$mm['nodes'][ $parent_id ]['children'][] = $cid;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
// rearranging the items only if they are pasted as siblings
// (for pasting as children, it makes sense to have them
// at the end)
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($as_sibling)
{
$sub_roots = [];
foreach ($st as $cid=>$cdata)
if ($cdata['parent']==$parent_id)
$sub_roots[] = $cid;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$children = [];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach ($mm['nodes'][ $parent_id ]['children'] as $child_id)
{
if (!in_array($child_id, $sub_roots))
$children[] = $child_id;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($child_id == $mm['active_node'])
$children = array_merge($children, $sub_roots);
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][ $parent_id ]['children'] = $children;
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['active_node'] = $new_id;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
display($mm);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ clipboard
function copy_to_clipboard(&$mm, $text)
2022-09-04 19:05:06 +02:00
{
switch ($mm['clipboard'])
{
case 'os':
$clip = popen($mm['os_clipboard']['write'],'wb');
2023-12-04 06:43:00 +01:00
if (!isset($clip))
return;
fwrite($clip,$text);
pclose($clip);
break;
case 'internal':
$GLOBALS['clipboard'] = $text;
break;
case 'file':
file_put_contents($mm['clipboard_file'],$text);
break;
case 'command':
shell_exec(str_replace('%text%', '"'.$text.'"', $mm['clipboard_in_command']));
break;
}
2022-08-30 17:54:49 +02:00
}
2022-09-18 13:48:51 +02:00
function get_from_clipboard(&$mm)
2022-09-04 19:05:06 +02:00
{
$text = 'clipboard unavailable!';
switch ($mm['clipboard'])
{
case 'os': $text = shell_exec($mm['os_clipboard']['read']); break;
case 'internal': $text = $GLOBALS['clipboard']; break;
case 'file': $text = file_get_contents($mm['clipboard_file']); break;
case 'command': $text = shell_exec($mm['clipboard_out_command']); break;
default: $text = "The clipboard type you've set is invalid!";
}
return
mb_ereg_replace
(
2022-09-17 00:41:45 +02:00
"[\000-\010\013-\037\177]|".BOM
,''
,$text
)
;
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ load empty map
2022-09-04 19:05:06 +02:00
function load_empty_map(&$mm)
{
2023-02-07 20:12:50 +01:00
change_window_title('new mind map');
2022-09-20 11:38:06 +02:00
if (isset($mm['nodes']))
unset($mm['nodes']);
$mm['nodes'][0] =
[
'title'=>'X'
,'is_leaf'=>false
,'children'=>[1]
,'collapsed'=>false
,'parent'=>-1
]
;
2022-08-30 17:54:49 +02:00
2023-04-20 08:50:00 +02:00
$title = basename( empty($mm['filename']) ? 'root' : $mm['filename'] );
if (substr($title,-4)=='.hmm')
$title = substr($title,0,-4);
2022-09-20 11:38:06 +02:00
$mm['nodes'][1] =
[
'title'=> $title
2022-09-20 11:38:06 +02:00
,'is_leaf'=>true
,'children'=>[]
,'collapsed'=>false
,'parent'=>0
]
;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['active_node']=1;
$mm['root_id']=1;
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ yank
2023-07-23 10:45:10 +02:00
function yank_node()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
copy_to_clipboard($mm, map_to_list($mm, $mm['active_node'], false));
message($mm, 'Item(s) are copied to the clipboard.');
}
function yank_children()
{
global $mm;
copy_to_clipboard($mm, map_to_list($mm, $mm['active_node'], true));
2022-09-04 19:05:06 +02:00
message($mm, 'Item(s) are copied to the clipboard.');
2022-08-30 17:54:49 +02:00
}
2022-09-18 13:48:51 +02:00
2022-08-30 17:54:49 +02:00
// }}}
// {{{ delete
2023-07-23 10:45:10 +02:00
function delete_node()
{
global $mm;
delete_node_vh($mm, false, false);
}
function delete_children()
{
global $mm;
delete_node_vh($mm, true, false);
}
function delete_node_without_clipboard()
{
global $mm;
delete_node_vh($mm, false, true);
}
function delete_node_vh(&$mm, $exclude_parent = false, $dont_copy_to_clipboard = false )
2022-09-04 19:05:06 +02:00
{
if ($mm['active_node']==$mm['root_id'])
2022-09-16 21:03:12 +02:00
$exclude_parent = true;
2022-08-30 17:54:49 +02:00
2022-09-16 21:03:12 +02:00
if (!$dont_copy_to_clipboard)
copy_to_clipboard($mm, map_to_list($mm, $mm['active_node'], $exclude_parent) );
2022-08-30 17:54:49 +02:00
2022-09-16 19:33:55 +02:00
push_change($mm);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
delete_node_internal($mm, $mm['active_node'], $exclude_parent);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
display($mm);
2022-08-30 17:54:49 +02:00
2022-09-16 21:03:12 +02:00
if (!$dont_copy_to_clipboard)
message($mm, 'Item(s) are cut and placed into the clipboard.');
2022-08-30 17:54:49 +02:00
}
2022-09-04 19:05:06 +02:00
function delete_node_internal(&$mm, $active_node, $exclude_parent = false )
{
// taking a shorter approach if it's for the whole tree
2022-08-30 17:54:49 +02:00
if ($active_node==$mm['root_id'] && !$exclude_parent)
2022-09-04 19:05:06 +02:00
{
load_empty_map($mm);
display($mm, true);
return;
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
// if it's for a sub-tree, then...
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
delete_children_vh($mm, $active_node);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($exclude_parent)
{
$mm['nodes'][ $active_node ]['is_leaf'] = true;
$mm['nodes'][ $active_node ]['children'] = [];
}
else
{
$parent_id = $mm['nodes'][ $active_node ]['parent'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$previous_sibling = 0;
$passed = false;
foreach ($mm['nodes'][$parent_id]['visible_children'] as $cid)
2022-09-04 19:05:06 +02:00
if ($cid==$active_node)
{
if ($previous_sibling!=0) break;
$passed = true;
}
else
{
$previous_sibling = $cid;
if ($passed) break;
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][$parent_id]['children'] =
array_diff
(
$mm['nodes'][$parent_id]['children'],
[$active_node]
)
;
2022-08-30 17:54:49 +02:00
$mm['nodes'][$parent_id]['visible_children'] =
array_diff
(
$mm['nodes'][$parent_id]['visible_children'],
[$active_node]
)
;
2022-09-04 19:05:06 +02:00
if (count($mm['nodes'][$parent_id]['children'])==0)
$mm['nodes'][$parent_id]['is_leaf'] = true;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
unset($mm['nodes'][ $active_node ]);
2022-08-30 17:54:49 +02:00
if (count($mm['nodes'][$parent_id]['visible_children'])==0)
2022-09-04 19:05:06 +02:00
$mm['active_node'] = $parent_id;
else
$mm['active_node'] = $previous_sibling;
}
2022-08-30 17:54:49 +02:00
}
2023-07-23 10:45:10 +02:00
function delete_children_vh(&$mm,$id)
2022-09-04 19:05:06 +02:00
{
foreach (($mm['nodes'][$id]['children'] ?? []) as $cid)
{
2023-07-23 10:45:10 +02:00
delete_children_vh($mm, $cid);
2022-09-04 19:05:06 +02:00
unset($mm['nodes'][$cid]);
}
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ focus
2023-07-23 10:45:10 +02:00
function toggle_focus_lock()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
$mm['focus_lock'] = !$mm['focus_lock'];
2022-09-18 13:48:51 +02:00
2022-09-04 19:05:06 +02:00
message($mm, $mm['focus_lock'] ? 'Focus locked' : 'Focus unlocked');
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
display($mm);
2022-08-30 17:54:49 +02:00
}
2022-09-18 13:48:51 +02:00
2023-07-23 10:45:10 +02:00
function focus()
{
global $mm;
focus_vh($mm);
build_map($mm);
display($mm,true);
}
function focus_vh(&$mm)
2022-09-04 19:05:06 +02:00
{
collapse_siblings($mm, $mm['active_node']);
expand_siblings($mm, $mm['active_node']);
2022-08-30 17:54:49 +02:00
}
2022-09-18 13:48:51 +02:00
2023-03-17 20:04:49 +01:00
// }}}
// {{{ collapse and expand
2023-07-23 10:45:10 +02:00
function expand_all()
2023-03-17 20:04:49 +01:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-03-17 20:04:49 +01:00
foreach ($mm['nodes'] as $id=>$node)
$mm['nodes'][$id]['collapsed'] = false;
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2023-03-17 20:04:49 +01:00
display($mm);
}
2022-09-04 19:05:06 +02:00
function collapse_siblings(&$mm, $id)
{
2023-03-18 08:44:43 +01:00
if ($id <= $mm['root_id'])
return;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$parent_id = $mm['nodes'][$id]['parent'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
foreach ($mm['nodes'][$parent_id]['children'] as $cid)
if ($cid!=$id)
$mm['nodes'][$cid]['collapsed'] = true;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
collapse_siblings($mm, $parent_id);
2022-08-30 17:54:49 +02:00
}
2022-09-04 19:05:06 +02:00
function expand_siblings(&$mm, $id)
{
2023-03-18 08:44:43 +01:00
if ($mm['nodes'][$id]['is_leaf'])
return;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$mm['nodes'][$id]['collapsed'] = false;
foreach ($mm['nodes'][$id]['children'] as $cid)
expand_siblings($mm, $cid);
2022-08-30 17:54:49 +02:00
}
2023-07-23 10:45:10 +02:00
function collapse_other_branches()
2022-09-10 01:18:32 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
if ($mm['active_node'] == $mm['root_id'])
2022-09-10 01:18:32 +02:00
return;
$branch = find_branch($mm, $mm['active_node']);
foreach ($mm['nodes'][ $mm['root_id'] ]['children'] as $bid)
2022-09-10 01:18:32 +02:00
if ($bid != $branch)
$mm['nodes'][$bid]['collapsed'] = true;
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-09-10 01:18:32 +02:00
display($mm);
}
2022-09-13 11:45:03 +02:00
2023-07-23 10:45:10 +02:00
function collapse_inner()
2022-09-13 11:45:03 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-13 11:45:03 +02:00
foreach ($mm['nodes'][ $mm['active_node'] ]['children'] as $cid)
$mm['nodes'][$cid]['collapsed'] = true;
$mm['nodes'][ $mm['active_node'] ]['collapsed'] = false;
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-09-13 11:45:03 +02:00
display($mm);
}
2022-09-10 01:18:32 +02:00
function find_branch(&$mm, $cid)
{
if ($mm['nodes'][$cid]['parent'] == $mm['root_id'])
2022-09-10 01:18:32 +02:00
return $cid;
else
return find_branch($mm, $mm['nodes'][$cid]['parent']);
}
2023-07-23 10:45:10 +02:00
function collapse_all()
2022-09-04 19:05:06 +02:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-09-04 19:05:06 +02:00
foreach ($mm['nodes'] as $id=>$node)
if (!$node['is_leaf'] && $id!=0 && $id!=$mm['root_id'])
2022-09-04 19:05:06 +02:00
$mm['nodes'][$id]['collapsed'] = true;
2022-08-30 17:54:49 +02:00
$mm['active_node'] = $mm['root_id'];
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-09-04 19:05:06 +02:00
display($mm);
2022-08-30 17:54:49 +02:00
}
2023-07-23 10:45:10 +02:00
function collapse_children()
2022-12-31 22:48:16 +01:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2022-12-31 22:48:16 +01:00
foreach ($mm['nodes'][ $mm['active_node'] ]['children'] as $cid)
if (!$mm['nodes'][$cid]['is_leaf'])
$mm['nodes'][$cid]['collapsed'] = true;
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-12-31 22:48:16 +01:00
display($mm);
}
2022-09-04 19:05:06 +02:00
function collapse(&$mm, $id, $keep)
{
if ($mm['nodes'][$id]['is_leaf']) return;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
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);
}
2022-08-30 17:54:49 +02:00
}
2023-07-23 10:45:10 +02:00
function collapse_level_1() { global $mm; collapse_level($mm, 1); }
function collapse_level_2() { global $mm; collapse_level($mm, 2); }
function collapse_level_3() { global $mm; collapse_level($mm, 3); }
function collapse_level_4() { global $mm; collapse_level($mm, 4); }
function collapse_level_5() { global $mm; collapse_level($mm, 5); }
function collapse_level_6() { global $mm; collapse_level($mm, 6); }
function collapse_level_7() { global $mm; collapse_level($mm, 7); }
function collapse_level_8() { global $mm; collapse_level($mm, 8); }
function collapse_level_9() { global $mm; collapse_level($mm, 9); }
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
function collapse_level(&$mm, $level, $no_display = false)
{
collapse($mm, $mm['root_id'], $level);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$id_collapsed = [];
$current = $mm['active_node'];
while ($current != $mm['root_id'])
2022-09-04 19:05:06 +02:00
{
$id_collapsed[$current] = $mm['nodes'][$current]['collapsed'];
$current = $mm['nodes'][$current]['parent'];
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
$id_collapsed = array_reverse( $id_collapsed, true);
foreach ($id_collapsed as $id=>$collapsed)
if ($collapsed)
{
$mm['active_node'] = $id;
break;
}
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
if ($no_display) return;
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
build_map($mm);
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-09-04 19:05:06 +02:00
display($mm);
2022-08-30 17:54:49 +02:00
}
// }}}
// {{{ display
2022-09-04 19:05:06 +02:00
function display(&$mm, $force_center = false)
{
if ($mm['focus_lock'])
{
2023-07-23 10:45:10 +02:00
focus_vh($mm);
2022-09-04 19:05:06 +02:00
build_map($mm);
}
if ($mm['center_lock'] || $force_center)
2023-07-23 10:45:10 +02:00
center_active_node_vh();
2022-09-04 19:05:06 +02:00
else
move_window($mm);
$mm['terminal_width'] = (int)exec('tput cols');
$mm['terminal_height'] = (int)exec('tput lines');
2022-09-04 19:05:06 +02:00
$blank = str_repeat(' ', $mm['terminal_width']);
2022-09-04 19:05:06 +02:00
// calculating the coordinates of the active node
$x1 =
max
(
0
,$mm['nodes'][ $mm['active_node'] ]['x']
-1
-$mm['viewport_left']
2022-09-04 19:05:06 +02:00
)
;
$x2
2022-09-04 19:05:06 +02:00
= $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']
2022-09-04 19:05:06 +02:00
)
;
$y2
2022-09-04 19:05:06 +02:00
= $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']]))
2022-09-04 19:05:06 +02:00
$line =
mb_substr(
$mm['map'][$y+$mm['viewport_top']],
$mm['viewport_left'],
2022-09-04 19:05:06 +02:00
$mm['terminal_width']
)
;
else
$line = $blank;
// ensuring that the line is long enough even if the map line is
// shorter than the terminal width
if (mb_strwidth($line) < $mm['terminal_width'])
$line .= str_repeat(' ', $mm['terminal_width'] - mb_strwidth($line));
2022-09-04 19:05:06 +02:00
// 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)
;
// styling the codes when the node is not active
// with "else", the downside is that other codes
// with the same "y" will not be styled.
// Without the "else", the downside is that the
// rest of the line in the active node won't be bold.
// between these two, I think the former is better.
else
$line =
mb_ereg_replace
(
'\b(.?\d+)\. '
,dim_on.'\\1. '.dim_off
,$line
)
;
2022-09-04 19:05:06 +02:00
2022-09-10 10:59:22 +02:00
// styling the search results
2022-09-04 19:05:06 +02:00
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
(
2022-09-09 21:35:23 +02:00
'([─-]+)'
2022-09-04 19:05:06 +02:00
,line_on.'\\1'.line_off
,$line
)
;
2023-07-23 16:57:17 +02:00
// styling "(?)
$line =
str_replace
(
'(?)'
,$mm['doubt_color'].'(?)'.default_color
,$line
);
2022-09-09 21:44:04 +02:00
// styling "???"
$line =
2022-09-09 21:44:04 +02:00
str_replace
(
'???'
,$mm['doubt_color'].'???'.default_color
2022-09-09 21:44:04 +02:00
,$line
);
2022-09-09 21:35:23 +02:00
// dimming {meta}s
$line =
2022-09-09 21:35:23 +02:00
str_replace
(
'{'
,dim_on.'{'
,$line
);
$line =
2022-09-09 21:35:23 +02:00
str_replace
(
'}'
,'}'.dim_off
,$line
);
2022-09-04 19:05:06 +02:00
// done!
$output .= $line;
}
2022-09-04 19:05:06 +02:00
echo reset_page.reset_color.$output;
// showing the logo :)
if ($mm['show_logo'])
{
$ll = $mm['terminal_width'] - 14;
2023-12-04 06:24:43 +01:00
echo "\033[1;{$ll}f\033[38;5;237m ┌────────────┐ ";
echo "\033[2;{$ll}f\033[38;5;237m │ ╭─ m │ ";
echo "\033[3;{$ll}f\033[38;5;237m │ h ──┤ │ ";
echo "\033[4;{$ll}f\033[38;5;237m │ ╰─── m │ ";
echo "\033[5;{$ll}f\033[38;5;237m └────────────┘ ";
}
2022-08-30 17:54:49 +02:00
}
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) );
}
2024-04-04 10:55:44 +02:00
function mmputcontent(&$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_strwidth($s) );
}
2022-12-22 17:07:30 +01:00
// }}}
// {{{ rank
2023-07-23 10:45:10 +02:00
function increase_positive_rank() { global $mm; rank($mm, +1, 0); }
function decrease_positive_rank() { global $mm; rank($mm, -1, 0); }
function increase_negative_rank() { global $mm; rank($mm, 0, +1); }
function decrease_negative_rank() { global $mm; rank($mm, 0, -1); }
2022-12-22 17:07:30 +01:00
function rank(&$mm, $add_positive, $add_negative)
{
$negative = 0;
$positive = 0;
$existing = [];
if (mb_ereg('^\((\d+)\+,(\d+)\-\)', $mm['nodes'][ $mm['active_node'] ]['title'], $existing))
{
$positive = $existing[1];
$negative = $existing[2];
$mm['nodes'][ $mm['active_node'] ]['title'] =
mb_ereg_replace('^\(\d+\+,\d+\-\) *', '', $mm['nodes'][ $mm['active_node'] ]['title']);
}
$negative = max(0, $negative + $add_negative);
$positive = max(0, $positive + $add_positive);
$mm['nodes'][ $mm['active_node'] ]['title'] =
"({$positive}+,{$negative}-) "
.$mm['nodes'][ $mm['active_node'] ]['title']
;
2023-01-01 11:14:54 +01:00
push_change($mm);
build_map($mm);
display($mm);
}
// }}}
// {{{ open link
2023-07-23 10:45:10 +02:00
function open_link()
{
2023-07-23 10:45:10 +02:00
global $mm;
message($mm,'Opening the node with xdg-open...');
exec('xdg-open "'.$mm['nodes'][ $mm['active_node'] ]['title'].'" >/dev/null 2>&1 &', $output, $result);
display($mm);
}
2023-01-01 11:14:54 +01:00
// }}}
// {{{ sort
2023-07-23 10:45:10 +02:00
function sort_siblings()
2023-01-01 11:14:54 +01:00
{
2023-07-23 10:45:10 +02:00
global $mm;
2023-01-01 11:14:54 +01:00
if ($mm['active_node'] <= $mm['root_id'])
return;
$siblings = [];
$sibling_ids = $mm['nodes'][ $mm['nodes'][ $mm['active_node'] ]['parent'] ]['children'];
if (empty($sibling_ids))
return;
foreach ($sibling_ids as $sid)
if (mb_ereg('^\((\d+)\+,(\d+)\-\)', $mm['nodes'][$sid]['title'], $values))
2023-03-18 08:34:00 +01:00
$siblings
[
(9000000 - $values[1] + $values[2])
.'.'
.$sid
]
= $sid;
2023-01-01 11:14:54 +01:00
else
2023-03-18 08:34:00 +01:00
$siblings
[
mb_ereg_replace
(
'^\[HIDDEN\] |^'.$mm['symbol1'].' |^'.$mm['symbol2'].' '
,''
,$mm['nodes'][$sid]['title']
)
.'.'
.$sid
]
= $sid;
2023-01-01 11:14:54 +01:00
ksort($siblings, SORT_NATURAL);
$mm['nodes'][ $mm['nodes'][ $mm['active_node'] ]['parent'] ]['children'] = $siblings;
push_change($mm);
2022-12-22 17:07:30 +01:00
build_map($mm);
display($mm);
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ monitor key presses
2022-09-04 19:05:06 +02:00
function monitor_key_presses(&$mm)
{
2023-07-23 10:45:10 +02:00
global $keybindings;
2022-09-04 19:05:06 +02:00
stream_set_blocking(STDIN,false);
2022-08-30 17:54:49 +02:00
2022-09-04 19:05:06 +02:00
while(true)
{
usleep(20000);
$in = fread(STDIN, 16);
if (empty($in))
continue;
2022-08-30 17:54:49 +02:00
2023-07-23 10:45:10 +02:00
if (isset($keybindings[$in]))
call_user_func($keybindings[$in]);
2022-08-30 17:54:49 +02:00
2023-07-25 09:09:07 +02:00
if ($mm['echo_keys'])
{
echo "\033[1;1f";
for ($i=0; $i<strlen($in); $i++)
echo base_convert(ord($in[$i]),10,8).' ';
echo ' ';
}
2022-09-04 19:05:06 +02:00
}
2022-08-30 17:54:49 +02:00
}
2023-02-07 20:12:50 +01:00
// }}}
// {{{ change window title
2023-02-07 20:12:50 +01:00
function change_window_title($title)
{
echo "\033]2;".( $title == '' ? ' ' : 'h-m-m -- '.str_replace('.hmm','',basename($title)) )."\007";
2023-02-07 20:12:50 +01:00
}
2022-08-30 17:54:49 +02:00
// }}}
// {{{ main
check_required_extensions();
check_the_available_clipboard_tool($mm);
enable_alternate_screen();
2022-08-30 17:54:49 +02:00
load_file($mm);
2022-09-09 21:35:23 +02:00
collapse_all($mm);
collapse_level($mm, $mm['initial_depth'], false);
2022-08-30 17:54:49 +02:00
2023-12-04 06:24:43 +01:00
$mm['show_logo'] = false;
2022-08-30 17:54:49 +02:00
monitor_key_presses($mm);
// }}}
2022-09-18 13:48:51 +02:00