Desktop stacks for Budgie/Nemo?

Already tried that before installing

python-pathlib2 (2.3.5-1ubuntu1)
python-scandir (1.10.0-2ubuntu3)
python-six (1.14.0-2)
python3-more-itertools (4.2.0-1build1)
python3-zipp (1.0.0-1)
pypy (7.3.1+dfsg-2)
pypy-lib (7.3.1+dfsg-2)
pypy-pathlib2 (2.3.5-1ubuntu1)
pypy-scandir (1.10.0-2ubuntu3)
pypy-six (1.14.0-2)

and had the same error.

Now that all these things are installed, I can see one difference :
⋅ the Junk File Organizer now works
⋅ whereas the Stacks still throw errors.

django@ASGARD:/media/DATA/coeurnoir/Bureau$ python3 stacks.py --stack
Traceback (most recent call last):
  File "stacks.py", line 110, in <module>
    stack()
  File "stacks.py", line 86, in stack
    if get_file_type(file, file_type_by_extension):
  File "stacks.py", line 73, in get_file_type
    if pathlib.Path(file).suffix.lower() in extension:
NameError: name 'pathlib' is not defined
django@ASGARD:/media/DATA/coeurnoir/Bureau$

you have installed python-2 stuff…
I’ve added some things related to pathlib searching through synaptic.
Many packages related to python2 were already installed on my system - not manually, not by me ( maybe as dependencies of some app’ ? ).
I really wonder here what is the default situation in 20.04 ?

Ok.

Get back to « Stacks » original script with that only one modification [ ~/Bureau instead of ~/Desktop ] which is mandatory.

And now it works, with
python3 stacks.py --stack
and
python3 stacks.py --unstack

So for Stacks, something from the latest installed packages is needed ( both *-pathlib2 I guess ), and be launched using python3 …

Now I don’t understand : from reading links provided in that discussion, pathlib is supposed to be part of Python 3. So why did I first need to install anything for pathlib to work for both of those scripts ?

Idea, silly or not : replacing lines 67-68

def get_desktop_path():
    return os.path.expanduser("~/Bureau")

by something like

def get_desktop_path():
    return os.path.expanduser($XDG_DESKTOP_DIR)

bet it can’t be that simple ?

trying it, reporting back in a moment – still a failure:

2011-iMac:~ xxxxx$ cd ‘/Users/xxxxxg/Desktop/’ && ‘/usr/local/bin/python3’ ‘/Users/xxxxx/Desktop/pystacks01.py’ && echo Exit status: $? && exit 1
File “/Users/xxxxx/Desktop/pystacks01.py”, line 68
return os.path.expanduser($XDG_DESKTOP_DIR)
^
SyntaxError: invalid syntax

my coworker will be back on monday, I’ve already let him know what we are attempting here, with a link to see. Hopefully he’ll be clever about this and rework it if we can’t sort it by then.

Kinda complex way, but if you are willing, this might work… (Can’t test because I am using English language install, but supposedly it will work for other languages):

def get_desktop_path():
    return os.path.expanduser(subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode("utf-8")[:-1])

I think the os.path.expanduser is unnecessary this way. You could probably even just make it:

def get_desktop_path():
    return subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode("utf-8")[:-1]

But either way, you will have to add “import subprocess” to the top. Mine looks like:

#!/usr/bin/env python3
import os
import pathlib
import subprocess
from sys import argv

(I also change the first line to python3 that way I can just make the script executable and run it without specifying which python to use at the command line)

Hope this is of some use.

trying this as well right now. I added import subprocess

still fails, but differently?:

