aboutsummaryrefslogtreecommitdiff
path: root/bm
diff options
context:
space:
mode:
Diffstat (limited to 'bm')
-rwxr-xr-xbm231
1 files changed, 231 insertions, 0 deletions
diff --git a/bm b/bm
new file mode 100755
index 0000000..4573135
--- /dev/null
+++ b/bm
@@ -0,0 +1,231 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+
+use File::Basename qw<basename dirname>;
+use File::Find qw<find>;
+use File::Glob qw<:bsd_glob :nocase>;
+use File::Path qw<make_path>;
+use File::Which qw<which>;
+use IPC::Open2 qw<open2>;
+use List::Util qw<all>;
+
+use JSON qw<decode_json>;
+
+my $BMDIR = "$ENV{HOME}/.bm";
+mkdir $BMDIR || die $!;
+
+my $YT = which('yt');
+my $GIT = "git -C '$BMDIR'" if which('git') && -d "$BMDIR/.git";
+
+sub is_uri {
+ my $text = shift;
+ return $text =~ /^http/;
+}
+
+sub all_contents {
+ my @files;
+ my $pre_len = length $BMDIR;
+ find({wanted => sub {
+ if (substr($_, 0, 1) eq '.' && $File::Find::name ne $BMDIR) {
+ # Ignore hidden files/dirs (except $BMDIR)
+ $File::Find::prune = 1;
+ } elsif (-f) {
+ push @files, $File::Find::name;
+ }
+ }},
+ $BMDIR);
+ @files = sort { $a cmp $b } @files;
+ return ([], \@files);
+}
+
+sub dir_contents {
+ my $dir = shift;
+
+ my @dirs = $dir eq $BMDIR ? () : ('..');
+ my @files;
+
+ foreach (glob "$dir/*") {
+ if (-d) {
+ push @dirs, $_;
+ } elsif (-f) {
+ push @files, $_;
+ }
+ }
+
+ return (\@dirs, \@files);
+}
+
+sub read_url {
+ my $file = shift;
+ open(my $fh, '<', $file) || die $!;
+ my $url = <$fh>;
+ return unless $url && is_uri($url);
+ $url =~ s/\s*$//g;
+ return $url;
+}
+
+sub open_bm {
+ my $url = shift;
+ if ($YT && $url =~ /youtube\.com\/watch/) {
+ `yt $url`;
+ return;
+ }
+ my $focused_name = `xdotool getwindowfocus getwindowname`;
+ if ($focused_name =~ / (Firefox|Chrom(e|ium))$/) {
+ `xdotool - <<<"key ctrl+t\ntype --delay 1ms $url\nsleep 0.2\nkey Return Tab"`;
+ } else {
+ `xdg-open $url`;
+ }
+}
+
+sub url_domain {
+ my $url = shift;
+ my ($domain) = $url =~ /^[a-z]+:\/\/([^\/]+)/;
+ return $domain;
+}
+
+sub notify {
+ my ($urgency, $text) = @_;
+ `notify-send -u '$urgency' '$text'`;
+}
+
+sub save_bm {
+ my ($basedir, $text) = @_;
+ my ($uri, $title);
+ if (is_uri($text)) {
+ $uri = $text;
+ $title = `rofi -dmenu -p "Set title" -no-fixed-num-lines` or return;
+ $title =~ s/\n//g;
+ } else {
+ $uri = `rofi -dmenu -p "Set URL" -no-fixed-num-lines` or return;
+ $uri =~ s/\n//g;
+ if (is_uri($uri)) {
+ $title = $text;
+ } else {
+ notify('critical', 'Not an URL; skipping');
+ return;
+ }
+ }
+ my $path = "$basedir/$title";
+ make_path(dirname($path));
+ open(my $fh, '>',$path);
+ print $fh "$uri\n";
+ if ($GIT) {
+ `$GIT add '$path'`;
+ my $basename = basename($path);
+ `$GIT commit -m "Add bookmark '$basename'"`;
+ }
+ notify('normal', "Bookmark saved");
+}
+
+sub sanitize_rofi {
+ return shift =~ s/&/&amp;/gr;
+}
+
+sub draw_dir_entry {
+ my $dir = shift;
+ return "<b>" . sanitize_rofi(basename($dir)) . "</b>";
+}
+
+sub draw_file_entry {
+ my $file = shift;
+ my $url = read_url($file);
+ unless ($url) {
+ print "$file does not seem to contain a valid URL; skipping\n";
+ return;
+ }
+ return sanitize_rofi(basename($file)) . " <u><tt>" . url_domain($url) . "</tt></u>";
+}
+
+sub select_bm {
+ my $basedir = $BMDIR;
+ my $recursive = 0;
+ while (-d $basedir) {
+ my ($dirs, $files) = $recursive ? all_contents() : dir_contents($basedir);
+
+ my $msg = $recursive ? "All bookmarks" : "Open bookmark";
+ open2(my $outh, my $inh, "rofi -sync -p '$msg' -dmenu -multi-select -i -markup-rows -lines 40 -width 80 -format 'i,f'");
+ print $inh "$_\n" foreach (map { draw_dir_entry($_) } @$dirs);
+ print $inh "$_\n" foreach (map { draw_file_entry($_) } @$files);
+ close $inh;
+
+ # Get all output lines from rofi
+ my @out = map { [/^(-?\d+),(.*)$/] } <$outh> or die 'cancelled';
+
+ # If there are sereval selections, all of them must be files
+ if (all { $_->[0] > $#$dirs } @out) {
+ foreach my $out (@out) {
+ my ($idx, $input) = @$out;
+ my $url = read_url($files->[$idx - @$dirs]);
+ open_bm($url);
+ }
+ return;
+ } elsif (@out == 1) {
+ my ($idx, $input) = @{$out[0]};
+ if ($idx == -1) {
+ # User entered something which was not among the options
+ if ($input) {
+ # If there was something in the search bar, save it
+ save_bm($basedir, $input);
+ return;
+ } else {
+ # Otherwise, take it as the recursive toggle (C-Ret)
+ $recursive = ! $recursive;
+ $basedir = $BMDIR;
+ }
+ } elsif ($idx <= $#$dirs) {
+ # User selected a directory
+ my $dir = $dirs->[$idx];
+ if ($dir eq '..') {
+ $basedir = dirname($basedir);
+ } else {
+ $basedir .= '/' . basename($dir);
+ }
+ }
+ }
+ }
+}
+
+sub sanitize_path {
+ my $path = shift;
+ $path =~ s/\//-/g;
+ $path = substr($path, 0, 100);
+ #$path =~ s/\(/-/g;
+ return $path;
+}
+
+sub import_firefox {
+ my ($basedir, $json) = @_;
+ if ($json->{type} eq "text/x-moz-place-container") {
+ my $title = sanitize_path($json->{title});
+ my $newdir = "$basedir/$title";
+ mkdir $newdir;
+ import_firefox($newdir, $_) foreach (@{$json->{children}});
+ } elsif ($json->{type} eq "text/x-moz-place") {
+ my $uri = $json->{uri};
+ my $title = sanitize_path($json->{title});
+ my $newfile = "$basedir/$title";
+ open(my $fh, '>', $newfile) || print "Unable to open $newfile\n";
+ print $fh $uri;
+ }
+}
+
+if (@ARGV == 0) {
+ select_bm();
+} elsif ($ARGV[0] eq "-h") {
+ die "Usage: $0 [--import-firefox <bookmarks.json>] [git <git command ...>]\n";
+} elsif ($ARGV[0] eq "git") {
+ if ($GIT) {
+ shift;
+ exec 'git', '-C', $BMDIR, @ARGV;
+ } else {
+ die which('git') ? "$BMDIR is not a git repository\n" : "git is not installed\n";
+ }
+} elsif ($ARGV[0] eq "--import-firefox") {
+ open(my $fh, '<', $ARGV[1]);
+ my $json = decode_json(<$fh>);
+ close($fh);
+ import_firefox($BMDIR, $json);
+}