Compare commits
26 Commits
main
...
ueberzugpp
Author | SHA1 | Date | |
---|---|---|---|
d0ac564cee | |||
53c3a55e07 | |||
1ae88d2182 | |||
e0ce0d1b1d | |||
e88cf6b98d | |||
5775502dc1 | |||
c91b3a3f93 | |||
ea80175dc7 | |||
5f9d528f3e | |||
39d7471dc8 | |||
f5344f606e | |||
da43eae362 | |||
fdf3e0fb50 | |||
5f200626b8 | |||
1fd5e820f4 | |||
|
429a7be16a | ||
|
75bc8efbb8 | ||
|
6be0e503fb | ||
|
6b48fbdec1 | ||
|
6795766160 | ||
|
bb049d5e92 | ||
|
86ab3a0596 | ||
|
59159e4ad2 | ||
|
8f9603976e | ||
|
4d86c53fe4 | ||
|
5ee8743ed2 |
39
README.md
Normal file → Executable file
39
README.md
Normal file → Executable file
@ -1,6 +1,10 @@
|
|||||||
# imgsort - Image Sorter
|
# imgsort - Image Sorter
|
||||||
This is a python program that lets you easily sort images from one directory into other directories.
|
This is a python program for Linux that lets you easily sort images from one directory into other directories.
|
||||||
For example, you could go through your phone's camera folder and sort the images into different folders, like *Family*, *Landscapes*, *Friends* etc.
|
It lets you go through a folder of images and simply move them using a single key press, which you define at program startup.
|
||||||
|
This is very useful when you want to sort your phone's camera folder or messenger media folders.
|
||||||
|
For example, you could quickly go through your WhatsApp media (after copying it to your pc) and sort the images into different directories like *Selfies*, *Landscapes*, *Friends* etc.
|
||||||
|
|
||||||
|
<img src="imgsort-example.jpg" width="70%" style="margin-left: auto; margin-right: auto;" />
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
1. Navigate to the folder containing the images and run "imgsort".
|
1. Navigate to the folder containing the images and run "imgsort".
|
||||||
@ -12,24 +16,41 @@ imgsort
|
|||||||
For example, you could use:
|
For example, you could use:
|
||||||
- `f` = `~/Pictures/Family`
|
- `f` = `~/Pictures/Family`
|
||||||
- `v` = `~/Pictures/Vacation_2019`
|
- `v` = `~/Pictures/Vacation_2019`
|
||||||
- `o` = `~/Pictures/Other`
|
- `O` = `~/Pictures/Other`
|
||||||
|
|
||||||
Note that `s`, `u` and `q` are reserved for *skip*, *undo* and *quit*, but you can use `S`, `U` and `Q` instead.
|
Note that `s`, `u`, `o` and `q` are reserved for *skip*, *undo*, *open* and *quit*, but you can use `S`, `U` and `Q` instead.
|
||||||
3. Save the config if you might want to use it again. The config file will be stored in `~/.config/imgsort`.
|
3. Save the config if you might want to use it again. The config file will be stored in `$XDG_CONFIG_DIR` or `~/.config/imgsort`.
|
||||||
4. Enjoy the slideshow!
|
4. Enjoy the slideshow!
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Clone this repository and install it using python-pip.
|
Clone this repository and install it using python-pip.
|
||||||
pip should also install https://github.com/seebye/ueberzug, which lets you view images in a terminal.
|
This project depends on ueberzug to display the images in the terminal.
|
||||||
|
The original ueberzug is no longer maintained, but there is [a continuation](https://github.com/ueber-devel/ueberzug/) as well as a [new C++ alternative](https://github.com/jstkdng/ueberzugpp) available.
|
||||||
|
You need to manually install one of them and then choose the corresponding `imgsort` branch.
|
||||||
|
I would recommend the `ueberzugpp` as it also works on Wayland.
|
||||||
|
|
||||||
|
For the version supporting the original **ueberzug**:
|
||||||
```shell
|
```shell
|
||||||
cd ~/Downloads
|
|
||||||
git clone https://github.com/MatthiasQuintern/imgsort.git
|
git clone https://github.com/MatthiasQuintern/imgsort.git
|
||||||
cd imgsort
|
cd imgsort
|
||||||
python3 -m pip install .
|
python3 -m pip install .
|
||||||
```
|
```
|
||||||
You can also install it system-wide using `sudo python3 -m pip install.`
|
For the version supporting the new **ueberzug++**:
|
||||||
|
```shell
|
||||||
|
git clone --branch ueberzugpp https://github.com/MatthiasQuintern/imgsort.git
|
||||||
|
cd imgsort
|
||||||
|
python3 -m pip install .
|
||||||
|
```
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
### 1.2
|
||||||
|
#### 1.2.1
|
||||||
|
- Refactored configuration management
|
||||||
|
#### 1.2.0
|
||||||
|
- Support ueberzugpp
|
||||||
|
- Added option to open file with `xdg-open`
|
||||||
|
- Use pyproject.toml for installation
|
||||||
|
|
||||||
### 1.1
|
### 1.1
|
||||||
- Terminal does not break anymore when program exits
|
- Terminal does not break anymore when program exits
|
||||||
- Todo-Images are now sorted by filename
|
- Todo-Images are now sorted by filename
|
||||||
@ -37,5 +58,5 @@ You can also install it system-wide using `sudo python3 -m pip install.`
|
|||||||
### 1.0
|
### 1.0
|
||||||
- Initial Release
|
- Initial Release
|
||||||
|
|
||||||
## Importand Notice:
|
## Important Notice:
|
||||||
This software comes with no warranty!
|
This software comes with no warranty!
|
||||||
|
BIN
imgsort-example.jpg
Normal file
BIN
imgsort-example.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
Binary file not shown.
@ -1,101 +1,169 @@
|
|||||||
from os import path, getcwd, listdir, mkdir, makedirs, rename
|
import enum
|
||||||
|
from os import EX_CANTCREAT, path, getcwd, listdir, rename
|
||||||
|
from sys import exit
|
||||||
import re
|
import re
|
||||||
|
|
||||||
def read_config(filepath):
|
from .globals import version, settings_map
|
||||||
if not path.isfile(filepath): return False
|
from .globals import warning, error, user_error, info, create_dir
|
||||||
|
|
||||||
file = open(filepath, 'r')
|
|
||||||
keys = {}
|
|
||||||
for line in file.readlines():
|
|
||||||
line = line.replace("\n", "")
|
|
||||||
match = re.match(r". = /?([a-z-A-ZöÖäÄüÜ0-9/: _-]+/)*[a-zA-ZöÖäÄüÜ0-9/: _-]+/?", line)
|
|
||||||
if match:
|
|
||||||
key, value = line.split(" = ")
|
|
||||||
keys[key] = value
|
|
||||||
return keys
|
|
||||||
|
|
||||||
def write_config(filepath, keys):
|
class ConfigManager():
|
||||||
file = open(filepath, 'w')
|
"""Manage config files for imgsort"""
|
||||||
file.write("Config written by imgsort.\n")
|
def __init__(self, config_dir: str):
|
||||||
for k, v in keys.items() :
|
"""TODO: to be defined.
|
||||||
file.write(f"{k} = {v}\n")
|
|
||||||
|
|
||||||
def create_config():
|
@param config_path TODO
|
||||||
keys = {}
|
|
||||||
print(
|
"""
|
||||||
"""
|
self._config_dir = config_dir
|
||||||
|
if not path.isdir(self._config_dir):
|
||||||
|
if path.exists(self._config_dir):
|
||||||
|
error(f"Config '{self._config_dir}' exists but is not a directory.")
|
||||||
|
info(f"Creating config dir '{self._config_dir}' since it does not exist")
|
||||||
|
try:
|
||||||
|
create_dir(self._config_dir)
|
||||||
|
except PermissionError as e:
|
||||||
|
error(f"Could not create '{self._config_dir}': PermissionError: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Could not create '{self._config_dir}': {e}")
|
||||||
|
|
||||||
|
self._configs = [ e for e in listdir(self._config_dir) if path.isfile(path.normpath(self._config_dir + "/" + e)) and e.endswith(".conf") ]
|
||||||
|
self._configs.sort()
|
||||||
|
|
||||||
|
def present_config_selection(self, root_directory="."):
|
||||||
|
"""
|
||||||
|
Returns to path to an existing config or False if a new config should be created
|
||||||
|
"""
|
||||||
|
# get configs
|
||||||
|
if len(self._configs) == 0:
|
||||||
|
info(f"No config valid file found in '{self._config_dir}'")
|
||||||
|
return self.create_config(root_directory)
|
||||||
|
|
||||||
|
print(" 0: create new configuration")
|
||||||
|
for i, c in enumerate(self._configs):
|
||||||
|
print(f"{i+1:2}: {c}")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
choice = input("Please select a config: ")
|
||||||
|
try:
|
||||||
|
choice = int(choice)
|
||||||
|
except ValueError:
|
||||||
|
user_error(f"Invalid choice: '{choice}'. Choice must be a number between 0 and {len(self._configs)}")
|
||||||
|
continue
|
||||||
|
if not 0 <= choice <= len(self._configs):
|
||||||
|
user_error(f"Invalid choice: '{choice}'. Choice must be a number between 0 and {len(self._configs)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if choice == 0:
|
||||||
|
return self.create_config(root_directory)
|
||||||
|
return self.load_config(self._configs[choice-1], root_directory)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_name(self, config_name: str):
|
||||||
|
return path.normpath(self._config_dir + "/" + config_name.removesuffix(".conf") + ".conf")
|
||||||
|
|
||||||
|
|
||||||
|
def write_config(self, config_name: str, keys: dict[str,str]):
|
||||||
|
file = open(path.normpath(self._config_dir + "/" + config_name), 'w')
|
||||||
|
file.write(f"# Config written by imgsort {version}\n")
|
||||||
|
for k, v in keys.items() :
|
||||||
|
file.write(f"{k} = {v}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(self, config_name: str, root_directory="."):
|
||||||
|
"""
|
||||||
|
@param root_directory Make all relative paths relative to this one
|
||||||
|
"""
|
||||||
|
if type(config_name) != str:
|
||||||
|
error(f"load config got wrong type: '{type(config_name)}'")
|
||||||
|
config_file = self._make_name(config_name)
|
||||||
|
if not path.isfile(config_file):
|
||||||
|
error(f"File '{config_file}' does not exist")
|
||||||
|
try:
|
||||||
|
file = open(config_file, 'r')
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Could not open file '{config_file}': {e}")
|
||||||
|
|
||||||
|
keys: dict[str, str] = {}
|
||||||
|
for i, line in enumerate(file.readlines()):
|
||||||
|
line = line.replace("\n", "")
|
||||||
|
match = re.fullmatch(r". = [^*?<>|]+", line)
|
||||||
|
if match:
|
||||||
|
key, value = line.split(" = ")
|
||||||
|
keys[key] = value
|
||||||
|
elif not line[0] == "#":
|
||||||
|
warning(f"In config file '{config_file}': Invalid line ({i+1}): '{line}'")
|
||||||
|
self.validate_config(keys, root_directory)
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def create_config(self, root_directory="."):
|
||||||
|
keys: dict[str, str] = {}
|
||||||
|
print(
|
||||||
|
f"""
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
Creating a new config
|
Creating a new config
|
||||||
|
You can now map keys to directories.
|
||||||
|
The key must be one single letter, a single digit number or some other keyboard key like .-#+&/ ...
|
||||||
|
The key can not be one of '{' '.join(settings_map.keys())}'.
|
||||||
|
The directory must be a valid path to a directory, but is does not have to exist.
|
||||||
|
You can use an absolute path (starting with '/', not '~') or a relative path (from here).
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
Please enter at least one key and one directory.
|
|
||||||
The key must be one single letter, a single digit number or some other keyboard key like .-#+&/ ...
|
|
||||||
The key can not be 'q', 's' or 'u'.
|
|
||||||
The directory must be a valid path to a directory, but is does not have to exist.
|
|
||||||
You can use an absolute path (starting with '/') or a relative path (from here).
|
|
||||||
Do not use '~'!
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
done = False
|
while True:
|
||||||
while not done:
|
# ask for key
|
||||||
|
key = input("Please enter a key or 'q' when you are done: ")
|
||||||
|
if (len(key) != 1):
|
||||||
|
user_error(f"Invalid key: '{key}' has a length other than 1")
|
||||||
|
continue
|
||||||
|
# if done
|
||||||
|
elif key == 'q':
|
||||||
|
if len(keys) == 0:
|
||||||
|
warning(f"No keys were mapped - exiting")
|
||||||
|
exit(0)
|
||||||
|
save = input(f"\nDo you want to save the config to {self._config_dir}/<name>.conf?\nType a name to save the config or type 'q' to not save the config: ")
|
||||||
|
if save != 'q':
|
||||||
|
self.write_config(save + ".conf", keys)
|
||||||
|
break
|
||||||
|
elif key in settings_map.keys():
|
||||||
|
user_error(f"Invalid key: '{key}' is reserved and can not be mapped")
|
||||||
|
continue
|
||||||
|
|
||||||
# ask for key
|
# ask for directory
|
||||||
key = input("Please enter a key or 'q' when you are done: ")
|
directory = input("Please enter the directory/path: ")
|
||||||
if (len(key) != 1):
|
# match = re.match(r"/?([a-z-A-ZöÖäÄüÜ0-9/: _\-]+/)*[a-z-A-ZöÖäÄüÜ0-9/: _\-]+/?", directory)
|
||||||
print("Invalid key: " + key)
|
INVALID_PATH_CHARS = r":*?<>|"
|
||||||
continue
|
if any(c in INVALID_PATH_CHARS for c in directory):
|
||||||
# if done
|
user_error(f"Invalid directory path: '{directory}' contains at least one of '{INVALID_PATH_CHARS}'")
|
||||||
elif key == 'q':
|
continue
|
||||||
save = input("\nDo you want to save the config to ~/.config/imgsort/<name>.conf?\nType a name to save the config or type 'q' to not save the config: ")
|
keys[key] = directory
|
||||||
if not save == 'q':
|
print(f"Added: {key}: '{directory}'\n")
|
||||||
config_path = path.expanduser("~") + "/.config/imgsort"
|
|
||||||
if not path.isdir(config_path):
|
|
||||||
mkdir(config_path)
|
|
||||||
|
|
||||||
write_config(path.normpath(config_path + "/" + save + ".conf"), keys)
|
self.validate_config(keys, root_directory)
|
||||||
done = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
# ask for directory
|
return keys
|
||||||
directory = input("Please enter the directory path: ")
|
|
||||||
match = re.match(r"/?([a-z-A-ZöÖäÄüÜ0-9/: _\-]+/)*[a-z-A-ZöÖäÄüÜ0-9/: _\-]+/?", directory)
|
|
||||||
if not match:
|
|
||||||
print("Invalid directory path: " + directory)
|
|
||||||
continue
|
|
||||||
|
|
||||||
keys[key] = directory
|
|
||||||
print(f"Added: ({key}: {directory})\n")
|
|
||||||
|
|
||||||
return keys
|
|
||||||
|
|
||||||
|
|
||||||
def select_config():
|
def validate_config(self, keys, root_directory):
|
||||||
"""
|
"""
|
||||||
Returns to path to an existing config or False if a new config should be created
|
Create the directories that dont exist.
|
||||||
"""
|
"""
|
||||||
# get configs
|
missing = []
|
||||||
config_path = path.expanduser("~") + "/.config/imgsort"
|
for k, d in keys.items():
|
||||||
if not path.isdir(config_path) or len(listdir(config_path)) == 0:
|
d = path.expanduser(d)
|
||||||
return False
|
if not path.isabs(d):
|
||||||
|
d = path.normpath(root_directory + "/" + d)
|
||||||
configs = {}
|
keys[k] = d
|
||||||
|
if not path.isdir(d):
|
||||||
|
missing.append(d)
|
||||||
|
if len(missing) == 0: return
|
||||||
|
print(f"The following directories do not exist:")
|
||||||
|
for d in missing: print(f"\t{d}")
|
||||||
|
decision = input(f"Create the ({len(missing)}) missing directories? y/*: ")
|
||||||
|
if (decision == "y"):
|
||||||
|
for d in missing:
|
||||||
|
create_dir(d)
|
||||||
|
else:
|
||||||
|
error("Exiting - can not use non-existing directories.")
|
||||||
|
|
||||||
i = 1
|
|
||||||
for file in listdir(config_path):
|
|
||||||
if not re.match(r"[a-zA-ZöÖäÄüÜ0-9_\- ]+\.conf", file): continue
|
|
||||||
|
|
||||||
configs[str(i)] = file
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# print configs
|
|
||||||
print("0: Create new config")
|
|
||||||
for n, conf in configs.items():
|
|
||||||
print(f"{n}: {conf}")
|
|
||||||
|
|
||||||
choice = input("Please select a config: ")
|
|
||||||
if choice == "0": return False
|
|
||||||
elif choice in configs:
|
|
||||||
return path.normpath(config_path + "/" + configs[choice])
|
|
||||||
else:
|
|
||||||
print("Invalid choice - creating new config")
|
|
||||||
return False
|
|
||||||
|
32
imgsort/globals.py
Normal file
32
imgsort/globals.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version = "1.2.1"
|
||||||
|
fullversion = f"{version}"
|
||||||
|
|
||||||
|
settings_map = {
|
||||||
|
"q": "quit",
|
||||||
|
"s": "skip",
|
||||||
|
"u": "undo",
|
||||||
|
"o": "open"
|
||||||
|
}
|
||||||
|
|
||||||
|
from os import makedirs
|
||||||
|
|
||||||
|
def error(*args, exitcode=2, **kwargs):
|
||||||
|
print("\033[31mError: \033[0m", *args, **kwargs)
|
||||||
|
exit(exitcode)
|
||||||
|
|
||||||
|
def user_error(*args, **kwargs):
|
||||||
|
print("\033[31mError: \033[0m", *args, **kwargs)
|
||||||
|
|
||||||
|
def warning(*args, **kwargs):
|
||||||
|
print("\033[33mWarning: \033[0m", *args, **kwargs)
|
||||||
|
|
||||||
|
def info(*args, **kwargs):
|
||||||
|
print("\033[34mInfo: \033[0m", *args, **kwargs)
|
||||||
|
|
||||||
|
def create_dir(d):
|
||||||
|
try:
|
||||||
|
makedirs(d)
|
||||||
|
except PermissionError as e:
|
||||||
|
error(f"Could not create '{d}': PermissionError: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Could not create '{d}': {e}")
|
@ -1,19 +1,23 @@
|
|||||||
#!/bin/python3
|
#!/bin/python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
import curses as c
|
import curses as c
|
||||||
import ueberzug.lib.v0 as uz
|
import os
|
||||||
|
from os import path, getcwd, listdir, makedirs, rename
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from imgsort.configs import read_config, write_config, select_config, create_config
|
if __name__ == "__main__": # make relative imports work as described here: https://peps.python.org/pep-0366/#proposed-change
|
||||||
|
if __package__ is None:
|
||||||
|
__package__ = "imgsort"
|
||||||
|
import sys
|
||||||
|
filepath = path.realpath(path.abspath(__file__))
|
||||||
|
sys.path.insert(0, path.dirname(path.dirname(filepath)))
|
||||||
|
|
||||||
from os import path, getcwd, listdir, mkdir, makedirs, rename
|
from .ueberzug import UeberzugLayer
|
||||||
|
|
||||||
from sys import argv
|
from .configs import ConfigManager
|
||||||
|
from .globals import version, fullversion, settings_map
|
||||||
settings = {
|
from .globals import warning, error, user_error, info, create_dir
|
||||||
"q": "quit",
|
|
||||||
"s": "skip",
|
|
||||||
"u": "undo",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Size settings
|
# Size settings
|
||||||
FOOTER_LEFT = 0
|
FOOTER_LEFT = 0
|
||||||
@ -27,7 +31,7 @@ CURSOR_Y = 2
|
|||||||
KEYS_BEGIN = 5
|
KEYS_BEGIN = 5
|
||||||
|
|
||||||
class Sorter:
|
class Sorter:
|
||||||
def __init__(self, wdir, canvas, config):
|
def __init__(self, wdir, config):
|
||||||
self.wd = wdir
|
self.wd = wdir
|
||||||
|
|
||||||
self.images = [] # old paths
|
self.images = [] # old paths
|
||||||
@ -37,9 +41,7 @@ class Sorter:
|
|||||||
|
|
||||||
self.keys = config
|
self.keys = config
|
||||||
|
|
||||||
self.settings = settings
|
self.settings = settings_map
|
||||||
|
|
||||||
self.validate_dirs()
|
|
||||||
|
|
||||||
# info about last action
|
# info about last action
|
||||||
self.last_dir = ""
|
self.last_dir = ""
|
||||||
@ -56,32 +58,14 @@ class Sorter:
|
|||||||
c.echo()
|
c.echo()
|
||||||
|
|
||||||
# ueberzug
|
# ueberzug
|
||||||
self.canvas = canvas
|
self._ueberzug = UeberzugLayer(pid_file="/tmp/ueberzug-imgsort.pid")
|
||||||
|
self._img_x = SIDEBAR_WIDTH + 1
|
||||||
|
self._img_y = 2
|
||||||
|
self._img_width = self.win_x - SIDEBAR_WIDTH - 1
|
||||||
|
self._img_height = self.win_y - FOOTER_HEIGHT - 2
|
||||||
|
self._img_identifier = "imgsort_preview"
|
||||||
|
|
||||||
self.placement = self.canvas.create_placement("p1", x=0, y=0, path="")
|
|
||||||
self.placement.visibility = uz.Visibility.VISIBLE
|
|
||||||
self.placement.scaler = uz.ScalerOption.FIT_CONTAIN.value
|
|
||||||
self.placement.x = SIDEBAR_WIDTH + 1
|
|
||||||
self.placement.y = 2
|
|
||||||
self.placement.width = self.win_x - SIDEBAR_WIDTH - 1
|
|
||||||
self.placement.height = self.win_y - FOOTER_HEIGHT - 2
|
|
||||||
|
|
||||||
# version
|
|
||||||
self.version = "Image Sorter 1.1"
|
|
||||||
|
|
||||||
def validate_dirs(self):
|
|
||||||
"""
|
|
||||||
Create the directories that dont exist.
|
|
||||||
"""
|
|
||||||
for d in self.keys.values():
|
|
||||||
if not path.isdir(d):
|
|
||||||
print(f"Directory '{d}' does not exist.")
|
|
||||||
decision = input(f"Create directory '{path.abspath(d)}'? y/n: ")
|
|
||||||
if (decision == "y"):
|
|
||||||
makedirs(d)
|
|
||||||
else:
|
|
||||||
print("Exiting - can not use non-existing directory.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_images(self):
|
def get_images(self):
|
||||||
@ -99,11 +83,11 @@ class Sorter:
|
|||||||
self.images.sort()
|
self.images.sort()
|
||||||
self.images_new = self.images.copy()
|
self.images_new = self.images.copy()
|
||||||
# print(self.images)
|
# print(self.images)
|
||||||
|
|
||||||
def display_image(self):
|
def display_image(self):
|
||||||
with self.canvas.lazy_drawing: # issue ueberzug command AFTER with-statement
|
self._ueberzug.display_image(self.image, x=self._img_x, y=self._img_y, max_width=self._img_width, max_height=self._img_height, identifier=self._img_identifier)
|
||||||
self.placement.path = self.image
|
self.window.addnstr(0, SIDEBAR_WIDTH + 1, self.image, self.win_x - SIDEBAR_WIDTH - 1)
|
||||||
self.window.addnstr(0, SIDEBAR_WIDTH + 1, self.placement.path, self.win_x - SIDEBAR_WIDTH - 1)
|
|
||||||
|
|
||||||
def sort(self):
|
def sort(self):
|
||||||
"""
|
"""
|
||||||
@ -117,7 +101,7 @@ class Sorter:
|
|||||||
|
|
||||||
self.pressed_key = self.window.getkey() # wait until user presses something
|
self.pressed_key = self.window.getkey() # wait until user presses something
|
||||||
|
|
||||||
# check for quit, skip or undo
|
# check for quit, skip, undo or open
|
||||||
if self.pressed_key in self.settings:
|
if self.pressed_key in self.settings:
|
||||||
if self.settings[self.pressed_key] == "quit":
|
if self.settings[self.pressed_key] == "quit":
|
||||||
self.quit(f"Key '{self.pressed_key}' pressed. Canceling image sorting")
|
self.quit(f"Key '{self.pressed_key}' pressed. Canceling image sorting")
|
||||||
@ -125,7 +109,7 @@ class Sorter:
|
|||||||
self.image_iter += 1
|
self.image_iter += 1
|
||||||
self.message = "Skipped image"
|
self.message = "Skipped image"
|
||||||
continue
|
continue
|
||||||
elif settings[self.pressed_key] == "undo":
|
elif settings_map[self.pressed_key] == "undo":
|
||||||
if self.image_iter > 0:
|
if self.image_iter > 0:
|
||||||
self.image_iter -= 1 # using last image
|
self.image_iter -= 1 # using last image
|
||||||
rename(self.images_new[self.image_iter], self.images[self.image_iter])
|
rename(self.images_new[self.image_iter], self.images[self.image_iter])
|
||||||
@ -135,6 +119,13 @@ class Sorter:
|
|||||||
else:
|
else:
|
||||||
self.message = "Nothing to undo!"
|
self.message = "Nothing to undo!"
|
||||||
continue
|
continue
|
||||||
|
elif settings_map[self.pressed_key] == "open":
|
||||||
|
try:
|
||||||
|
subprocess.run(['xdg-open', self.image], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
||||||
|
self.message = "Opening with xdg-open"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"open: Error: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
# move to folder
|
# move to folder
|
||||||
elif self.pressed_key in self.keys:
|
elif self.pressed_key in self.keys:
|
||||||
@ -143,24 +134,26 @@ class Sorter:
|
|||||||
self.images_new[self.image_iter] = new_filepath
|
self.images_new[self.image_iter] = new_filepath
|
||||||
self.message = f"Moved image to {self.keys[self.pressed_key]}"
|
self.message = f"Moved image to {self.keys[self.pressed_key]}"
|
||||||
else:
|
else:
|
||||||
self.message = f"ERROR: Failed to move '{self.image}' to '{keys[self.pressed_key]}'."
|
self.message = f"ERROR: Failed to move '{self.image}' to '{self.keys[self.pressed_key]}'."
|
||||||
self.image_iter += 1
|
self.image_iter += 1
|
||||||
|
|
||||||
self.quit("All done!")
|
self.quit("All done!")
|
||||||
|
|
||||||
def print_window(self):
|
def print_window(self):
|
||||||
"""
|
"""
|
||||||
Draw lines and text
|
Draw lines and text
|
||||||
"""
|
"""
|
||||||
self.window.erase()
|
self.window.erase()
|
||||||
|
self.win_y, self.win_x = self.window.getmaxyx()
|
||||||
|
|
||||||
# lines
|
# lines
|
||||||
self.window.hline(self.win_y - FOOTER_HEIGHT, FOOTER_LEFT, '=', self.win_x)
|
self.window.hline(self.win_y - FOOTER_HEIGHT, FOOTER_LEFT, '=', self.win_x)
|
||||||
self.window.vline(0, SIDEBAR_WIDTH, '|', self.win_y - FOOTER_HEIGHT + 1)
|
self.window.vline(0, SIDEBAR_WIDTH, '|', self.win_y - FOOTER_HEIGHT + 1)
|
||||||
|
|
||||||
# version
|
# version
|
||||||
x = self.win_x - len(self.version) - 1
|
version_str = f"imgsort {version}"
|
||||||
self.window.addstr(self.win_y - 1, x, self.version)
|
x = self.win_x - len(version_str) - 1
|
||||||
|
self.window.addstr(self.win_y - 1, x, version_str)
|
||||||
|
|
||||||
# wd
|
# wd
|
||||||
wdstring = f"Sorting {self.wd} - {len(self.images)} files - {len(self.images) - self.image_iter} remaining."
|
wdstring = f"Sorting {self.wd} - {len(self.images)} files - {len(self.images) - self.image_iter} remaining."
|
||||||
@ -202,6 +195,8 @@ class Sorter:
|
|||||||
for k, v in self.keys.items():
|
for k, v in self.keys.items():
|
||||||
if i >= self.win_y - KEYS_BEGIN - FOOTER_HEIGHT: # dont write into footer
|
if i >= self.win_y - KEYS_BEGIN - FOOTER_HEIGHT: # dont write into footer
|
||||||
break
|
break
|
||||||
|
# show only last part
|
||||||
|
v = v.split("/")[-1]
|
||||||
if k == self.pressed_key:
|
if k == self.pressed_key:
|
||||||
self.window.addnstr(KEYS_BEGIN + i, 0, f" {k}: {v}", SIDEBAR_WIDTH, c.A_STANDOUT)
|
self.window.addnstr(KEYS_BEGIN + i, 0, f" {k}: {v}", SIDEBAR_WIDTH, c.A_STANDOUT)
|
||||||
else:
|
else:
|
||||||
@ -209,57 +204,63 @@ class Sorter:
|
|||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
self.window.move(CURSOR_Y, CURSOR_X)
|
self.window.move(CURSOR_Y, CURSOR_X)
|
||||||
|
|
||||||
def move_file(self, file, dest):
|
def move_file(self, file, dest):
|
||||||
# if not path.isdir(dest):
|
# if not path.isdir(dest):
|
||||||
# makedirs(dest)
|
# makedirs(dest)
|
||||||
if not path.isfile(file): return False
|
if not path.isfile(file): return False
|
||||||
if not path.isdir(dest): return False
|
if not path.isdir(dest): return False
|
||||||
|
|
||||||
new_path = path.normpath(dest + '/' + path.split(file)[1])
|
new_path = path.normpath(dest + '/' + path.split(file)[1])
|
||||||
|
|
||||||
rename(file, new_path)
|
rename(file, new_path)
|
||||||
return new_path
|
return new_path
|
||||||
|
|
||||||
def quit(self, message = ""):
|
def quit(self, message = ""):
|
||||||
|
print(message)
|
||||||
|
print(f"Quitting imgsort {version}")
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
self.window.clear()
|
self.window.clear()
|
||||||
self.window.refresh()
|
self.window.refresh()
|
||||||
c.endwin()
|
c.endwin()
|
||||||
print(message)
|
|
||||||
print("Quitting " + self.version)
|
|
||||||
exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# set working directory
|
# set working directory
|
||||||
print("""
|
print(f"""
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
Image Sorter
|
Image Sorter {fullversion}
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
""")
|
""")
|
||||||
if len(argv) > 1:
|
config_dir = os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "imgsort")
|
||||||
wd = path.abspath(argv[1])
|
# check if environment variables are set and use them if they are
|
||||||
|
if 'IMGSORT_CONFIG_DIR' in os.environ: config_dir = os.environ['IMGSORT_CONFIG_DIR']
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser("imgsort")
|
||||||
|
parser.add_argument("-c", "--config", action="store", help="name of the config file in ($IMGSORT_CONFIG_DIR > $XDG_CONFIG_HOME/imgsort > ~/.config/imgsort)", default=None)
|
||||||
|
parser.add_argument("-i", "--sort-dir", action="store", help="directory in which the subdirectories from a config are created")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
wd = getcwd();
|
||||||
|
|
||||||
|
if args.sort_dir:
|
||||||
|
args.sort_dir = path.abspath(args.sort_dir)
|
||||||
else:
|
else:
|
||||||
wd = getcwd();
|
args.sort_dir = getcwd()
|
||||||
|
|
||||||
config_name = select_config()
|
confman = ConfigManager(config_dir)
|
||||||
if type(config_name) == str:
|
|
||||||
config = read_config(config_name)
|
# configuration
|
||||||
|
if type(args.config) == str:
|
||||||
|
config = confman.load_config(args.config, args.sort_dir)
|
||||||
else:
|
else:
|
||||||
config = create_config()
|
config = confman.present_config_selection(args.sort_dir)
|
||||||
|
|
||||||
if not config:
|
sorter = Sorter(wd, config)
|
||||||
print("Error reading the config:")
|
sorter.get_images()
|
||||||
print(" Config Name:", config_name)
|
sorter.sort()
|
||||||
print(" Config:", config)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
with uz.Canvas() as canvas:
|
|
||||||
sorter = Sorter(wd, canvas, config)
|
|
||||||
sorter.get_images()
|
|
||||||
sorter.sort()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
49
imgsort/ueberzug.py
Normal file
49
imgsort/ueberzug.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import subprocess
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
from .globals import error
|
||||||
|
|
||||||
|
class UeberzugLayer():
|
||||||
|
"""Wrapper for Ueberzug++"""
|
||||||
|
|
||||||
|
def __init__(self, pid_file = "/tmp/ueberzug-py.pid", socket="/tmp/ueberzugpp-%pid%.socket", no_opencv=True):
|
||||||
|
self._socket = None
|
||||||
|
self._pid_file = pid_file
|
||||||
|
self._pid = None
|
||||||
|
ret = subprocess.run(["ueberzug", "layer", "--pid-file", pid_file, "--no-stdin", "--no-opencv" if no_opencv else ""], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
|
if not ret.returncode == 0:
|
||||||
|
error(f"UeberzugLayer.init: ueberzug layer exited with {ret.returncode}")
|
||||||
|
if not path.isfile(pid_file):
|
||||||
|
error(f"UeberzugLayer.init: can not find ueberzug pid file at '{pid_file}'")
|
||||||
|
with open(pid_file, "r") as file:
|
||||||
|
try:
|
||||||
|
self._pid = int(file.read())
|
||||||
|
except ValueError as e:
|
||||||
|
raise Exception(f"Invalid content of pid file {pid_file}: {e}")
|
||||||
|
self._socket = socket.replace("%pid%", str(self._pid))
|
||||||
|
# if not path.exists(self._socket):
|
||||||
|
# raise Exception(f"Ueberzug socket not found: {self._socket}")
|
||||||
|
|
||||||
|
def display_image(self, image, x=0, y=0, max_width=0, max_height=0, identifier="Image"):
|
||||||
|
ret = subprocess.run(["ueberzug", "cmd", "-s", self._socket, "-a", "add", "-i", identifier, "-f", image, "-x", str(x), "-y", str(y), "--max-width", str(max_width), "--max-height", str(max_height)])
|
||||||
|
if not ret.returncode == 0:
|
||||||
|
self._socket = None
|
||||||
|
error(f"UeberzugLayer.display_image: ueberzug layer exited with {ret.returncode}")
|
||||||
|
|
||||||
|
def remove_image(self, identifier="Image"):
|
||||||
|
ret = subprocess.run(["ueberzug", "cmd", "-s", self._socket, "-a", "remove", "-i", identifier])
|
||||||
|
if not ret.returncode == 0:
|
||||||
|
self._socket = None
|
||||||
|
error(f"UeberzugLayer.remove_image: ueberzug layer exited with {ret.returncode}")
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
from os import remove
|
||||||
|
try:
|
||||||
|
remove(self._pid_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if self._socket is not None:
|
||||||
|
import subprocess # might be unloaded
|
||||||
|
ret = subprocess.run(["ueberzug", "cmd", "-s", self._socket, "-a", "exit"])
|
||||||
|
if not ret.returncode == 0:
|
||||||
|
error(f"UeberzugLayer.__del__: ueberzug layer exited with {ret.returncode}")
|
@ -3,7 +3,7 @@ requires = ["setuptools"]
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "imgsort"
|
name = "imgsort"
|
||||||
version = "1.2.0"
|
version = "1.2.1"
|
||||||
description = "A program that lets you easily sort images into different folders."
|
description = "A program that lets you easily sort images into different folders."
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
Loading…
Reference in New Issue
Block a user