cd ‘/Users/xxx/Desktop/’ && ‘/usr/local/bin/python3’ ‘/Users/xxx/Desktop/pystacks.py’ && echo Exit status: ? && exit 1 2011-iMac:~ xxx cd ‘/Users/xxx/Desktop/’ && ‘/usr/local/bin/python3’ ‘/Users/xxx/Desktop/pystacks.py’ && echo Exit status: $? && exit 1
Traceback (most recent call last):
File “/Users/xxx/Desktop/pystacks.py”, line 109, in
os.chdir(get_desktop_path())
File “/Users/xxx/Desktop/pystacks.py”, line 69, in get_desktop_path
return subprocess.check_output([‘xdg-user-dir’, ‘DESKTOP’]).decode(“utf-8”)[:-1]
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/subprocess.py”, line 420, in check_output
return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/subprocess.py”, line 501, in run
with Popen(*popenargs, **kwargs) as process:
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/subprocess.py”, line 947, in init
self._execute_child(args, executable, preexec_fn, close_fds,
File “/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/subprocess.py”, line 1819, in _execute_child
raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'xdg-user-dir’

I don’t even know what I’m looking at here. Is this a list of all the things that didn’t work, or a list of the processes it got thru before finally stopping for an error?

From the looks of it, it is saying that it can’t find the “xdg-user-dir” command, should be there by default on Ubuntu Budgie…
You can try the full path instead (in fact its good practice to):

def get_desktop_path():
    return subprocess.check_output(['/usr/bin/xdg-user-dir', 'DESKTOP']).decode("utf-8")[:-1]

My change was more of a fix that I think @Coeur-Noir was looking for, to make it so the stacks.py would work no matter what language you’re using (i.e. its ~/Bureau instead of ~/Desktop in French I am assuming)

The issue you are running into, I am not sure what is happening. The paths that I am seeing in your output looks odd to me…

Yes, exactly that !
A path for ~/Desktop exists only on a system set in English so it seemed weird to me to « hardcode » such a specific path.

One may want to have something like $PWD instead of DESKTOP in order to play that script on any folder ( with caution ) ?


test

So I changed first 5 lines to

#!/usr/bin/env python3
import os
import pathlib
import subprocess
from sys import argv

and lines 68-70 to

def get_desktop_path():
    # return os.path.expanduser("~/Bureau")
    return subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode("utf-8")[:-1]

and the result is :

django@ASGARD:/media/DATA/coeurnoir/Bureau$ python stacks.py --stack
  File "stacks.py", line 101
    print(f"{folder_name} not found.")
                                    ^
SyntaxError: invalid syntax
django@ASGARD:/media/DATA/coeurnoir/Bureau$ python3 stacks.py --stack
django@ASGARD:/media/DATA/coeurnoir/Bureau$

In other word it works but I still need to mention python3 for launching, if only python we get back to the first error I’d had.


other enhancements ?

⋅ regarding language and translation, we have the same kind of problem with the names for created folders ( Pictures → Images → Bilder → Immagini… )

⋅ file types to be included in each folder. « Stacks » was aimed at Apple, so some file types may actually be useless ( or rare ) on a Linux Desktop ?

⋅ I’m pretty sure those « families » of types are already defined somewhere in the system ( /usr/share/applications/mimeinfo.cache ? Or through categories in *.desktop files ?). Can’t the script extract them / concatenate them for actually reflecting user’s context instead of being « hardcoded » ?

⋅ as is, for file type not recognized by the script, it let them where they are. One might expect an « Other » folder.

⋅ a date-stamped name for folders might be a nice option ?

Sorry, what i meant was just make stacks.py executable, then you can just run ./stacks.py without python at all…

sam@budgie:~$ ./stacks.py --stack
sam@budgie:~$ ./stacks.py --unstack
Audio not found.
Source codes not found.
Apps not found.
Archives not found.
Adobe files not found.
sam@budgie:~$

My bad… I could have thought of it myself, sorry.
And indeed

django@ASGARD:/media/DATA/coeurnoir/Bureau$ ./stacks.py --stack
django@ASGARD:/media/DATA/coeurnoir/Bureau$ ./stacks.py --unstack
Videos not found.
Audio not found.
Documents not found.
Apps not found.
Archives not found.
Adobe files not found.
django@ASGARD:/media/DATA/coeurnoir/Bureau$ 

Coeur-Noir I feel like I missed a step there. You just made it executable? Explain for the non-programmers in the room (like myself) s’il vous plait.

AND: There has been a lot of aller et retour I’ve been trying to play along and change the code at home to keep up. Is there a final python code that seems to work now? Desktop vs Bureau?

You have lots of good suggestions, but not sure if I am the one for that job. Hopefully someone is… but this suggestion maybe I can do, if you want to try. Consider it very experimental. It will add the non-recognized types to an Other folder, with the exception of “.desktop” files (because I would assume its better to have those remain, but that can be changed easy enough by editing the IGNORE_LIST if you would rather them be stacked too)

Also, far from a great solution, but at the top of the script you can easily edit what you want the folder names to be. Not an alternative to proper translation, but maybe an acceptable workaround in the meantime.

https://github.com/samlane-ma/stacks-for-linux

Also following @fossfreedom’s suggestion, which seems to work very well so far… If stacks.py and both nemo_action files are moved to the “nemo/actions/” folder, you can do this easy with a right click on the desktop, as can be seen here: https://raw.githubusercontent.com/samlane-ma/stacks-for-linux/main/stacks.gif

Far from perfect, but maybe a start for someone?

1 Like

They are just suggestions, ideas, for anyone. Not a roadmap nor demand :wink:
I’m just trying to understand the logic behind, and you help a lot !

I’ll try to tidy a bit the types for each folder. Missing things like .xcf, .sla…

Maybe mention now that script can no longer works in Windows/Apple I guess ( because of the xdg-user-dir ), Linux only.

The one posted by @samlane on Github should work.

Or here is the one I’ve tried so far :

#!/usr/bin/env python3
import os
import pathlib
import subprocess
from sys import argv

file_type_by_extension = {
    'Images': [
        '.jpg', '.jpeg', '.jfif', '.jpe', '.jif', '.jfi',      
        '.jp2', '.j2k', '.jpf', '.jpx', 'jpm', 'mj2',          
        '.tiff', '.tif',                                       
        '.gif',                                                
        '.bmp', '.dib',                                        
        '.png',                                                
        '.pbm', '.pgm', '.ppm', '.pnm',                        
        '.webp',                                               
        '.heif', '.heic',                                      
        '.3fr', '.ari', '.arw', '.srf', '.sr2', '.bay',        
        '.crw', '.cr2', '.cap', '.iiq', '.eip', '.dcs',        
        '.dcr', '.drf', '.k25', '.kdc', '.dng', '.erf',        
        '.fff', '.mef', '.mos', '.mrw', '.nef', '.nrw',        
        '.orf', '.ptx', '.pef', '.pxn', '.r3d', '.raf',        
        '.raw', '.rw2', '.rw1', '.rwz', '.x3f'                 
    ],
    'Videos': [
        '.webm', '.mkv', '.flv', '.vob', '.ogv' '.ogg',
        '.drc', '.gifv', '.mng', '.avi', '.mts', '.m2ts',
        '.mov', '.qt', '.wmv', '.yuv', '.rm', '.rmvb',
        '.asf', '.amv', '.mp4', '.m4p', '.m4v', '.mpg',
        '.mp2', '.mpeg', '.mpe', '.mpv', '.m2v', '.m4v',
        '.svi', '.3gp', '.3g2', '.mxf', '.roq', '.nsv',
        '.fl4', '.f4p', '.f4v', '.f4a', '.f4b'
    ],
    'Audio': [
        '.3gp', '.aa', '.aac', '.aax', '.act', '.aiff',
        '.amr', '.ape', '.au', '.awb', '.dct', '.dss',
        '.dvf', '.flac', '.gsm', '.iklax', '.ivs', '.m4a',
        '.m4b', '.m4p', '.mmf', '.mp3', '.mpc', '.msv',
        '.nmf', '.nsf', '.ogg', '.oga', '.mogg', '.opus',
        '.ra', '.rm', '.tta', '.vox', '.wav', '.wma',
        '.wv', '.webm', '.8svx'
    ],
    'Source codes': [
        '.C', '.cc', '.cpp', '.cxx', '.c++', '.h', '.hh',
        '.hpp', '.hxx', '.h++', '.py', '.pyc', '.c',
        '.java', '.class', '.bash', '.sh', '.bat', '.ps1',
        '.perl', '.asm', '.S', '.js', '.html', '.css',
        '.scss', '.ts', '.go', '.rs', '.json', '.bin'
    ],
    'Documents': [
        '.txt', '.doc', '.docx', '.pptx', '.ppt', '.xls',
        '.xlsx', '.md', '.pdf', '.odt', '.ods', '.odp',
        '.odf', '.odb'
    ],
    'Apps': [
        '.exe', '.elf', '.lnk', '.msi'
    ],
    'Archives': [
        '.zip', '.rar', '.7z', '.gz', '.bz2', '.Z', '.lzma',
        '.tar', '.xz'
    ],
    'Adobe files': [
        '.psd', '.aep', '.prproj', '.ai', '.xd'
    ]
}


def get_desktop_path():
    # return os.path.expanduser("~/Bureau")
    return subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode("utf-8")[:-1]


def get_file_type(file, types):  # types = file_type_by_extension
    for type, extension in types.items():
        if pathlib.Path(file).suffix.lower() in extension:
            return type


def create_folders(types):     # types = file_type_by_extension
    for type_ in types:
        os.makedirs(type_, exist_ok=True)


def stack():
    create_folders(file_type_by_extension)
    files = os.listdir()
    for file in files:
        if get_file_type(file, file_type_by_extension):
            os.rename(file, os.path.join(get_file_type(file,
                                                       file_type_by_extension), file))
    for folder in file_type_by_extension:
        if not os.listdir(folder):
            os.removedirs(folder)


def unstack():
    for folder_name in file_type_by_extension:
        try:
            os.chdir(folder_name)
        except FileNotFoundError:
            print(f"{folder_name} not found.")
        else:
            for file in os.listdir():
                os.rename(file, os.path.join('..', file))
            os.chdir('..')
            os.removedirs(folder_name)


if __name__ == "__main__":
    os.chdir(get_desktop_path())
    if "--stack" in argv:
        stack()
    elif "--unstack" in argv:
        unstack()
    else:
        print("Error: Argument missing.")

oooh. Okay, no windows or macOS because of xdg-user-dir. This will require to do this NOT during my work day then :frowning:

I modified it a bit… now what it will do is try to run xdg-user-dir, but if it can’t, it will fall back to the original hard-coded method (i hope).

def get_desktop_path():
    try:
        desktop = subprocess.check_output(['/usr/bin/xdg-user-dir', 'DESKTOP']).decode("utf-8")[:-1]
    except:
        desktop = os.path.expanduser("~/Desktop")
    return desktop
1 Like

Yep that will work - just to say under GTK better and safer to use the GLIB api https://askubuntu.com/a/457584/14356 - this will cope with translation issues as well.

Good implementation! Sorry, I see this now but really nice job! :smiley:

Thanks, appreciate the kind words!

I actually made more changes to this, trying to keep a bit in line with suggestions of @Coeur-Noir in regards to mime types. The roadblock, unfortunately, is that while audio, video, and images can easily be separated this way, things like, for a random example, archives and pdf files fall under the broad “application” type, even though they are two completely different types. So it is hard to categorize them without still falling back to some detailed list about what goes in which folder.

The new way I played with is grouping the remaining files by the application associated with them. So for example if you use LibreOffice to open spreadsheets, they will all be put in a folder called “LibreOffice Calc”. Drawback of course is LibreOffice Writer files will be in a separate folder.

So its not perfect, but it might be a more desired alternative for some?

This change also bases the folder names for the images, audio, and pictures using the method suggested by @fossfreedom, therefore providing a sort of built-in translation of these folders, instead of using hard-coded English names.

Also, more importantly, let’s say you have a file named “image.jpg” on the desktop, and in your Desktop/Pictures folder, you have a file with the same name. This will no longer overwrite that file if you group/ungroup the files. Instead, you will end up with "image(1).jpg or image(2).jpg and so on…

I waited a while before posting these changes because the original author did not include a license in his repo, so it wouldn’t have been right for me to borrow his work. However, he has officially given permission, so all is good now.

New version is here if you want to try:
(I’d probably suggest removing the other version from ~/.local/share/nemo/actions if you install this one)