Skip to content

Commit

Permalink
Merge pull request jarun#789 from LeXofLeviafan/swap-indices
Browse files Browse the repository at this point in the history
Bookmark index swapping
  • Loading branch information
jarun authored Oct 22, 2024
2 parents 1f6c1a8 + de8bc94 commit 48bf29e
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 56 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ EDIT OPTIONS:
clears description, if no arguments
--immutable N disable web-fetch during auto-refresh
N=0: mutable (default), N=1: immutable
--swap N M swap two records at specified indices
SEARCH OPTIONS:
-s, --sany [...] find records with ANY matching keyword
Expand Down Expand Up @@ -290,12 +291,24 @@ SYMBOLS:
PROMPT KEYS:
1-N browse search result indices and/or ranges
R [N] print out N random search results
(or random bookmarks if negative or N/A)
^ id1 id2 swap two records at specified indices
O [id|range [...]] open search results/indices in GUI browser
toggle try GUI browser if no arguments
a open all results in browser
s keyword [...] search for records with ANY keyword
S keyword [...] search for records with ALL keywords
d match substrings ('pen' matches 'opened')
m search with markers - search string is split
into keywords by prefix markers, which determine
what field the keywords is searched in:
'.', '>' or ':' - title, description or URL
'#'/'#,' - tags (comma-separated, partial/full match)
'*' - all fields (can be omitted in the 1st keyword)
note: tag marker is not affected by 'd' (deep search)
v fields change sorting order (default is '+index')
multiple comma/space separated fields can be specified
r expression run a regex search
t [tag, ...] search by tags; show taglist, if no args
g taglist id|range [...] [>>|>|<<] [record id|range ...]
Expand Down Expand Up @@ -548,7 +561,11 @@ PROMPT KEYS:
$ buku --random -S kernel debugging --export random.md
46. More **help**:
46. Swap positions of records #4 and #5:
$ buku --swap 4 5
47. More **help**:
$ buku -h
$ man buku
Expand Down
3 changes: 3 additions & 0 deletions auto-completion/bash/buku-completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,13 @@ _buku () {
--order
-p --print
-r --sreg
--random
--replace
-s --sany
-S --sall
--shorten
--suggest
--swap
-t --stag
--tacit
--tag
Expand Down Expand Up @@ -76,6 +78,7 @@ _buku () {
-s --sany
-S --sall
--shorten
--swap
--threads
--url
-x --exclude
Expand Down
2 changes: 2 additions & 0 deletions auto-completion/fish/buku.fish
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ complete -c buku -l offline --description 'add a bookmark without connec
complete -c buku -l order -r --description 'order by fields (+/- prefix for direction)'
complete -c buku -s p -l print --description 'show bookmark details'
complete -c buku -s r -l sreg -r --description 'match a regular expression'
complete -c buku -l random --description 'random subset (of 1 or given amount)'
complete -c buku -l replace -r --description 'replace a tag'
complete -c buku -s s -l sany -r --description 'match any keyword'
complete -c buku -s S -l sall -r --description 'match all keywords'
complete -c buku -l shorten -r --description 'shorten a URL using tny.im'
complete -c buku -l suggest --description 'show a list of similar tags'
complete -c buku -l swap -r --description 'swap 2 given bookmark indices'
complete -c buku -s t -l stag --description 'search by tag or show tags'
complete -c buku -l tacit --description 'reduce verbosity'
complete -c buku -l tag --description 'set tags, use + to append, - to remove'
Expand Down
2 changes: 2 additions & 0 deletions auto-completion/zsh/_buku
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ args=(
'(--order)--order[order by fields (+/- prefix for direction)]:fields'
'(-p --print)'{-p,--print}'[show bookmark details]'
'(-r --sreg)'{-r,--sreg}'[match a regular expression]:regex'
'(--random)--random[random subset (of 1 or given amount)]::amount'
'(--replace)--replace[replace a tag]:tag to replace'
'(-s --sany)'{-s,--sany}'[match any keyword]:keyword(s)'
'(-S --sall)'{-S,--sall}'[match all keywords]:keyword(s)'
'(--shorten)--shorten[shorten a URL using tny.im]:index/url'
'(--suggest)--suggest[show a list of similar tags]'
'(--swap)--swap[swap 2 given bookmark indices]:index1 index2'
'(-t --stag)'{-t,--stag}'[search by tag or show tags]'
'(--tacit)--tacit[reduce verbosity]'
'(--tag)--tag[set tags, use + to append, - to remove]'
Expand Down
57 changes: 55 additions & 2 deletions buku
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,40 @@ class BukuDb:
return search_results
return filter_from(search_results, self.searchdb(without, deep=deep, markers=markers), exclude=True)

def swap_recs(self, index1: int, index2: int, *, lock: bool = True, delay_commit: bool = False):
"""Swaps two records with given indices
Parameters
----------
index1 : int
Index of the 1st record to be exchanged.
index2 : int
Index of the 2nd record to be exchanged.
lock : bool
Whether to restrict concurrent access (True by default).
delay_commit : bool
True if record should not be committed to the DB,
leaving commit responsibility to caller. Default is False.
Returns
-------
bool
True on success, False on failure.
"""
if lock:
with self.lock:
return self.swap_recs(index1, index2, lock=False, delay_commit=delay_commit)

max_id = self.get_max_id()
if not max_id or index1 == index2 or not all(0 < x <= max_id for x in [index1, index2]):
return False

self.cur.executemany('UPDATE bookmarks SET id = ? WHERE id = ?',
[(max_id+1, index1), (index1, index2), (index2, max_id+1)])
if not delay_commit:
self.conn.commit()
return True

def compactdb(self, index: int, delay_commit: bool = False):
"""When an entry at index is deleted, move the
last entry in DB to index, if index is lesser.
Expand Down Expand Up @@ -3362,6 +3396,7 @@ PROMPT KEYS:
1-N browse search result indices and/or ranges
R [N] print out N random search results
(or random bookmarks if negative or N/A)
^ id1 id2 swap two records at specified indices
O [id|range [...]] open search results/indices in GUI browser
toggle try GUI browser if no arguments
a open all results in browser
Expand Down Expand Up @@ -4705,7 +4740,7 @@ def prompt(obj, results, noninteractive=False, deep=False, listtags=False, sugge
if nav == 'n':
continue

if (m := re.match(r'^R(?: (-)?([0-9]+))?$', nav)) and (n := int(m[2] or 1)) > 0:
if (m := re.match(r'^R(?: (-)?([0-9]+))?$', nav.rstrip())) and (n := int(m[2] or 1)) > 0:
skip_print = True
if results and not m[1]: # from search results
picked = random.sample(results, min(n, len(results)))
Expand All @@ -4716,6 +4751,14 @@ def prompt(obj, results, noninteractive=False, deep=False, listtags=False, sugge
print_single_rec(row, columns=columns)
continue

if (m := re.match(r'^\^ ([1-9][0-9]*) ([1-9][0-9]*)$', nav.rstrip())):
index1, index2 = map(int, m.group(1, 2))
if bdb.swap_recs(index1, index2):
bdb.print_rec({index1, index2})
else:
print('Failed to swap records #%d and #%d' % (index1, index2))
continue

# search ANY match with new keywords
if nav.startswith('s '):
keywords = (nav[2:].split() if not markers else split_by_marker(nav[2:]))
Expand Down Expand Up @@ -5881,13 +5924,15 @@ POSITIONAL ARGUMENTS:
-c, --comment [...] notes or description of the bookmark
clears description, if no arguments
--immutable N disable web-fetch during auto-refresh
N=0: mutable (default), N=1: immutable''')
N=0: mutable (default), N=1: immutable
--swap N M swap two records at specified indices''')
addarg = edit_grp.add_argument
addarg('--url', nargs=1, help=hide)
addarg('--tag', nargs='*', help=hide)
addarg('--title', nargs='*', help=hide)
addarg('-c', '--comment', nargs='*', help=hide)
addarg('--immutable', type=int, choices={0, 1}, help=hide)
addarg('--swap', nargs=2, type=int, help=hide)
_bool = lambda x: x if x is None else bool(x)
_immutable = lambda args: _bool(args.immutable)

Expand Down Expand Up @@ -6181,6 +6226,14 @@ POSITIONAL ARGUMENTS:
colorize=not args.nc
)

if args.swap:
index1, index2 = args.swap
if bdb.swap_recs(index1, index2):
bdb.print_rec({index1, index2})
else:
LOGERR('Failed to swap records #%d and #%d', index1, index2)
bdb.close_quit(0)

# Editor mode
if args.write is not None:
if not is_editor_valid(args.write):
Expand Down
12 changes: 12 additions & 0 deletions buku.1
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Bookmarks with immutable titles are listed with '(L)' after the title.
- If --url is passed (and --title is omitted), update the title from web using the URL. Description is updated (if --comment is omitted). Tags remain untouched.
- If indices are passed without any other options (--url, --title, --tag, --comment and --immutable), read the URLs from DB and update titles, description and append tags from web. Bookmarks marked immutable are skipped.
- Can update bookmarks matching a search, when combined with any of the search options and no arguments to update are passed.
- Additionally, --swap allows to modify records order (standalone operation).
.PP
.IP 7. 4
\fBDelete\fR operation:
Expand Down Expand Up @@ -143,6 +144,9 @@ Add notes or description of the bookmark, works with --add, --update. Clears the
.TP
.BI \--immutable " N"
Set the title, description and tags of a bookmark immutable during autorefresh. Works with --add, --update. N=1 sets the immutable flag, N=0 removes it. If omitted, bookmarks are added with N=0.
.TP
.BI \--swap " N M"
Swap two records at specified indices. This is a standalone operation (cannot be invoked along with any other).
.SH SEARCH OPTIONS
.TP
.BI \-s " " \--sany " keyword [...]"
Expand Down Expand Up @@ -957,6 +961,14 @@ Print out a single \fBrandom\fR bookmark matching \fBsearch\fR criteria, and \fB
.B buku --random -S kernel debugging --export random.md
.EE
.PP
.IP 46. 4
Swap positions of records #4 and #5:
.PP
.EX
.IP
.B buku --swap 4 5
.EE
.PP


.SH AUTHOR
Expand Down
5 changes: 5 additions & 0 deletions bukuserver/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class BookmarkForm(FlaskForm):
fetch = HiddenField(filters=[bool])


class SwapForm(FlaskForm):
id1 = HiddenField(filters=[int])
id2 = HiddenField(filters=[int])


class ApiTagForm(FlaskForm):
class Meta:
csrf = False
Expand Down
48 changes: 47 additions & 1 deletion bukuserver/templates/bukuserver/bookmarks_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,62 @@
{% block head %}
{{ super() }}
{{ buku.close_if_popup() }}
<script>
function promptSwap(input, rowId, maxId={{count|tojson}}) {
let _id = input.value = prompt({{ _('Swap record #{} with record #')|tojson }}.replace('{}', rowId), rowId) || "";
let error = (!_id ? "" :
!/^[1-9][0-9]*$/.test(_id) ? {{ _("Not a valid record index: '{}'")|tojson }}.replace('{}', _id) :
_id > maxId ? {{ _('There are only {} records in total!')|tojson }}.replace('{}', maxId) :
_id == `${rowId}` ? {{ _('Swapping a record with itself has no effect!')|tojson }} : null);
error && alert(error);
return (error == null);
}
</script>
{% endblock %}

{% block model_menu_bar_before_filters %}
{{ super() }}
{% if data %}
{% set _random = url_for('bookmark.details_view', modal=True, id='random', url=return_url, **(request.args|flt)) %}
{% set _random = url_for('.details_view', modal=True, id='random', url=return_url, **(request.args|flt)) %}
<li><a id="random" data-target="#fa_modal_window" data-toggle="modal" href="{{ _random }}">{{ _('Random') }}</a></li>
{% endif %}
{% endblock %}

{% macro swap_rows_action(icon, row_id, step=None) %} {# based on admin/model/row_actions.delete_row() #}
<form class="icon" method="POST" action="{{ get_url('.swap') }}">
{% if csrf_token %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
{% endif %}
{% set _input = 'swap' + row_id|string %}
<input type="hidden" name="url" value="{{ return_url }}"/>
<input type="hidden" name="id1" value="{{ row_id }}"/>
<input type="hidden" name="id2"{% if step %} value="{{ row_id + step }}"{% else %} id="{{ _input }}"{% endif %}/>
<button title="{{ _('Swap with…') if not step else _('Move down') if step > 0 else _('Move up') }}"
{%- if not step %} onclick="return promptSwap({{_input}}, {{row_id}})"{% endif %}>
<span class="fa fa-{{icon}} glyphicon glyphicon-{{icon}}"></span>
</button>
</form>
{% endmacro %}

{% block list_row_actions scoped %}
{% for action in list_row_actions %}
{{ action.render_ctx(get_pk_value(row), row) }}
{% endfor %}
{% if request.args|flt|length == 0 %} {# only shown when filters/ordering are disabled #}
<div class="swap-toolbar" style="margin-left: 10px">
{% if row.id < 2 %}
<div style="display: inline-block; width: 14px"><!-- placeholder for the 1st row button --></div>
{% else %}
{{ swap_rows_action('arrow-up', row.id, -1) }}
{% endif %}
{{ swap_rows_action('transfer', row.id) }}
{% if row.id < count %}
{{ swap_rows_action('arrow-down', row.id, +1) }}
{% endif %}
</div>
{% endif %}
{% endblock %}

{% block tail %}
{{ buku.fix_translations('bookmarks') }}
{{ super() }}
Expand Down
Loading

0 comments on commit 48bf29e

Please sign in to comment.