diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
new file mode 100644
index 0000000..09af46b
--- /dev/null
+++ b/.github/workflows/python-publish.yml
@@ -0,0 +1,23 @@
+name: Upload Python Package
+on:
+ push:
+ tags:
+ - '[0-9]+\.[0-9]+\.[0-9]+'
+ - '[0-9]+\.[0-9]+\.[0-9]+-?rc\.?[0-9]+'
+
+jobs:
+ pypi:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Replace media paths in README.md
+ run: sed -E 's#(\]\((asset/[a-zA-Z0-9._-]+))#](https://github.com/CZ-NIC/mininterface/blob/main/\2?raw=True#g' README.md | less > README.md.tmp && mv README.md.tmp README.md
+ - name: Build the package
+ run: python3 -m pip install --upgrade build && python3 -m build
+ - name: Publish package
+ uses: pypa/gh-action-pypi-publish@v1.8.10
+ with:
+ password: ${{ secrets.PYPI_GITHUB_TOUCHTIMESTAMP }}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..cc7f4eb
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+export TAG := `grep version pyproject.toml | pz --search '"(\d+\.\d+\.\d+(?:rc\d+))?"'`
+
+release:
+ git tag $(TAG)
+ git push origin $(TAG)
\ No newline at end of file
diff --git a/README.md b/README.md
index 143f82a..d849b4f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,80 @@
-XXX asi z toho můžu udělat package na pypi
+[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
-Krusader user action command
-`python3 ...gui-touch/gui-touch.py %aList("Selected")%`
+Change file timestamps with a dialog window.
+
+![Gui window](asset/mininterface-gui.avif "Graphical interface")
+
+GUI automatically fallback to a text interface when display is not available.
+
+![Text interface](asset/textual.avif "Runs in the terminal")
+
+
+# Installation
+
+Install with a single command from [PyPi](https://pypi.org/project/touch-timestamp/).
+
+```bash
+pip install touch-timestamp
+```
+
+Or eventually add [eel](https://github.com/python-eel/Eel) to provide browser-like GUI window.
+
+```bash
+pip install touch-timestamp[eel]
+```
+
+![Browser interface](asset/eel-gui.avif "Eel interface")
+
+# Docs
+
+## Exact date
+
+When invoked with file paths, the program sets their modification times to the specified time.
+
+![Gui window](asset/mininterface-gui.avif "Graphical interface")
+
+## Fetch the time from the file name
+
+Should you end up with files that keep the date in the file name, use the `--from-name` parameter.
+
+```bash
+$ touch-timestamp 20240828_160619.heic --from-name True
+Changed 2001-01-01T12:00:00 → 2024-08-28T16:06:19: 20240828_160619.heic
+```
+
+## Full help
+
+Use the `--help` to see full options.
+
+```bash
+$ touch-timestamp --help
+usage: Touch [-h] [--eel | --no-eel] [--from-name {True,False}|STR]
+ [[PATH [PATH ...]]]
+
+╭─ positional arguments ─────────────────────────────────────────────────────╮
+│ [[PATH [PATH ...]]] │
+│ Files the modification date is to be changed. (default: ) │
+╰────────────────────────────────────────────────────────────────────────────╯
+╭─ options ──────────────────────────────────────────────────────────────────╮
+│ -h, --help │
+│ show this help message and exit │
+│ --eel, --no-eel │
+│ Prefer Eel GUI. (Set the date as in a chromium browser.) (default: │
+│ True) │
+│ --from-name {True,False}|STR │
+│ Fetch the modification time from the file names stem. Set the format │
+│ as for `datetime.strptime` like '%Y%m%d_%H%M%S'. │
+│ If set to True, the format will be auto-detected. │
+│ If a file name does not match the format or the format cannot be │
+│ auto-detected, the file remains unchanged. │
+│ │
+│ │
+│ Ex: `--from-name True 20240827_154252.heic` → modification time = │
+│ 27.8.2024 15:42 (default: False) │
+╰────────────────────────────────────────────────────────────────────────────╯
+```
+
+
+## Krusader user action
+
+To change the file timestamps easily from Krusader, import this [user action](extra/touch-timestamp-krusader-useraction.xml): `touch-timestamp %aList("Selected")%`
\ No newline at end of file
diff --git a/asset/eel-gui.avif b/asset/eel-gui.avif
new file mode 100644
index 0000000..e8d9e03
Binary files /dev/null and b/asset/eel-gui.avif differ
diff --git a/asset/mininterface-gui.avif b/asset/mininterface-gui.avif
new file mode 100644
index 0000000..6f12515
Binary files /dev/null and b/asset/mininterface-gui.avif differ
diff --git a/asset/textual.avif b/asset/textual.avif
new file mode 100644
index 0000000..ea98c8b
Binary files /dev/null and b/asset/textual.avif differ
diff --git a/extra/touch-timestamp-krusader-useraction.xml b/extra/touch-timestamp-krusader-useraction.xml
new file mode 100644
index 0000000..3392407
--- /dev/null
+++ b/extra/touch-timestamp-krusader-useraction.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ &touch-timestamp
+ touch-timestamp
+ Files
+ %aList("Selected")%
+ touch-timestamp %aList("Selected")%
+ Alt+Shift+T
+
+
diff --git a/gui-touch.py b/gui-touch.py
deleted file mode 100644
index d91f8ed..0000000
--- a/gui-touch.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from os import utime
-from pathlib import Path
-from sys import argv
-
-import dateutil.parser
-from eel import expose, init, start
-
-files = [Path(p) for p in argv[1:]]
-
-@expose
-def get_len_files():
- return len(files)
-
-@expose
-def get_first_file_date():
- return Path(files[0]).stat().st_mtime
-
-@expose
-def set_timestamp(date, time):
- print("Touching files", date, time)
- print(", ".join(str(f) for f in files))
- if date and time:
- time = dateutil.parser.parse(date + " " + time).timestamp()
- [utime(f, (time, time)) for f in files]
- return True
-
-
-init(Path(__file__).absolute().parent.joinpath('static'))
-start('index.html', size=(330, 30), port=0, block=True)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c00051a
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,22 @@
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "touch-timestamp"
+version = "0.3.0"
+description = "Change file timestamps with a dialog window."
+authors = ["Edvard Rejthar "]
+license = "GPL-3.0-or-later"
+homepage = "https://github.com/CZ-NIC/touch-timestamp"
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = "^3.10"
+mininterface = "*"
+
+[tool.poetry.extras]
+eel = ["eel"]
+
+[tool.poetry.scripts]
+touch-timestamp = "touch_timestamp.touch_timestamp:main"
\ No newline at end of file
diff --git a/static/index.html b/touch_timestamp/static/index.html
similarity index 100%
rename from static/index.html
rename to touch_timestamp/static/index.html
diff --git a/touch_timestamp/touch_timestamp.py b/touch_timestamp/touch_timestamp.py
new file mode 100755
index 0000000..84c8ed9
--- /dev/null
+++ b/touch_timestamp/touch_timestamp.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+from dataclasses import dataclass, field
+from datetime import datetime
+from os import utime
+from pathlib import Path
+
+import dateutil.parser
+from mininterface import run
+from tyro.conf import Positional
+
+try:
+ from eel import expose, init, start
+ eel = True
+except ImportError:
+ eel = None
+
+
+DateFormat = str # Use type as of Python3.12
+
+
+@dataclass
+class Env:
+ files: Positional[list[Path]] = field(default_factory=list)
+ """ Files the modification date is to be changed. """
+
+ eel: bool = True
+ """ Prefer Eel GUI. (Set the date as in a chromium browser.) """
+
+ from_name: bool | DateFormat = False
+ """
+ Fetch the modification time from the file names stem. Set the format as for `datetime.strptime` like '%Y%m%d_%H%M%S'.
+ If set to True, the format will be auto-detected.
+ If a file name does not match the format or the format cannot be auto-detected, the file remains unchanged.
+
+ Ex: `--from-name True 20240827_154252.heic` → modification time = 27.8.2024 15:42
+ """
+
+
+def set_files_timestamp(date, time, files: list[str]):
+ print("Touching files", date, time)
+ print(", ".join(str(f) for f in files))
+ if date and time:
+ time = dateutil.parser.parse(date + " " + time).timestamp()
+ [utime(f, (time, time)) for f in files]
+ return True
+
+
+def run_eel(files):
+ @expose
+ def get_len_files():
+ return len(files)
+
+ @expose
+ def get_first_file_date():
+ return Path(files[0]).stat().st_mtime
+
+ @expose
+ def set_timestamp(date, time):
+ return set_files_timestamp(date, time, files)
+
+ init(Path(__file__).absolute().parent.joinpath('static'))
+ start('index.html', size=(330, 30), port=0, block=True)
+
+
+def main():
+ m = run(Env, prog="Touch")
+
+ if m.env.from_name:
+ for p in m.env.files:
+ if m.env.from_name is True: # auto detection
+ try:
+ # 20240828_160619.heic -> "20240828 160619" -> "28.8."
+ dt = dateutil.parser.parse(p.stem.replace("_", ""))
+ except ValueError:
+ print(f"Cannot auto detect the date format: {p}")
+ continue
+ else:
+ try:
+ dt = datetime.strptime(p.stem, m.env.from_name)
+ except ValueError:
+ print(f"Does not match the format {m.env.from_name}: {p}")
+ continue
+ timestamp = int(dt.timestamp())
+ original = datetime.fromtimestamp(p.stat().st_mtime)
+ utime(str(p), (timestamp, timestamp))
+ print(f"Changed {original.isoformat()} → {dt.isoformat()}: {p}")
+ elif eel and m.env.eel: # set exact date with eel
+ run_eel(m.env.files)
+ else: # set exact date with Mininterface
+ if len(m.env.files) > 1:
+ title = f"Touch {len(m.env.files)} files"
+ else:
+ title = f"Touch {m.env.files[0].name}"
+
+ with m:
+ m.title = title # NOTE: Changing title does not work
+ date = datetime.fromtimestamp(Path(m.env.files[0]).stat().st_mtime)
+ output = {title: {"date": str(date.date()), "time": str(date.time())}}
+ m.form(output)
+ set_files_timestamp(output["date"], output["time"], m.env.files)
+
+
+if __name__ == "__main__":
+ main()