#!/usr/bin/env php * @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]) ] = trim( $a[1] ?? true, '"' ); } elseif (isset($mm['arguments']['filename'])) { echo "Invalid arguments: more than one filename!".PHP_EOL; 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); } } elseif (isset($mm['arguments']['config'])) die("ERROR: the custom config file ($conf) doesn't exit!\n"); // 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, 'align_levels', 0); config($mm, 'symbol1', "✓"); config($mm, 'symbol2', "✗"); 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"); 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', ""); 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; $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_right'] = str_repeat('─', conn_right_len - 2 ); $mm['conn_left'] = str_repeat('─', conn_left_len -2 ); $mm['conn_single'] = str_repeat('─', conn_left_len + conn_right_len - 3 ); 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 ctrl_h = "\010"; const ctrl_o = "\017"; 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] = str_replace ( '\033' ,"\033" , $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!'.PHP_EOL; exit(1); } } 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 ;) 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.".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; default: echo "I can't find your clipboard tool!".PHP_EOL; 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"); change_window_title(''); 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'].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 function load_file(&$mm) { $mm['filename'] = $mm['arguments']['filename'] ?? ''; if (isset($mm['arguments']['filename']) && file_exists($mm['filename'])) $lines = file($mm['filename'], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); else { 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); if (empty($new_nodes)) { load_empty_map($mm); return; } change_window_title($mm['filename']); // 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; } // }}} // {{{ 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); } // }}} // {{{ 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 ) ; $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'] ; if ( mb_strlen($node['title']) > width_tolerance * $max_width ) { $lines = explode(PHP_EOL ,wordwrap($node['title'] ,$max_width, PHP_EOL)); $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 ($at_the_end) $mm['nodes'][$id]['clh'] = $mm['nodes'][$id]['lh']; foreach ($node['visible_children'] as $cid) { calculate_x_and_lh($mm, $cid); $mm['nodes'][$id]['clh'] += $mm['nodes'][$cid]['clh']; } } // }}} // {{{ 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; foreach ($mm['nodes'][$id]['visible_children'] as $cid) { $max_width = max( $max_width, $mm['nodes'][$cid]['w'] ); $mm['nodes'][$cid]['x'] = $x; } foreach ($mm['nodes'][$id]['visible_children'] as $cid) calculate_aligned_x ( $mm ,$cid ,$max_width +$x +conn_left_len +conn_right_len +1 ) ; } // }}} // {{{ calculate h function calculate_h(&$mm) { $unfinished = true; while ($unfinished) { $unfinished = false; foreach ($mm['nodes'] as $id=>$node) if ($node['is_leaf'] || $node['visible_children']==0 || ($node['collapsed'] ?? false)) $mm['nodes'][$id]['h'] = $mm['line_spacing'] + $mm['nodes'][$id]['lh'] ; else { $h = 0; $unready = false; foreach ($node['visible_children'] as $cid) if ($mm['nodes'][$cid]['h']>=0) $h += $mm['nodes'][$cid]['h']; else { $unready = true; break; } if ($unready) $unfinished = true; else $mm['nodes'][$id]['h'] = max ( $h , $node['lh'] + $mm['line_spacing'] ) ; } } } // }}} // {{{ calculate y and yo function calculate_y(&$mm) { $mm['map_top'] = 0; $mm['map_bottom'] = 0; $mm['map_height'] = $mm['nodes'][0]['h']; $mm['nodes'][0]['y'] = 0; calculate_children_y($mm, 0); } function calculate_children_y(&$mm,$pid) { $y = $mm['nodes'][$pid]['y']; $mm['nodes'][$pid]['yo'] = round( ( + $mm['nodes'][$pid]['h'] - $mm['nodes'][$pid]['lh'] )/2 ) ; if (!($mm['nodes'][$pid]['collapsed'] ?? false)) foreach ($mm['nodes'][$pid]['visible_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, floor( (($mm['nodes'][$id]['lh'] - $mm['nodes'][$id]['clh']))/2 - 0.9 )); foreach ($mm['nodes'][$id]['visible_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]; $num_visible_children = count($mm['nodes'][$id]['visible_children']); $num_children = count($mm['nodes'][$id]['children']); $has_hidden_children = $num_visible_children != $num_children; // if the node is collapsed if ( ($node['collapsed'] ?? false) && ($num_children > 0) ) { if ($num_visible_children == $num_children) mmput ( $mm ,$node['x'] + $node['w']+1 ,$node['y'] + $node['yo'] ,' [+]' ); else mmput ( $mm ,$node['x'] + $node['w']+1 ,$node['y'] + $node['yo'] ,'─╫─ [+]' ); 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 ) ,'─╫─' ); return; } // if there's only one child: the same y coordinate if ($num_visible_children==1) { $child_id = $node['visible_children'][ array_key_first( $node['visible_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 ); $x = $mm['align_levels'] ? $node['x'] + $node['w'] - 2 : $child['x'] - conn_left_len - conn_right_len ; $line = ( $has_hidden_children ? '─╫' : '──' ) . ( $mm['align_levels'] ? str_repeat('─', $child['x'] - $node['x'] - $node['w'] - 1) : $mm['conn_single'] ) ; mmput ( $mm ,$x ,min($y1,$y2) ,$line ); if (abs(min($y1,$y2)-$y2)>0) { for ($yy=min($y1,$y2); $yy $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['visible_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); $x = $mm['align_levels'] ? $node['x'] + $node['w'] - 2 : $mm['nodes'][$top_child]['x'] - conn_left_len - conn_right_len ; $line = ( $has_hidden_children ? '─╫' : '──' ) . ( $mm['align_levels'] ? str_repeat('─', $mm['nodes'][$top_child]['x'] - $node['x'] - $node['w'] - 3 ) : $mm['conn_left'] ) ; mmput ( $mm ,$x ,$middle ,$line ); 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 ($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'] ); $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['visible_children'] as $cid) draw_connections($mm, $cid); } // }}} // {{{ add content to the map function add_content_to_the_map(&$mm, $id) { $node = $mm['nodes'][$id]; $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'] ; if ( mb_strlen($node['title']) > width_tolerance * $max_width) $lines = explode ( PHP_EOL, wordwrap ( $node['title'] ,$max_width ,PHP_EOL ) ) ; 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['visible_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'][$id]['visible_children'] = $mm['nodes'][$id]['children']; } $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 if (!$mm['show_hidden']) list_visible_children($mm, $mm['root_id']); calculate_x_and_lh($mm,$mm['root_id']); if ($mm['align_levels']) calculate_aligned_x($mm,$mm['root_id'],$mm['nodes'][ $mm['root_id'] ]['w'] + conn_left_len + conn_right_len + 1); 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 numbers function toggle_numbers(&$mm) { if ($mm['active_node'] <= $mm['root']) return; $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 $mm['nodes'][$cid]['title'] = str_pad($i++, $padlen, '0', STR_PAD_LEFT).'. '.$mm['nodes'][$cid]['title']; $mm['modified'] = true; build_map($mm); display($mm); } // }}} // {{{ toggle symbol function toggle_symbol(&$mm) { $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 $mm['nodes'][ $mm['active_node'] ]['title'] = $mm['symbol1'] .' ' .$mm['nodes'][ $mm['active_node'] ]['title']; $mm['modified'] = true; build_map($mm); display($mm); } // }}} // {{{ toggle hide function toggle_hide(&$mm) { if ($mm['active_node'] == $mm['root_id']) return; push_change($mm); $mm['modified'] = true; $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); } build_map($mm); display($mm); message($mm, 'Hidden attribute is turned '.($is_hidden ? 'on' : 'off').' for the node.'); } function toggle_show_hidden(&$mm) { $mm['show_hidden'] = !$mm['show_hidden']; move_active_node_to_sibling_or_parent($mm); 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! $mm['active_node'] = $parent_id; } } // }}} // {{{ toggle align function toggle_align(&$mm) { $mm['align_levels'] = !$mm['align_levels']; build_map($mm); display($mm); } // }}} // {{{ 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==$mm['root_id']) 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; 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); push_change($mm); $mm['modified'] = true; $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); 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 = max ( 0 ,mb_strrpos ( $title ,' ' ,max ( -mb_strlen($title) ,$cursor-mb_strlen($title)-3 ) ) ) ; $title = mb_substr($title, 0, $from + ($from>0) ) .mb_substr($title, $cursor-1) ; $cursor = $from+1+($from>0); } // 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") { $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 ) ; } // delete elseif ($in=="\033\133\063\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 = str_replace(["\n","\r","\t"],[" ",""," "],$title); $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' ) or $title=='NEW' ) $title = ''; $output = magic_readline($mm, $title); if ( ( $output === false or $output === '' ) && $mm['nodes'][ $mm['active_node'] ]['title'] === '' && $mm['nodes'][ $mm['active_node'] ]['is_leaf'] ) { delete_node($mm, false, true); push_change($mm); $mm['modified'] = true; 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); $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 && ( $mm['show_hidden'] || substr($node['title'],0,9) != '[HIDDEN] ' ) && $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 && ( $mm['show_hidden'] || substr($node['title'],0,9) != '[HIDDEN] ' ) && $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 = []; $just_passed_active = false; foreach ($mm['nodes'][ $parent_id ]['children'] as $child) { if ($child!=$mm['active_node']) $children[] = $child; if ($just_passed_active && ($mm['show_hidden'] || substr($mm['nodes'][$child]['title'],0,9)!='[HIDDEN] ' ) ) { $children[] = $mm['active_node']; $just_passed_active = false; } if ($child==$mm['active_node']) $just_passed_active = true; } if ($just_passed_active) $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 = []; $just_passed_active = false; $rev_children = array_reverse($mm['nodes'][$parent_id]['children']); foreach ($rev_children as $child) { if ($child!=$mm['active_node']) $children[] = $child; if ($just_passed_active && ($mm['show_hidden'] || substr($mm['nodes'][$child]['title'],0,9)!='[HIDDEN] ' ) ) { $children[] = $mm['active_node']; $just_passed_active = false; } if ($child==$mm['active_node']) $just_passed_active = true; } if ($just_passed_active) $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 ,'' .'' .'' .'' .$mm['nodes'][ $mm['root_id'] ]['title'] .'' .'' .'' .'' .'' .'' .'
' .export_html_node($mm, $mm['root_id']) .'
' .'
' .'This is a limited, read-only version of a mind-map created in h-m-m | view source!' .'
'
		.map_to_list($mm,$mm['root_id'])
		.'
