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.
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
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