├── 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 |
--------------------------------------------------------------------------------