#!/usr/bin/env perl use strict; use warnings; use File::Basename qw; use File::Find qw; use File::Glob qw<:bsd_glob :nocase>; use File::Path qw; use IPC::Open2 qw; use List::Util qw; use JSON qw; my $BMDIR = "$ENV{HOME}/.bm"; mkdir $BMDIR || die $!; sub exists_cmd { my $cmd = shift; return `sh -c 'command -v $cmd'`; } my $YT = exists_cmd('yt'); my $GIT = "git -C '$BMDIR'" if exists_cmd('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/&/&/gr; } sub draw_dir_entry { my $dir = shift; return "" . sanitize_rofi(basename($dir)) . ""; } 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)) . " " . url_domain($url) . ""; } 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 ] [git ]\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); }