├── img ├── dot.png ├── neato.png ├── sfdp.png └── twopi.png ├── LICENSE ├── README.md └── pfsg.py /img/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niveknosredneh/PFSG/HEAD/img/dot.png -------------------------------------------------------------------------------- /img/neato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niveknosredneh/PFSG/HEAD/img/neato.png -------------------------------------------------------------------------------- /img/sfdp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niveknosredneh/PFSG/HEAD/img/sfdp.png -------------------------------------------------------------------------------- /img/twopi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niveknosredneh/PFSG/HEAD/img/twopi.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin Henderson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PFSG - Python Filesystem Grapher 2 | 3 | Uses matplotlib, networkx and graphviz to create a beautiful graph of directory structure and exports as a png image for use as desktop wallpaper 4 | 5 | ## Examples 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ## Prerequisites 16 | 17 | Ubuntu and Debian based: 18 | ``` 19 | # Mandatory dependencies: 20 | sudo apt-get install python3 python3-pip 21 | sudo pip3 install matplotlib networkx pydot graphviz 22 | 23 | # Optional dependencies: 24 | sudo apt-get install imagemagick feh 25 | ``` 26 | (untested) Arch based: 27 | ``` 28 | # Mandatory dependencies: 29 | sudo pacman -S python3 python3-pip python-pygraphviz 30 | sudo pip3 install matplotlib networkx pydot graphviz 31 | 32 | # Optional dependencies: 33 | sudo pacman -S imagemagick feh 34 | ``` 35 | 36 | ## Installing 37 | ``` 38 | wget https://raw.githubusercontent.com/niveknosredneh/PFSG/master/pfsg.py 39 | sudo chmod +x pfsg.py 40 | ./pfsg.py 41 | ``` 42 | 43 | ## Options 44 | 45 | All options have been moved to top of pfsg.py file 46 | 47 | 48 | 49 | See [https://matplotlib.org](https://matplotlib.org/gallery/color/colormap_reference.html) for full colourmap reference 50 | 51 | ## Authors 52 | 53 | * **Kevin Matthew Henderson** 54 | 55 | ## Contributors 56 | 57 | ## License 58 | 59 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/niveknosredneh/PFSG/blob/master/LICENSE) file for details 60 | -------------------------------------------------------------------------------- /pfsg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Kevin Matthew Henderson - 2022 3 | 4 | directory="~/Code" 5 | output="twopi.jpg" 6 | 7 | # scan Init process tree if True, instead of filesystem if False 8 | process=True 9 | 10 | layout="fdp" # neato|twopi|sfdp|fdp|dot|circo 11 | width=1920 12 | height=1080 13 | 14 | background_colour="#000000FF" 15 | image="./debian.png" # path of background image to underlay, or None 16 | #image=None 17 | opacity=40 18 | 19 | max_depth=7 # currently crashes (for me) with huge number of files / dirs 20 | label_size=12 21 | label_colour="#000000FF" 22 | label_depth=10 23 | label_size_reduce=1.0 24 | label_alpha_reduce=0.3 25 | label_alpha=1.0 26 | 27 | edge_colour="#666666FF" #RRGGBBAA' or matplotlib colormap 28 | edge_width=1.0 29 | edge_style="dotted" #solid|dashed|dotted|dashdot 30 | edge_width_reduce=0 31 | edge_length=10.0 #fdp/neato only 32 | 33 | node_colour="#222222FF" #RRGGBBAA' or matplotlib colormap 34 | node_size=1.0 35 | 36 | file_node_colour="#22FF22FF" 37 | file_node_size=1.0 38 | file_label_colour="#FF2222FF" 39 | file_label_size=12 40 | show_file_labels=False 41 | map_by_size=False 42 | colourmap_soften=0 43 | show_files=False # if False only shows directories 44 | 45 | set_wallpaper=True # uses 'feh' 46 | verbose=True 47 | 48 | 49 | 50 | """ 51 | PFSG - Python Filesystem Grapher 52 | 53 | Uses matplotlib and networkx to create graph of a directory structure and exports to png image for use as desktop wallpaper 54 | """ 55 | 56 | 57 | from networkx.drawing.nx_pydot import graphviz_layout 58 | #from networkx.drawing.nx_pydot import write_dot 59 | import matplotlib.pyplot as plt 60 | import networkx as nx 61 | import os 62 | import subprocess 63 | 64 | import psutil 65 | 66 | # throw error early if colormap not valid 67 | if edge_colour[0]=="#": edge_colourmap = None 68 | else: edge_colourmap = plt.get_cmap(edge_colour) 69 | if node_colour[0]=="#": node_colourmap = None 70 | else: node_colourmap = plt.get_cmap(node_colour) 71 | if file_node_colour[0]=="#": file_node_colourmap = None 72 | else: file_node_colourmap = plt.get_cmap(file_node_colour) 73 | 74 | print("Creating Network and Adding Nodes ...") 75 | G = nx.Graph() # create graph 76 | 77 | deepest = 0 # keeps track of max folder depth 78 | largest = 0 79 | num_nodes = 1 80 | if not process: 81 | base_depth = len(directory.split("/")) 82 | 83 | files = {} 84 | G.add_node(directory, file='False', depth=0, label=directory.split("/")[-1], size=0) 85 | for (dirpath, dirnames, filenames) in os.walk(directory): 86 | cur_depth = len(dirpath.split("/"))-base_depth + 1 87 | if cur_depth > deepest: 88 | deepest = cur_depth 89 | if cur_depth < max_depth: 90 | for f in filenames: # FILES 91 | new_label = "" 92 | size = 0 93 | if map_by_size: 94 | size=os.path.getsize(os.path.join(dirpath, f)) 95 | files[os.path.join(dirpath, f)] = size 96 | if size > largest: largest = size 97 | if cur_depth < label_depth and show_file_labels: new_label = f 98 | if show_files: 99 | G.add_node(os.path.join(dirpath, f), file='True', depth=cur_depth, label=new_label, size=size) 100 | G.add_edge(dirpath , os.path.join(dirpath, f), file='True', depth=cur_depth, label=size, size=size) 101 | num_nodes+=1 102 | 103 | for d in dirnames: # DIRECTORIES 104 | new_label = "" 105 | size = 0 106 | if map_by_size: size=os.path.getsize(os.path.join(dirpath, d)) 107 | if cur_depth < label_depth: new_label = d 108 | G.add_node(os.path.join(dirpath, d), file='False', depth=cur_depth, label=new_label, size=size) 109 | G.add_edge(dirpath , os.path.join(dirpath, d), file='False', depth=cur_depth, label=size, size=size) 110 | num_nodes+=1 111 | print("Added " + str(num_nodes) + " Nodes") 112 | 113 | else: # Processes, not filesystem 114 | 115 | root = psutil.Process(1) 116 | G.add_node(root.pid, size=1, file='False', depth=0, label=f"{root.name()}") 117 | for p in root.children(recursive=True): 118 | #for p in psutil.process_iter(['pid', 'name', 'username']): 119 | 120 | if p.memory_percent()<1: mem="" 121 | else: mem = f"{int(p.memory_percent())}%" 122 | G.add_node(p.pid, size=1, file='False', depth=nx.shortest_path_length(G,root.pid,p.parent().pid), label=f"{p.name()} {mem}") 123 | if not (p.parent() is None): G.add_edge(p.pid , p.parent().pid, depth=3) 124 | 125 | #par = p.parent().pid 126 | #while par is not root.pid: 127 | 128 | print("Calculating Layout ...") 129 | pos = graphviz_layout(G, prog=layout)#, root=1) # use layout 130 | fig = plt.figure(figsize=(width/100.0, height/100.0)) # set figure size 131 | 132 | # DRAW 133 | print("Drawing Graph ...") 134 | #nodes 135 | node_depths = nx.get_node_attributes(G,'depth') 136 | edge_depths = nx.get_edge_attributes(G, 'depth') 137 | node_labels = nx.get_node_attributes(G, 'label') 138 | edge_labels = nx.get_edge_attributes(G, 'label') 139 | node_sizes = nx.get_node_attributes(G,'size') 140 | edge_sizes = nx.get_edge_attributes(G,'size') 141 | is_file = nx.get_node_attributes(G,'file') 142 | 143 | vmin=0 144 | if map_by_size: vmax = largest + int(colourmap_soften) 145 | else: vmax = deepest + int(colourmap_soften) 146 | 147 | if map_by_size: 148 | for file, size in files.items(): 149 | path = file 150 | while len(path.split("/")) > base_depth: 151 | if G.has_edge(os.path.dirname(path), path): 152 | edge_sizes[(os.path.dirname(path), path)]= int(edge_sizes[(os.path.dirname(path), path)])+int(size) 153 | path = os.path.dirname(path) 154 | new_size = int(node_sizes[path])+int(size) 155 | if new_size > largest and path != directory: 156 | largest = new_size # 157 | if G.has_node(path): 158 | node_sizes[path]= new_size 159 | 160 | for node, depth in node_depths.items(): 161 | if map_by_size: var = node_sizes[node] 162 | else: var = depth 163 | if node_colourmap: node_colour = [var] 164 | if file_node_colourmap: file_node_colour = [var] 165 | if is_file[node]=="False": # is dir 166 | nx.draw_networkx_nodes(G, pos, nodelist=[node], node_size=node_size, node_color=node_colour, cmap=node_colourmap, vmin=vmin, vmax=vmax) 167 | else: # is file 168 | nx.draw_networkx_nodes(G, pos, nodelist=[node], node_size=file_node_size, node_color=file_node_colour, cmap=file_node_colourmap, vmin=vmin, vmax=vmax) 169 | #edges 170 | for edge, depth in edge_depths.items(): 171 | if map_by_size: var = node_sizes[edge[1]] 172 | else: var = depth*3 173 | new_width=(edge_width- depth*edge_width_reduce) 174 | if edge_colourmap: edge_colour = [var] 175 | nx.draw_networkx_edges(G, pos, edgelist=[edge], edge_color=edge_colour, width=new_width, edge_cmap=edge_colourmap, style=edge_style, edge_vmin=vmin,edge_vmax=vmax, alpha=1.0) 176 | #labels 177 | for node, label in node_labels.items(): 178 | if map_by_size: 179 | new_label_size = int( int(label_size) * (node_sizes[node] / largest) ) 180 | new_label_alpha = label_alpha * (node_sizes[node] / largest) 181 | else: # map by depth 182 | new_label_size = int(int(label_size) - (int(node_depths[node]) * int(label_size_reduce))) 183 | new_label_alpha = label_alpha / (label_alpha_reduce*int(node_depths[node]) + 1) 184 | nx.draw_networkx_labels(G, pos, labels={node:label} , font_color=label_colour, font_size=new_label_size, alpha=new_label_alpha) 185 | #if int(node_size[node]) > largest/20: nx.draw_networkx_labels(G, pos, labels={node:node.split("/")[-1]} , font_color=label_colour, font_size=new_label_size, alpha=new_label_alpha) 186 | 187 | 188 | plt.axis('off') 189 | #plt.axis('equal') 190 | 191 | if image: fig.set_facecolor('#00000000') # for transparency 192 | else: fig.set_facecolor(background_colour) 193 | 194 | # SAVE 195 | print("Saving File ...") 196 | plt.savefig(output, facecolor=fig.get_facecolor() ) 197 | plt.close() 198 | 199 | # depends on imagemagick 200 | if image is not None: 201 | print("Overlaying Over Image ...") 202 | out = subprocess.call(['composite', '-blend', str(opacity), output, image, output]) 203 | # depends on feh 204 | if set_wallpaper: 205 | print("Setting Wallpaper ...") 206 | out = subprocess.call(['feh', '--bg-scale', output]) 207 | print("Done!") 208 | 209 | 210 | #if __name__ == '__main__': 211 | # main() 212 | --------------------------------------------------------------------------------