Page 1 of 1

a (not so simple) filetagger for testing

Posted: Mon Jun 27, 2022 6:57 pm
by HerrBert

What it's about:
I've been searching for a simple filetagger.

Just to tag any file to be able to find it later by simply searching for a tag.

What i find is heavy bloated QT/KDE taggers for media files, that alter fileheaders for e.g. audio or image files.

Not my intention...
I was looking for a standalone app to tag files independent from it's filetype.
So i recalled what my mother in law always used to say: help yourself!

Now this is what i got so far:

A standalone app that stores filenames associated with tags and comments in a local database.

Code: Select all

#!/bin/bash

APP="$(readlink -f "$0")"
HERE="${APP%/*}"
TEMP="/tmp/gtagger"
mkdir "$TEMP" 2>/dev/null
cd "$TEMP" || exit
MIMEICONS="/usr/share/icons/PMaterial/scalable/mimetypes"
DB="$HERE/tags0.3.csv"
export DB HERE MIMEICONS TEMP

#BT1="Entfernen,Abbrechen"
BT1="Remove,Cancel"
#BT3="Übernehmen"
BT3="Apply"
#ERR1=$'Keine Datenbank vorhanden.\nZiehe eine Datei auf das Script,\num sie hinzuzufügen.'
ERR1=$'No database found.\nDrag a file on the script to add an entry.'
#ERR2="kein Tag angegeben!"
ERR2="no tag specified!"
#ERR3="ist keine Datei!"
ERR3="is not a file!"
#HEADER1="Datei|Tags|Kommentar"
HEADER1="file|tags|comment"
#RMMSG1="Soll der Eintrag entfernt werden?"
RMMSG1="Remove entry from database?"
#RMMSG2=$'Datenbankeintrag ungültig\nDatei verschoben oder gelöscht?\n\nSoll der Eintrag entfernt werden?'
RMMSG2=$'Database entry invalid!\nFile moved or deleted?\n\nRemove entry from database?'
#STB1="Doppelklick öffnet Datei | Mittelklick öffnet Ort | Rechtsklick entfernt"
STB1="doubleclick opens file | middleclick opens location | rightclick removes"
#STB2="Nicht vergessen die Änderungen zu übernehmen..."
STB2="Don't forget to apply your changes..."
#STB3="Beliebige Tags oben rechts, Kommentare unten hinzufügen. Mehrere Tags mit Leerzeichen trennen."
STB3="Add any tags you like in top right pane, comments at bottom. Separate tags with whitespaces."
#STB4="Wort✚Wort findet Einträge, die alle Wörter enthalten, Wort Wort findet jeden Eintrag mit einem der Wörter"
STB4="Word✚Word finds entries that only match containing words, Word Word finds any entry with either word"
#STB5="Neue Suche mit leerem Suchfeld zeigt alle Einträge"
STB5="New search with empty searchfield shows all entries"
#TX0="Suche in Dateinamen, Tags und Kommentaren oder neue Datei hinzufügen:"
TX0="Search filenames,tags and comments or add a new file:"
#TX1="Suche: "
TX1="Search: "
#TX2="Verfügbare Tags:"
TX2="Currently available Tags:"
TX3="Tags:"
#TX4="Kommentar:"
TX4="Comment:"
export BT1 BT3 ERR2 HEADER1 RMMSG1 RMMSG2 STB1 STB2 STB3 STB4 STB5 TX0 TX1 TX2 TX3 TX4

################
# DEFAULTICONS #
################

