=head1 NAME du_scanner.pl - display du output scanner like way =head1 SYNOPSIS du | perl du_scanner.pl or du > du.txt du_scanner.pl du.txt =head1 DESCRIPTION du の結果を Scanner っぽく表示します。 =cut use strict; use warnings; use encoding 'cp932'; binmode(STDERR, 'raw: encoding(cp932)'); use constant PI => (4 * atan2(1, 1)); my @lines; # ファイルの一覧 # 引数ファイル or 標準入力から読み込み while (<>) { push @lines, $_; } unless (@lines) { print "invalid data format!\n"; exit(1); } #################### # 設定 #################### # キャンバスの幅、高さ my $width = 400; my $height = $width; # 中心点の座標 my $r = $width / 2 - 1; my $cx = $width / 2 + 1; my $cy = $height / 2 + 1; # 各階層での円描画位置 my (@x1, @y1, @x2, @y2); # 現在中心に描画している node に関する情報 my $cur_root; my $cur_root_size; my $cur_root_name; #################### # 解析 #################### my %nodes; my $max_depth; # 描画する必要のある階層数 my $count = 0; my @depth_files; # 各 depth の角度毎のファイル名。 # $depth_files[4] = [ [100, 110, 'hoge'], [110, 120, 'fuga'] ]; みたいな感じ foreach my $line (@lines) { chomp $line; my ($size, $path) = ($line =~ /^(\d+)\s+([^\r\n]*)/); my @dirs = split(qr#/#, $path); # '/' の場合、ファイル名が空文字になるらしいのでその対策 if (scalar(@dirs) == 0) { unshift @dirs, ''; } my $code = '$nodes' . (join '', map { "{'$_'}" } @dirs) . "{'*'} = $size"; # サイズは '*' というキーで格納する eval $code; die $@ if $@; $count++; } my $root_path = (keys(%nodes))[0]; my $root = $nodes{$root_path}; print "file count: $count\n"; print "root path: $root_path\n"; #################### # 描画 #################### use Tk; my $top = MainWindow->new(); my $text = $top->Text( -width => 50, -height => 3, -wrap => "word", )->grid(-row => 0, -column => 0, -rowspan => 2, -sticky => "news"); my $b_root = $top->Button( -text => "root", -command => sub { draw_all($root, $root_path) } )->grid(-row => 0, -column => 1, -sticky => "ew"); my $b_parent = $top->Button( -text => "parent", -command => sub { draw_parent() } )->grid(-row => 1, -column => 1, -sticky => "ew"); my $canvas = $top->Canvas( width => $width, height => $height )->grid(-row => 2, -column => 0, -columnspan => 2); $top->gridColumnconfigure(0, -minsize => $width - 50); #################### # イベントの割り付け #################### # Canvas マウス移動→ファイル名表示 $canvas->Tk::bind("", [\&show_name, Ev('x'), Ev('y')]); # Canvas ダブルクリック→そのノードで再描画 $canvas->Tk::bind("", [\&redraw_all, Ev('x'), Ev('y')]); # Canvas の描画 draw_all($root, $root_path); # ウィンドウの表示その他 MainLoop(); exit(0); =head1 FUNCTIONS =cut =head2 draw_all(\%nodes, $name) 対象のノード以下を描画する。 =cut sub draw_all { my $nodes = shift @_; my $name = shift @_; # 初期化 @depth_files = (); $cur_root = $nodes; $cur_root_size = $nodes->{'*'}; $cur_root_name = $name; $cur_root_name = '/' if $cur_root_name eq ''; # '/' を空文字で表示しないための小細工 # メインウィンドウのタイトルに設定 $top->configure(-title => $cur_root_name); # 描画する必要のある階層数を調べる $max_depth = 1; count_depth($nodes, 1); for (my $i = 1; $i <= $max_depth; $i++) { my $rr = $r * $i / $max_depth; $x1[$i] = $cx - $rr; $y1[$i] = $cy - $rr; $x2[$i] = $cx + $rr; $y2[$i] = $cy + $rr; } # 砂時計表示 $canvas->Busy(); # 外側の円を描画 my $depth = $max_depth; $canvas->createOval($x1[$depth], $y1[$depth], $x2[$depth], $y2[$depth], -fill => 'white'); # 扇形を描画 draw_circle($nodes, 1, 0, $name, undef); # 中心円を描画 $depth = 1; $canvas->createOval($x1[$depth], $y1[$depth], $x2[$depth], $y2[$depth], -fill => name2color($name)); # 砂時計表示の解除 $canvas->Unbusy(); } =head2 count_depth(\%nodes, $depth) 描画する必要のある階層数を調べる。 =cut sub count_depth { my $files = shift @_; my $depth = shift @_; my $size = $files->{'*'}; # 実際のサイズ # 1 degree 以下のものは描画対象外 if (360 * $size / $cur_root_size < 1) {return}; if ($depth > $max_depth) { $max_depth = $depth; } while (my ($key, $val) = each %$files) { next if $key eq '*' || $key eq '..'; count_depth($val, $depth+1); } } =head2 count_depth(\%nodes, $depth, $total_size, $name, $parent) 扇形を描画する。 =cut sub draw_circle { my $files = shift @_; my $depth = shift @_; my $total_size = shift @_; my $name = shift @_; my $parent = shift @_; my $size = $files->{'*'}; # 実際のサイズ my $dir_size = 0; # 現在のディレクトリのうち、描画済みの部分 # サブノードがある場合は再帰処理 foreach my $key (sort keys(%$files)) { my $val = $files->{$key}; next if $key eq '*' || $key eq '..'; draw_circle($val, $depth+1, $total_size + $dir_size, "$name/$key", $files); $dir_size += $val->{'*'}; } # あまり角度が小さいと 360 度描画されるので、スキップ if (360 * $size / $cur_root_size < 1) {return}; ### $total_size から $total_size+$size までを描画 # 描画開始角と描画角度。3 時方向が 0 度で、半時計回り my $start_org = 360.0 * $total_size / $cur_root_size; my $extent = 360.0 * $size / $cur_root_size; # 描画開始角と描画角度を時計的な表現に変換したもの my $start = 90 - $start_org - $extent; # ファイル名を元に色を設定する my $color = name2color($name); # 扇形の描画 $canvas->createArc( $x1[$depth], $y1[$depth], $x2[$depth], $y2[$depth], -start => $start, -extent => $extent, -fill => $color, ); ### Canvas クリック時に使用する情報の構築。元々の情報で構築する。 $name = '/' if $name eq ''; # '/' を空文字で表示しないための小細工 push @{$depth_files[$depth]}, [ $start_org, $start_org + $extent, $name, $size, $files, ]; # 親へのリファレンスを設定 $files->{'..'} = $parent unless exists $files->{'..'}; } =head2 name2color($name) 文字列を適当な色に変換する。 =cut sub name2color { my $name = shift @_; # ハッシュ値を計算 my $hash = 0; for (my $i = 0; $i < length($name); $i++) { $hash = ($hash * 33 + ord(substr($name, $i, 1))) & 0xffffff; } # ハッシュ値を適当にマスク return sprintf("#%06x", $hash | 0x808080); } =head2 show_name($x, $y) canvas 上の $x, $y に対応するファイル名を表示する。 =cut sub show_name { my ($self, $x, $y) = @_; my ($name, $size, $noderef) = get_node($x, $y); # text のクリア $text->selectAll(); $text->deleteSelected(); if (defined $name) { $text->insert('end', $name . " " . commify($size)); } } =head2 popup_name($x, $y) canvas 上の $x, $y に対応するファイル名をポップアップ表示する。 =cut sub popup_name { my ($self, $x, $y) = @_; my ($name, $size, $noderef) = get_node($x, $y); if (defined $name) { } } =head2 redraw_all($x, $y) canvas 上の $x, $y に対応するノードを元に再描画を行う。 =cut sub redraw_all { my ($self, $x, $y) = @_; my ($name, $size, $noderef) = get_node($x, $y); if (defined $name) { draw_all($noderef, $name); } } =head2 draw_parent() 現在の中心ノードの親ノードを中心に持ってくる。 =cut sub draw_parent { my $parent = $cur_root->{'..'}; return unless $parent; (my $parent_name = $cur_root_name) =~ s#(.*)/[^/]*$#$1#; draw_all($parent, $parent_name); } =head2 get_node($x, $y) canvas 上の $x, $y に対応するノードの情報($name, $size, \%node)を返す。 =cut sub get_node { my ($x, $y) = @_; # 中心点からの距離を元に階層を計算する my $mx = $x - $cx; my $my = $cy - $y; my $d = ($mx**2 + $my**2) ** 0.5; my $depth = int($d / $r * $max_depth) + 1; # 角度を計算する。角度表記は時計と同じ。 my $deg = atan2($mx, $my) / PI * 180; $deg += 360 if $deg < 0; # 条件に該当するノードを探す foreach my $range (@{$depth_files[$depth]}) { my ($start, $end, $name, $size, $noderef) = @$range; if ($start <= $deg && $deg <= $end) { return ($name, $size, $noderef); } } return undef; } =head2 commify(I) 3 桁毎のカンマ区切り処理。 "Perl Cookbook 2nd Edition" Recipe 2.16 そのまんま。 =cut sub commify { my $text = reverse $_[0]; $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; return scalar reverse $text; }