Desktop-Workspace-Util
view release on metacpan or search on metacpan
lib/Desktop/Workspace/Util.pm view on Meta::CPAN
package Desktop::Workspace::Util;
use 5.010001;
use strict 'subs', 'vars';
use utf8;
use warnings;
use Log::ger;
use Exporter qw(import);
use List::Util qw(first);
use Perinci::Sub::Util qw(gen_modified_sub);
our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
our $DATE = '2026-05-04'; # DATE
our $DIST = 'Desktop-Workspace-Util'; # DIST
our $VERSION = '0.004'; # VERSION
our @EXPORT_OK = qw(
get_desktop_workspace_module
instantiate_desktop_workspace_module
list_desktop_workspace_items
open_desktop_workspace_items
);
our %SPEC;
our %argspec0_module = (
module => {
schema => 'perl::modname*',
req => 1,
pos => 0,
},
);
our %argspecopt_ns_prefixes = (
ns_prefixes => {
'x.name.is_plural' => 1,
'x.name.singular' => 'ns_prefix',
summary => 'List of namespaces to search for a Desktop Workspace specification modules',
schema => ['array*', of=>['any*', of=>['perl::modname', ['str', in=>[""]]]]],
default => ['DesktopWorkspace', ''],
},
);
our %argspecs_module = (
%argspec0_module,
%argspecopt_ns_prefixes,
);
our %argspecopt_module_args = (
module_args => {
schema => 'hash*',
},
);
$SPEC{':package'} = {
v => 1.1,
summary => 'Utilities related to DesktopWorkspace',
};
$SPEC{get_desktop_workspace_module} = {
v => 1.1,
summary => 'Get the first Perl desktop workspace specification module',
args => {
%argspecs_module,
},
result_naked => 1,
};
sub get_desktop_workspace_module {
my %args = @_;
my $module = $args{module} or die "Please specify 'module'";
my $ns_prefixes = $args{ns_prefixes} // ["DesktopWorkspace"];
push @$ns_prefixes, "" unless @$ns_prefixes;
for my $ns_prefix (@$ns_prefixes) {
my $mod = (length($ns_prefix) ? "$ns_prefix\::" : "") . $module;
(my $mod_pm = "$mod.pm") =~ s!::!/!g;
if (eval { require $mod_pm; 1 }) {
return $mod;
}
}
die "Can't find desktop workspace specification module '$module' (searched in ". join(", ", @$ns_prefixes).")";
}
$SPEC{instantiate_desktop_workspace_module} = {
v => 1.1,
summary => 'Instantiate the desktop workspace specification module (class)',
args => {
%argspecs_module,
%argspecopt_module_args,
},
result_naked => 1,
};
sub instantiate_desktop_workspace_module {
my %args = @_;
my $mod = get_desktop_workspace_module(
module => $args{module}, ns_prefixes => $args{ns_prefixes});
$mod->new(%{ $args{module_args} // {} });
}
sub _find_window {
my ($re, $all_windows) = @_;
log_trace "Checking existing window (%s) ...", $re;
for my $row (@$all_windows) {
if ($row->{title} =~ $re) {
log_trace "Found matching window %s", $row;
return $row;
}
}
return;
}
sub _items2windows {
require Desktop::XWindowManager::Util;
my $items = shift;
my $windowkey;
my $auto_file_num = 0;
my $auto_prog_num = 0;
my $auto_firefox_num = 0;
my $auto_dolphin_num = 0;
my %windows; # key = prefix+arbitrary number
my $all_windows;
{
my $res = Desktop::XWindowManager::Util::list_xwm_windows(
detail=>1,
with_kde_activity_name => 1,
);
return [500, "Can't list windows: $res->[0] - $res->[1]"]
unless $res->[0] == 200;
$all_windows = $res->[2];
}
ITEM:
for my $item (@$items) {
if (defined($item->{url}) || defined($item->{dir})) {
my $url = $item->{url} // $item->{dir};
if (defined $item->{dolphin_window_num}) {
$windowkey = "dolphin_".sprintf("%04d", $item->{dolphin_window_num});
} elsif (defined $item->{firefox_window_num}) {
$windowkey = "firefox_".sprintf("%04d", $item->{firefox_window_num});
} else {
my ($app, $num);
if ($item->{new_browser_window}) {
$app = "firefox";
$num = ++$auto_firefox_num;
} elsif ($item->{new_file_manager_window}) {
$app = "dolphin";
$num = ++$auto_dolphin_num;
} else {
if (defined($item->{url})) {
$app = "firefox";
$num = $auto_firefox_num;
} else {
$app = "dolphin";
$num = $auto_dolphin_num;
}
}
$windowkey = "${app}_auto_".sprintf("%04d", $num);
}
} elsif (defined $item->{file}) {
$windowkey = "file_" . (++$auto_file_num);
} elsif (defined $item->{prog_name} or defined $item->{prog_path}) {
$windowkey = "prog_" . (++$auto_prog_num);
}
$windows{$windowkey} //= { items=>[] };
push @{ $windows{$windowkey}{items} }, $item;
} # for item
CHECK_EXISTING_WINDOWS: {
for my $windowkey (sort keys %windows) {
my $window = $windows{$windowkey};
my $re = $window->{items}[-1]{window_title_re};
if (!$re && $windowkey =~ /^dolphin/) {
my $url = $window->{items}[-1]{url} // $window->{items}[-1]{dir};
$re = qr/\Q$url\E â Dolphin$/;
}
if ($re) {
my $w = _find_window($re, $all_windows);
if ($w) {
$window->{existing_window_id} = $w->{id};
$window->{existing_window_kde_activity_name} = $w->{kde_activity_name};
}
}
}
}
\%windows;
}
$SPEC{list_desktop_workspace_items} = {
v => 1.1,
summary => 'List the items from desktop workspace specification module, with filtering options',
args => {
%argspecs_module,
%argspecopt_module_args,
all => {
summary => 'Whether to include items that are not included by default (has property `include_by_default`=0)',
schema => 'bool*',
tags => ['category:filtering'],
},
include_any_tags => {
summary => 'Include all items that have any tag specified',
schema => ['array*', of=>'str*'],
tags => ['category:filtering'],
},
include_all_tags => {
summary => 'Include all items that have ALL tags specified',
schema => ['array*', of=>'str*'],
tags => ['category:filtering'],
},
exclude_any_tags => {
summary => 'Exclude all items that have any tags specified',
schema => ['array*', of=>'str*'],
tags => ['category:filtering'],
},
exclude_all_tags => {
summary => 'Exclude all items that have ALL tags specified',
schema => ['array*', of=>'str*'],
tags => ['category:filtering'],
},
include_url => {
summary => 'Whether to include URL items',
schema => ['bool*'],
tags => ['category:filtering'],
},
include_file => {
summary => 'Whether to include file items',
schema => ['bool*'],
tags => ['category:filtering'],
},
include_dir => {
summary => 'Whether to include dir items',
schema => ['bool*'],
tags => ['category:filtering'],
},
include_prog => {
summary => 'Whether to include program items',
schema => ['bool*'],
tags => ['category:filtering'],
},
query => {
schema => ['array*', of=>'str*'],
pos => 1,
slurpy => 1,
tags => ['category:filtering'],
},
detail => {
schema => 'bool*',
cmdline_aliases => {l=>{}},
tags => ['category:result'],
},
shuffle => {
schema => 'bool*',
tags => ['category:result'],
},
},
};
sub list_desktop_workspace_items {
my %args = @_;
my $obj = instantiate_desktop_workspace_module(
module => $args{module},
ns_prefixes => $args{ns_prefixes},
module_args => $args{module_args},
);
my $items = $obj->items;
if ($args{shuffle}) {
require List::Util;
$items = [List::Util::shuffle(@$items)];
}
require List::Util::Find;
my @filtered_items;
ITEM:
for my $i (0 .. $#{$items}) {
my $item = $items->[$i];
# if not included by default, will be included only if specifically matching a filter
my $include_by_default = $args{all} ? 1 :
($item->{include_by_default} // 1);
my $match_a_filter = 0;
FILTER: {
INCLUDE_ANY_TAGS: {
last unless $args{include_any_tags} && @{ $args{include_any_tags} };
do { log_debug "Skipping item %s: does not pass include_any_tags %s", $item, $args{include_any_tags}; next ITEM }
unless List::Util::Find::hasanystrs($args{include_any_tags}, @{ $item->{tags} // []});
$match_a_filter++;
}
INCLUDE_ALL_TAGS: {
last unless $args{include_all_tags} && @{ $args{include_all_tags} };
do { log_debug "Skipping item %s: does not pass include_all_tags %s", $item, $args{include_all_tags}; next ITEM }
unless List::Util::Find::hasallstrs($args{include_all_tags}, @{ $item->{tags} // []});
$match_a_filter++;
}
EXCLUDE_ANY_TAGS: {
last unless $args{exclude_any_tags} && @{ $args{exclude_any_tags} };
do { log_debug "Skipping item %s: does not pass exclude_any_tags %s", $item, $args{exclude_any_tags}; next ITEM }
if List::Util::Find::hasanystrs($args{exclude_any_tags}, @{ $item->{tags} // []});
$match_a_filter++;
}
EXCLUDE_ALL_TAGS: {
last unless $args{exclude_all_tags} && @{ $args{exclude_all_tags} };
do { log_debug "Skipping item %s: does not pass exclude_all_tags %s", $item, $args{exclude_all_tags}; next ITEM }
if List::Util::Find::hasallstrs($args{exclude_all_tags}, @{ $item->{tags} // []});
$match_a_filter++;
}
QUERY: {
last unless $args{query} && @{ $args{query} };
my $num_positive_queries = 0;
my $num_negative_queries = 0;
my $match = 0;
Q:
for my $query0 (@{ $args{query} }) {
my ($is_negative, $query) = $query0 =~ /\A(-?)(.*)/;
$num_positive_queries++ if !$is_negative;
$num_negative_queries++ if $is_negative;
lib/Desktop/Workspace/Util.pm view on Meta::CPAN
if (defined $item->{file}) {
if ($item->{file} =~ /$query/i) {
if ($is_negative) { goto L1 } else { $match = 1; last Q }
}
}
if (defined $item->{dir}) {
if ($item->{dir} =~ /$query/i) {
if ($is_negative) { goto L1 } else { $match = 1; last Q }
}
}
if (defined $item->{prog_name}) {
if ($item->{prog_name} =~ /$query/i) {
if ($is_negative) { goto L1 } else { $match = 1; last Q }
}
}
if (defined $item->{prog_path}) {
if ($item->{prog_path} =~ /$query/i) {
if ($is_negative) { goto L1 } else { $match = 1; last Q }
}
}
for my $tag (@{ $item->{tags} // [] }) {
if ($tag =~ /$query/i) {
if ($is_negative) { goto L1 } else { $match = 1; last Q }
}
}
if (defined $item->{firefox_container}) {
if ($item->{firefox_container} =~ /$query/i) {
if ($is_negative) { goto L1 } else { $match = 1; last Q }
}
}
} # for query
$match++ if $num_positive_queries == 0;
L1:
do { log_debug "Skipping item %s: does not pass query %s", $item, $args{query}; next ITEM }
unless $match;
$match_a_filter++;
} # QUERY
} # FILTER
if (!$include_by_default && !$match_a_filter) {
log_debug "Skipping item %s: not included by default and does not match filter(s)", $item;
next ITEM;
}
push @filtered_items, $item;
} # for item
unless ($args{detail}) {
@filtered_items = map {
$_->{url} // $_->{file} // $_->{dir} // $_->{prog_name} // $_->{prog_path}
} @filtered_items;
}
[200, "OK", \@filtered_items, {
'func.obj' => $obj,
}];
}
gen_modified_sub(
die => 1,
output_name => 'open_desktop_workspace_items',
base_name => 'list_desktop_workspace_items',
summary => 'Open desktop workspace items',
description => <<'MARKDOWN',
MARKDOWN
add_args => {
kde_activity => {
summary => 'Switch to the specified KDE activity name',
description => <<'MARKDOWN',
Override's desktop workspace specification's `kde_activity` property.
MARKDOWN
schema => 'str*',
},
reuse_window => {
summary => 'Do not open if existing window is detected',
schema => 'bool*',
},
},
modify_meta => sub {
my $meta = shift;
$meta->{features} = {
dry_run => 1,
};
$meta->{deps} = {
all => [
{prog => 'dolphin'},
{prog => 'firefox-container'},
],
};
},
wrap_code => sub {
require IPC::System::Options;
my $orig = shift;
my %args = @_;
my $dry_run = $args{-dry_run};
my $obj;
my $items;
LIST_ITEMS: {
my $res = $orig->(%args, detail=>1);
unless ($res->[0] == 200) {
return [500, "Can't list desktop workspace items: $res->[0] - $res->[1]"];
}
$items = $res->[2];
$obj = $res->[3]{'func.obj'};
} # LIST_ITEMS
my $windows = _items2windows($items);
WINDOW:
for my $windowkey (sort keys %$windows) {
my $window = $windows->{$windowkey};
my $items = $window->{items};
my $target_kde_activity;
GET_TARGET_KDE_ACTIVITY: {
my $item_kde_activity = first {defined $_->{kde_activity}} @$items; $item_kde_activity = $item_kde_activity->{kde_activity} if defined $item_kde_activity;
$target_kde_activity = $args{kde_activity} // $item_kde_activity // $obj->{kde_activity};
}
REUSE_EXISTING_WINDOW: {
last unless $window->{existing_window_id};
log_trace "Window for items %s already exists (ID %s)%s",
$items,
$window->{existing_window_id},
$args{reuse_window} ? ", reusing it" : "";
last unless $args{reuse_window};
#log_error "D: target kde activity: %s, existing window's kde activity: %s", $target_kde_activity, $window->{existing_window_kde_activity_name};
if (defined($window->{existing_window_kde_activity_name}) &&
defined($target_kde_activity) &&
$window->{existing_window_kde_activity_name} ne $target_kde_activity) {
log_trace "%sMoving window ID %s from KDE activity %s to KDE activity %s ...",
$args{-dry_run} ? "[DRY-RUN]" : "",
$window->{existing_window_id},
$window->{existing_window_kde_activity_name},
$target_kde_activity;
last if $args{-dry_run};
require Desktop::XWindowManager::Util;
my $res = Desktop::XWindowManager::Util::move_windows_to_kde_activity(
id => $window->{existing_window_id},
activity_name => $target_kde_activity,
);
log_warn "Can't move window ID %s to KDE activity %s: %d - %s",
$window->{existing_window_id},
$target_kde_activity,
$res->[0], $res->[1]
unless $res->[0] == 200;
}
next WINDOW;
} # REUSE_EXISTING_WINDOW
SWITCH_KDE_ACTIVITY: {
last unless defined $target_kde_activity;
log_trace "%sSetting KDE activity to %s ...",
$args{-dry_run} ? "[DRY-RUN]" : "",
$target_kde_activity;
last if $args{-dry_run};
require Desktop::KDEActivity::Util;
my $res = Desktop::KDEActivity::Util::set_current_kde_activity(
name => $target_kde_activity);
return [500, "Can't set current KDE activity: $res->[0] - $res->[1]"]
unless $res->[0] == 200;
}
if ($windowkey =~ /^firefox/) {
my $i = 0;
for my $item (@$items) {
$i++;
my $url = $item->{url};
my @ff_args;
my $env;
if ($i == 1) {
push @ff_args, "--new-window", $url;
} else {
push @ff_args, $url;
}
if (defined $item->{firefox_container}) {
$env->{FIREFOX_CONTAINER} = $item->{firefox_container};
}
log_trace "%sOpening URL %s in firefox (window %s) tab [#%d/%d]%s ...",
$dry_run ? "[DRY-RUN]" : "",
$url,
$windowkey,
$i,
scalar(@$items),
(defined $item->{firefox_container} ? " container $item->{firefox_container}" : "");
unless ($dry_run) {
IPC::System::Options::system(
{env=>$env, log=>1},
"firefox-container", @ff_args);
}
} # for item
} elsif ($windowkey =~ /^file_/) {
my $i = 0;
for my $item (@$items) {
$i++;
my $file = $item->{file};
log_trace "%sOpening file %s (window %s) ...",
$dry_run ? "[DRY-RUN]" : "",
$file,
$windowkey;
unless ($dry_run) {
require Desktop::Open;
Desktop::Open::open_desktop($file);
}
} # for item
} elsif ($windowkey =~ /^dolphin_/) {
my $i = 0;
my @urls;
for my $item (@$items) {
$i++;
my $url = $item->{dir} // $item->{url};
push @urls, $url;
}
log_trace "%sOpening dirs %s (window %s) ...",
$dry_run ? "[DRY-RUN]" : "",
\@urls,
$windowkey;
unless ($dry_run) {
IPC::System::Options::system(
{log=>1, shell=>1},
"dolphin", "--new-window", @urls, \"&");
}
} elsif ($windowkey =~ /^prog_/) {
my $i = 0;
for my $item (@$items) {
$i++;
my $prog = $item->{prog_name} // $item->{prog_path};
log_trace "%sOpening program %s (window %s) ...",
$dry_run ? "[DRY-RUN]" : "",
$prog,
$windowkey;
unless ($dry_run) {
IPC::System::Options::system(
{log=>1, shell=>1},
$prog, ($item->{prog_args} ? @{ $item->{prog_args} } : ()),
\"&");
}
}
} else {
die "BUG: Unknown window type $windowkey";
}
} # for window
[200];
},
);
1;
# ABSTRACT: Utilities related to DesktopWorkspace
__END__
=pod
=encoding UTF-8
=head1 NAME
Desktop::Workspace::Util - Utilities related to DesktopWorkspace
=head1 VERSION
This document describes version 0.004 of Desktop::Workspace::Util (from Perl distribution Desktop-Workspace-Util), released on 2026-05-04.
=head1 SYNOPSIS
=head1 DESCRIPTION
=head1 FUNCTIONS
=head2 get_desktop_workspace_module
Usage:
get_desktop_workspace_module(%args) -> any
Get the first Perl desktop workspace specification module.
This function is not exported by default, but exportable.
Arguments ('*' denotes required arguments):
=over 4
=item * B<module>* => I<perl::modname>
(No description)
=item * B<ns_prefixes> => I<array[perl::modname|str]> (default: ["DesktopWorkspace",""])
List of namespaces to search for a Desktop Workspace specification modules.
=back
Return value: (any)
=head2 instantiate_desktop_workspace_module
Usage:
instantiate_desktop_workspace_module(%args) -> any
Instantiate the desktop workspace specification module (class).
This function is not exported by default, but exportable.
Arguments ('*' denotes required arguments):
=over 4
=item * B<module>* => I<perl::modname>
(No description)
=item * B<module_args> => I<hash>
(No description)
=item * B<ns_prefixes> => I<array[perl::modname|str]> (default: ["DesktopWorkspace",""])
List of namespaces to search for a Desktop Workspace specification modules.
=back
Return value: (any)
=head2 list_desktop_workspace_items
Usage:
list_desktop_workspace_items(%args) -> [$status_code, $reason, $payload, \%result_meta]
List the items from desktop workspace specification module, with filtering options.
This function is not exported by default, but exportable.
Arguments ('*' denotes required arguments):
=over 4
=item * B<all> => I<bool>
Whether to include items that are not included by default (has property `include_by_default`=0).
=item * B<detail> => I<bool>
(No description)
=item * B<exclude_all_tags> => I<array[str]>
Exclude all items that have ALL tags specified.
=item * B<exclude_any_tags> => I<array[str]>
Exclude all items that have any tags specified.
=item * B<include_all_tags> => I<array[str]>
Include all items that have ALL tags specified.
=item * B<include_any_tags> => I<array[str]>
Include all items that have any tag specified.
=item * B<include_dir> => I<bool>
Whether to include dir items.
=item * B<include_file> => I<bool>
Whether to include file items.
=item * B<include_prog> => I<bool>
Whether to include program items.
=item * B<include_url> => I<bool>
Whether to include URL items.
=item * B<module>* => I<perl::modname>
(No description)
=item * B<module_args> => I<hash>
(No description)
=item * B<ns_prefixes> => I<array[perl::modname|str]> (default: ["DesktopWorkspace",""])
List of namespaces to search for a Desktop Workspace specification modules.
=item * B<query> => I<array[str]>
(No description)
=item * B<shuffle> => I<bool>
(No description)
=back
Returns an enveloped result (an array).
First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.
Return value: (any)
=head2 open_desktop_workspace_items
Usage:
open_desktop_workspace_items(%args) -> [$status_code, $reason, $payload, \%result_meta]
Open desktop workspace items.
This function is not exported by default, but exportable.
This function supports dry-run operation.
Arguments ('*' denotes required arguments):
=over 4
=item * B<all> => I<bool>
Whether to include items that are not included by default (has property `include_by_default`=0).
=item * B<detail> => I<bool>
(No description)
=item * B<exclude_all_tags> => I<array[str]>
Exclude all items that have ALL tags specified.
=item * B<exclude_any_tags> => I<array[str]>
Exclude all items that have any tags specified.
=item * B<include_all_tags> => I<array[str]>
Include all items that have ALL tags specified.
=item * B<include_any_tags> => I<array[str]>
Include all items that have any tag specified.
=item * B<include_dir> => I<bool>
Whether to include dir items.
=item * B<include_file> => I<bool>
Whether to include file items.
=item * B<include_prog> => I<bool>
Whether to include program items.
=item * B<include_url> => I<bool>
Whether to include URL items.
=item * B<kde_activity> => I<str>
Switch to the specified KDE activity name.
Override's desktop workspace specification's C<kde_activity> property.
=item * B<module>* => I<perl::modname>
(No description)
=item * B<module_args> => I<hash>
(No description)
=item * B<ns_prefixes> => I<array[perl::modname|str]> (default: ["DesktopWorkspace",""])
List of namespaces to search for a Desktop Workspace specification modules.
=item * B<query> => I<array[str]>
(No description)
=item * B<reuse_window> => I<bool>
Do not open if existing window is detected.
=item * B<shuffle> => I<bool>
(No description)
=back
Special arguments:
=over 4
=item * B<-dry_run> => I<bool>
Pass -dry_run=E<gt>1 to enable simulation mode.
=back
Returns an enveloped result (an array).
First element ($status_code) is an integer containing HTTP-like status code
(200 means OK, 4xx caller error, 5xx function error). Second element
($reason) is a string containing error message, or something like "OK" if status is
200. Third element ($payload) is the actual result, but usually not present when enveloped result is an error response ($status_code is not 2xx). Fourth
element (%result_meta) is called result metadata and is optional, a hash
that contains extra information, much like how HTTP response headers provide additional metadata.
Return value: (any)
=head1 HOMEPAGE
Please visit the project's homepage at L<https://metacpan.org/release/Desktop-Workspace-Util>.
=head1 SOURCE
Source repository is at L<https://github.com/perlancar/perl-Desktop-Workspace-Util>.
=head1 SEE ALSO
L<DesktopWorkspace>
( run in 2.855 seconds using v1.01-cache-2.11-cpan-b16cb0d3907 )