' .'
' .'' .'' ); fclose($file); message($mm, 'Exported as '.$mm['filename'].'.html'); copy_to_clipboard($mm, $mm['filename'].'.html'); $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.')'); } } function export_html_node(&$mm, $parent_id) { if ($mm['nodes'][$parent_id]['visible_children']==[]) { $output = "

" .$mm['nodes'][$parent_id]['title'] ."

"; } elseif ($parent_id==$mm['root_id']) { $output = "
" .$mm['nodes'][$parent_id]['title'] ."
"; foreach ($mm['nodes'][$parent_id]['visible_children'] as $cid) $output .= export_html_node($mm, $cid); } else { $output = "
" ."" .$mm['nodes'][$parent_id]['title'] .""; foreach ($mm['nodes'][$parent_id]['visible_children'] as $cid) $output .= export_html_node($mm, $cid); $output .= "
"; } return $output; } // }}} // {{{ export text function export_text(&$mm) { $output = ''; foreach ($mm['map'] as $line) if (rtrim($line) != '') $output .= substr(rtrim($line),3)."\n"; copy_to_clipboard($mm, $output); message($mm, 'Exported the map to clipboard.'); } // }}} // {{{ 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, (empty($mm['filename']) ? $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; } change_window_title($mm['filename']); $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']) . "\n"); fclose($file); display($mm); message($mm, 'Saved '.$mm['filename']); } // }}} // {{{ message function message(&$mm, $text) { $mm['terminal_width'] = (int)exec('tput cols'); $mm['terminal_height'] = (int)exec('tput lines'); echo "\033[" .$mm['terminal_height'] // y .";" .max(0,$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['change_index']--; $mm['nodes'] = $mm['changes'][$mm['change_index']]; $mm['active_node'] = $mm['change_active_node'][$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 (count($node['visible_children'])==0) return; // auto-unfold node on right move if ($node['collapsed'] ?? false) { toggle_node($mm); $node = $mm['nodes'][ $mm['active_node'] ]; } $distance = []; foreach ($node['visible_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'] ]['visible_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'] ]['visible_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; } // }}} // {{{ 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'])); // \n instead of PHP_EOL, just to be sure. // existing \r's will be removed later. $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) { switch ($mm['clipboard']) { case 'os': $clip = popen($mm['os_clipboard']['write'],'wb'); 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; } } function get_from_clipboard(&$mm) { $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 ( "[\000-\010\013-\037\177]|".BOM ,'' ,$text ) ; } // }}} // {{{ load empty map function load_empty_map(&$mm) { change_window_title('new mind map'); if (isset($mm['nodes'])) unset($mm['nodes']); $mm['nodes'][0] = [ 'title'=>'X' ,'is_leaf'=>false ,'children'=>[1] ,'collapsed'=>false ,'parent'=>-1 ] ; $title = basename( empty($mm['filename']) ? 'root' : $mm['filename'] ); if (substr($title,-4)=='.hmm') $title = substr($title,0,-4); $mm['nodes'][1] = [ 'title'=> $title ,'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]['visible_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] ) ; $mm['nodes'][$parent_id]['visible_children'] = array_diff ( $mm['nodes'][$parent_id]['visible_children'], [$active_node] ) ; if (count($mm['nodes'][$parent_id]['children'])==0) $mm['nodes'][$parent_id]['is_leaf'] = true; unset($mm['nodes'][ $active_node ]); if (count($mm['nodes'][$parent_id]['visible_children'])==0) $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']); } // }}} // {{{ collapse and expand function expand_all(&$mm) { foreach ($mm['nodes'] as $id=>$node) $mm['nodes'][$id]['collapsed'] = false; build_map($mm); center_active_node($mm); display($mm); } 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); } 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_all_children(&$mm) { foreach ($mm['nodes'][ $mm['active_node'] ]['children'] as $cid) if (!$mm['nodes'][$cid]['is_leaf']) $mm['nodes'][$cid]['collapsed'] = true; 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) ; // 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 ) ; // 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; // showing the logo :) if ($mm['show_logo']) { $ll = $mm['terminal_width'] - 14; 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 └────────────┘ "; $mm['show_logo'] = false; } } 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) ); } // }}} // {{{ rank 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'] ; push_change($mm); $mm['modified'] = true; build_map($mm); display($mm); } // }}} // {{{ open link function open_link(&$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); } // }}} // {{{ sort function sort_siblings(&$mm) { 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)) $siblings [ (9000000 - $values[1] + $values[2]) .'.' .$sid ] = $sid; else $siblings [ mb_ereg_replace ( '^\[HIDDEN\] |^'.$mm['symbol1'].' |^'.$mm['symbol2'].' ' ,'' ,$mm['nodes'][$sid]['title'] ) .'.' .$sid ] = $sid; ksort($siblings, SORT_NATURAL); $mm['nodes'][ $mm['nodes'][ $mm['active_node'] ]['parent'] ]['children'] = $siblings; push_change($mm); $mm['modified'] = true; build_map($mm); display($mm); } // }}} // {{{ 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 'H': toggle_hide($mm); break; case ctrl_h: toggle_show_hidden($mm); 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 '~': go_to_root($mm); break; case 'n': next_search_result($mm); break; case 'N': previous_search_result($mm); break; case ctrl_o: open_link($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 't': toggle_symbol($mm); break; case 'T': sort_siblings($mm); break; case '#': toggle_numbers($mm); break; case 'u': undo($mm); break; case 'v': collapse_all($mm); break; case 'V': collapse_all_children($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 'X': export_text($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 ' ': toggle_node($mm); break; case '|': toggle_align($mm); break; case '/': search($mm); break; case '?': search($mm); break; case ctrl_f: search($mm); break; case '=': rank($mm, +1, 0); break; case '+': rank($mm, -1, 0); break; case '-': rank($mm, 0,+1); break; case '_': rank($mm, 0,-1); 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: // echo "\033[1;1f"; // for ($i=0; $i