[ ! -f ./tag.svg ] && { echo '<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="100" width="100">
	<path style="fill:#ededed;fill-rule:evenodd;stroke:#333333;stroke-width:6px;stroke-linejoin:round" d="m 20,10 l 30,0 40,40 -40,40 -40,-40 0,-30 z m 8,12 a 5,5 0 1 1 -1,0 z"/>
</svg>
' > ./tag.svg
echo '<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="256px" width="256px" viewBox="0 0 192 192">
  <path style="fill:#EDEDED;stroke:#646464;stroke-width:6;" d="m 64,96 0,-64 64,0 -16,16 c 40,12 16,104 16,80 c 0,-20 0,-40 -48,-48 z"/>
</svg>
' > ./inode-symlink.svg
echo '<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="100" width="100">
	<path style="fill:#ededed;stroke:#333333;stroke-width:8px" d="m 50,5 a 45,45 0 1 0 0.1,0 z"/>
	<path style="fill:none;stroke:#333333;stroke-width:8px;stroke-linecap:round" d="m 30,30 a 4,4 0 1 0 0.1,0 z m 40,0 a 4,4 0 1 0 0.1,0 z m -45,40 a 40,40 0 0 1 50,0"/>
</svg>
' > ./sad.svg
}
#############
# FUNCTIONS #
#############

func_preview() {
	[ "$1" ] || { [ ! -s ./tags-files ] && ln -sf ./sad.svg ./tags-image || ln -sf ./tag.svg ./tags-image; return; }
	[ -e "$1" ] || { ln -sf ./tag.svg ./tags-image; func_remove "$1" "$RMMSG2"; return; }
	MIMETYPE="$(file -bi "$1")"; MIMETYPE=${MIMETYPE%;*}
	case "${MIMETYPE}" in
		image*) ln -sf "$1" ./tags-image ;;
		*symlink*) ln -sf ./inode-symlink.svg ./tags-image ;;
		*) if [ -e ${MIMEICONS}/${MIMETYPE/\//-}.svg ]; then
			ln -sf ${MIMEICONS}/${MIMETYPE/\//-}.svg ./tags-image
		else
			ln -sf ./tag.svg ./tags-image
		fi
		;;
	esac
	CUTOFF="$(sed -e "\#${1}#!d;s#.*|\(.*\)|\(.*\)#\1|\2#" ./tags-files)"
	echo "${CUTOFF%|*}" > ./tags-tmp # ENTRY3
	echo "${CUTOFF#*|}" > ./tags-comment # ENTRY4
}
export -f func_preview

func_find() {
	if [ "$1" ]; then
		echo -n "" > ./tags-find
		STR=( $(echo "$1") )
		for ((i=0;i<${#STR[*]};i++)); do
			case ${STR[$i]} in
				*+*) for N in ${STR[$i]//+/ }; do
					PTN="${PTN}(?=.*${N})"
				done
				grep -i -P "$PTN" $DB >> ./tags-find
				;;
				*) grep -i -P "(?=.*${STR[$i]})" $DB >> ./tags-find
				;;
			esac
		done
		sort -u ./tags-find > ./tags-files
	else
		sort $DB > ./tags-files
	fi
	echo "" > ./tags-tmp
	echo "" > ./tags-comment
}
export -f func_find

func_loadtags() {
	[ -f ./tags-restart ] && [ "$(cat ./tags-restart)" = "true" ] && return
	cut -f2 -d\| $DB | tr \  '\n' | sort -u | tr '\n' \  > ./tags-all
}
export -f func_loadtags

func_add() { #set -x
	[ ! -f "$1" ] && exec xmessage -name gtagger -center -timeout 5 -buttons "" "$1"$'\n'"$ERR3"
	[ -f ./tags-tmp ] && { 
		echo "" > ./tags-tmp
		echo "" > ./tags-comment
		echo true > ./tags-restart
		sleep .3
	}
	echo "$STB3" > ./tags-status
	SENS1="false"
	PTN="$(sed -e "\#${1}#!d" $DB)"
	[ "$PTN" ] && { echo "$PTN" > ./tags-files; return; } # already exist but can not select it in tree or don't know how
	echo -n "${1}||" > ./tags-files
}
# export -f func_add

func_edit() {
	[ -z "$1" ] && return
	[ -z "$2" ] && { xmessage -name gtagger -center -timeout 5 -buttons "" "$ERR2"; return; } # no tags
	DBENTRY="$(grep "${1}" $DB)"
	[ "$DBENTRY" = "${1}|${2}|${3}" ] && return
	[ "$DBENTRY" ] && {
		sed -i "\#${1}# s#|.*|.*#|${2//&/\\&}|${3//&/\\&}#" $DB
		sed -i "\#${1}# s#|.*|.*#|${2//&/\\&}|${3//&/\\&}#" ./tags-files
	} || { echo "${2}|${3}" >> ./tags-files; cat ./tags-files | tr -s \| >> $DB; }
	func_loadtags # in case u added/removed tag
}
export -f func_edit

func_remove() { #set -x
	xmessage -name gtagger -center -buttons $BT1 "$2"$'\n'"$1"
	[ $? -ne 101 ] && return
	sed -i "\#${1}# d" $DB
	sed -i "\#${1}# d" ./tags-files
	echo "" > ./tags-tmp
	echo "" > ./tags-comment
	echo true > ./tags-gui
	func_loadtags # in case file removed tag
}
export -f func_remove

###########
# PROGRAM #
###########

[ ! -f $DB ] && { touch $DB; exec xmessage -name gtagger -center "$ERR1"; } # first start

[ -z "$1" -a -f ./tags-tmp ] && exit
[ "$1" ] && func_add "$@" || {
	echo "$STB1" > ./tags-status
	sort $DB > ./tags-files
	SENS1="true"
	}
func_loadtags

# some basics for gui:
ln -sf ./tag.svg ./tags-image
touch ./tags-{all,files,tmp,comment}
echo false > ./tags-gui
echo false > ./tags-restart

export MAIN_DIALOG='
<window title="gtagger" image-name="./tag.svg" default-width="800" window-position="1">
 <vbox>
  <vbox space-fill="false" space-expand="false">
   <frame '$TX0'>
   <hbox>
    <text><label>'$TX1'</label></text>
    <entry activates-default="true" primary-icon-stock="gtk-find" secondary-icon-stock="gtk-clear" is-focus="false">
     <variable>ENTRY1</variable>
     <sensitive>'$SENS1'</sensitive>
     <action signal="button-release-event" condition="command_is_true([ -f \"$ENTRY1\" ] && echo true)">enable:BTN2</action>
     <action signal="button-release-event">echo "'$STB4'" > ./tags-status</action>
     <action signal="activate">func_find "$ENTRY1"</action>
     <action signal="activate">echo true > ./tags-gui</action>
     <action signal="primary-icon-release">func_find "$ENTRY1"</action>
     <action signal="primary-icon-release">echo true > ./tags-gui</action>
     <action signal="secondary-icon-release">clear:ENTRY1</action>
     <action>echo "'$STB1'" > ./tags-status</action>
    </entry>
    <button>
     <variable>BTN2</variable>
     <sensitive>false</sensitive>
     <input file stock="gtk-add"></input>
     <action condition="command_is_true([ -f \"$ENTRY1\" ] && echo true)">'${APP}' "$ENTRY1" &</action>
    </button>
   </hbox>
   </frame>
   <frame '$TX2'>
    <text justify="left" xalign="left" space-fill="true" space-expand="true" width-chars="90" selectable="true" can-focus="false">
     <variable>ENTRY2</variable>
     <input file>./tags-all</input>
    </text>
   </frame>
  </vbox>
  <hbox space-fill="true" space-expand="true">
   <vbox space-fill="true" space-expand="true">
    <tree column-visible="true|false|false" is-focus="true">
     <variable>TREE1</variable>
     <width>400</width>
     <height>460</height>
     <label>'$HEADER1'</label>
     <input file>./tags-files</input>
     <action signal="changed">func_preview "$TREE1"</action>
     <action signal="button-release-event">echo "'$STB1'" > ./tags-status</action>
     <action signal="changed">refresh:STATUSBAR</action>
     <action signal="changed">refresh:PIXMAP</action>
     <action signal="changed">refresh:ENTRY3</action>
     <action signal="changed">refresh:ENTRY4</action>
     <action signal="changed">disable:BTN1</action>
     <action>[ "$TREE1" ] && xdg-open "$TREE1"</action>
     <action signal="button-release-event" condition="command_is_true([ $PTR_BTN = 3 ] && echo true)">[ "$TREE1" ] && func_remove "$TREE1" "'$RMMSG1'"</action>
     <action signal="button-release-event" condition="command_is_true([ $PTR_BTN = 2 ] && echo true)">[ "$TREE1" ] && rox -s "$TREE1"</action>
    </tree>
   </vbox>
   <vbox width-request="400" space-fill="false" space-expand="false">
    <hbox>
     <frame '$TX3'>
      <entry>
       <variable>ENTRY3</variable>
       <input file>./tags-tmp</input>
       <action signal="focus-in-event">enable:BTN3</action>
       <action signal="focus-in-event">enable:ENTRY1</action>
       <action signal="focus-in-event">echo "'$STB2'" > ./tags-status</action>
      </entry>
     </frame>
    </hbox>
    <pixmap space-expand="true" space-fill="true">
     <variable>PIXMAP</variable>
     <width>400</width>
     <height>400</height>
     <input file>./tags-image</input>
    </pixmap>
   </vbox>
  </hbox>
  <vbox space-fill="false" space-expand="false">
   <frame '$TX4'>
    <entry truncate-multiline="true" secondary-icon-stock="gtk-clear">
     <variable>ENTRY4</variable>
     <input file>./tags-comment</input>
     <action signal="focus-in-event">enable:BTN3</action>
     <action signal="focus-in-event">enable:ENTRY1</action>
     <action signal="focus-in-event">echo "'$STB2'" > ./tags-status</action>
     <action signal="secondary-icon-release">clear:ENTRY4</action>
     <action signal="secondary-icon-release">enable:BTN1</action>
     <action signal="secondary-icon-release">echo "'$STB2'" > ./tags-status</action>
    </entry>
   </frame>
   <hbox>
    <button>
     <variable>BTN3</variable>
     <sensitive>false</sensitive>
     <label>'$BT3'</label>
     <input file stock="gtk-save"></input>
     <action>func_edit "$TREE1" "$ENTRY3" "$ENTRY4"</action>
     <action>disable:BTN3</action>
     <action>enable:ENTRY1</action>
     <action>echo "'$STB5'" > ./tags-status</action>
    </button>
    <button ok></button>
   </hbox>
   <hbox>
    <statusbar use-markup="true" auto-refresh="true" space-expand="true" space-fill="true" height-request="22">
     <variable>STATUSBAR</variable>
     <input file>./tags-status</input>
    </statusbar>
   </hbox>
   <checkbox auto-refresh="true" visible="false">
    <variable>GUI</variable>
    <input file>./tags-gui</input>
    <action>if true func_preview ""</action>
    <action>if true refresh:TREE1</action>
    <action>if true refresh:ENTRY2</action>
    <action>if true refresh:ENTRY3</action>
    <action>if true refresh:ENTRY4</action>
    <action>if true refresh:PIXMAP</action>
    <action>if true disable:BTN2</action>
    <action>if true echo "'$STB1'" > ./tags-status</action>
    <action>if true echo false > ./tags-gui</action>
   </checkbox>
   <checkbox auto-refresh="true" visible="false">
    <variable>RESTART</variable>
    <input file>./tags-restart</input>
    <action>if true exit:RESTART</action>
   </checkbox>
  </vbox>
 </vbox>
</window>
'

gtkdialog --program=MAIN_DIALOG

[ "$(cat ./tags-restart)" != "true" ] && rm ./tags-*

If you want to try it, make sure to run this from its' own directory - it's intended to run as ROX AppDir later...

As always i don't dare to publish this without someone having a look at it before. :roll:

My doubts are about:

  • adding new entry in a running instance

  • the search function, espescally using grep -P and its' syntax

This got more complex than it was intended to be :oops:

If you find any similarity to @MochiMoppel's mm_view - it's not accidentally ;)

Though i started from scratch and just had an eye on how things work... (editing other people's code can be a PITA...)

Internationalization needs to be implemented in the future, IF this get's published anyway.

Any thoughts on this?

[EDIT] Added screenshot (in german lang) to give an impression of what it looks like:
[EDIT] Updated code - project abandoned


Re: a (not so simple) filetagger for testing

Posted: Wed Jun 29, 2022 9:40 pm
by HerrBert

Project abandoned.

Code in 1st post updated to what i use ATM of writing.

Changes:
window resizeable
text and statusbar updated
added add button and functionality