├── air.toml
├── .github
├── .gitignore
├── workflows
│ ├── pkgdown.yaml
│ ├── test-coverage.yaml
│ ├── R-CMD-check.yaml
│ └── pr-commands.yaml
└── CODE_OF_CONDUCT.md
├── .gitignore
├── LICENSE
├── .vscode
├── extensions.json
└── settings.json
├── NAMESPACE
├── NEWS.md
├── R
├── xopen-package.R
└── package.R
├── tests
├── testthat
│ ├── helper.R
│ └── test.R
└── testthat.R
├── codecov.yml
├── .Rbuildignore
├── _pkgdown.yml
├── man
├── wait_for_finish.Rd
├── xopen-package.Rd
└── xopen.Rd
├── DESCRIPTION
├── LICENSE.md
├── README.md
└── inst
└── xdg-open
/air.toml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | docs
2 | /revdep
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2025
2 | COPYRIGHT HOLDER: xopen authors
3 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "Posit.air-vscode"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | S3method(xopen,default)
4 | export(xopen)
5 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | # xopen (development version)
2 |
3 | # xopen 1.0.1
4 |
5 | No changes.
6 |
7 | # xopen 1.0.0
8 |
9 | First public release.
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[r]": {
3 | "editor.formatOnSave": true,
4 | "editor.defaultFormatter": "Posit.air-vscode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/R/xopen-package.R:
--------------------------------------------------------------------------------
1 | #' @aliases xopen-package NULL
2 | #' @keywords internal
3 | "_PACKAGE"
4 |
5 | ## usethis namespace: start
6 | ## usethis namespace: end
7 | NULL
8 |
--------------------------------------------------------------------------------
/tests/testthat/helper.R:
--------------------------------------------------------------------------------
1 | chrome <- function() {
2 | switch(
3 | get_os(),
4 | win = "Chrome",
5 | macos = "google chrome",
6 | other = "google-chrome"
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 |
3 | coverage:
4 | status:
5 | project:
6 | default:
7 | target: auto
8 | threshold: 1%
9 | informational: true
10 | patch:
11 | default:
12 | target: auto
13 | threshold: 1%
14 | informational: true
15 |
--------------------------------------------------------------------------------
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^appveyor\.yml$
2 | ^codecov\.yml$
3 | ^\.travis\.yml$
4 | ^.*\.Rproj$
5 | ^\.Rproj\.user$
6 | ^Makefile$
7 | ^README.Rmd$
8 | ^.travis.yml$
9 | ^appveyor.yml$
10 | ^_pkgdown\.yml$
11 | ^docs$
12 | ^pkgdown$
13 | ^\.github$
14 | ^LICENSE\.md$
15 | ^revdep$
16 | ^[\.]?air\.toml$
17 | ^\.vscode$
18 |
--------------------------------------------------------------------------------
/_pkgdown.yml:
--------------------------------------------------------------------------------
1 | url: https://r-lib.github.io/xopen/
2 | template:
3 | bootstrap: 5
4 |
5 | includes:
6 | in_header: |
7 |
8 |
9 |
10 | development:
11 | mode: auto
12 |
--------------------------------------------------------------------------------
/tests/testthat.R:
--------------------------------------------------------------------------------
1 | # This file is part of the standard setup for testthat.
2 | # It is recommended that you do not modify it.
3 | #
4 | # Where should you do additional test configuration?
5 | # Learn more about the roles of various files in:
6 | # * https://r-pkgs.org/testing-design.html#sec-tests-files-overview
7 | # * https://testthat.r-lib.org/articles/special-files.html
8 |
9 | library(testthat)
10 | library(xopen)
11 |
12 | if (ps::ps_is_supported()) {
13 | reporter <- ps::CleanupReporter(testthat::SummaryReporter)$new()
14 | } else {
15 | ## ps does not support this platform
16 | reporter <- "progress"
17 | }
18 |
19 | test_check("xopen", reporter = reporter)
20 |
--------------------------------------------------------------------------------
/man/wait_for_finish.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/package.R
3 | \name{wait_for_finish}
4 | \alias{wait_for_finish}
5 | \title{Wait for a process to finish}
6 | \usage{
7 | wait_for_finish(process, target, timeout1 = 2000, timeout2 = 5000)
8 | }
9 | \arguments{
10 | \item{process}{The process. It should not have \code{stdout} or \code{stderr}
11 | pipes, because that can make it freeze.}
12 |
13 | \item{timeout1}{Timeout before message.}
14 |
15 | \item{timeout2}{Timeout after message.}
16 | }
17 | \description{
18 | With timeout(s), and interaction, if the session is interactive.
19 | }
20 | \details{
21 | First we wait for 2s. If the process is still alive, then we give
22 | it another 5s, but first let the user know that they can interrupt
23 | the process.
24 | }
25 | \keyword{internal}
26 |
--------------------------------------------------------------------------------
/man/xopen-package.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/xopen-package.R
3 | \docType{package}
4 | \name{xopen-package}
5 | \alias{xopen-package}
6 | \title{xopen: Open System Files, 'URLs', Anything}
7 | \description{
8 | Cross platform solution to open files, directories or 'URLs' with their associated programs.
9 | }
10 | \seealso{
11 | Useful links:
12 | \itemize{
13 | \item \url{https://github.com/r-lib/xopen#readme}
14 | \item \url{https://r-lib.github.io/xopen/}
15 | \item Report bugs at \url{https://github.com/r-lib/xopen/issues}
16 | }
17 |
18 | }
19 | \author{
20 | \strong{Maintainer}: Gábor Csárdi \email{csardi.gabor@gmail.com}
21 |
22 | Authors:
23 | \itemize{
24 | \item Fathi Boudra
25 | \item Rex Dieter
26 | \item Kevin Krammer
27 | \item Jeremy White
28 | }
29 |
30 | Other contributors:
31 | \itemize{
32 | \item Posit Software, PBC [copyright holder, funder]
33 | }
34 |
35 | }
36 | \keyword{internal}
37 |
--------------------------------------------------------------------------------
/man/xopen.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/package.R
3 | \name{xopen}
4 | \alias{xopen}
5 | \title{Open a file, directory or URL}
6 | \usage{
7 | xopen(target = NULL, app = NULL, quiet = FALSE, ...)
8 | }
9 | \arguments{
10 | \item{target}{String, the path or URL to open.}
11 |
12 | \item{app}{Specify the app to open \code{target} with, and its arguments,
13 | in a character vector. Note that app names are platform dependent.}
14 |
15 | \item{quiet}{Whether to echo the command to the screen, before
16 | running it.}
17 |
18 | \item{...}{Additional arguments, not used currently.}
19 | }
20 | \description{
21 | Open a file, directory or URL, using the local platforms conventions,
22 | i.e. associated applications, default programs, etc. This is usually
23 | equivalent to double-clicking on the file in the GUI.
24 | }
25 | \section{Examples}{
26 |
27 |
28 | \if{html}{\out{
}}\preformatted{xopen("test.R")
29 | xopen("https://ps.r-lib.org")
30 | xopen(tempdir())
31 | }\if{html}{\out{
}}
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: xopen
2 | Title: Open System Files, 'URLs', Anything
3 | Version: 1.0.1.9000
4 | Authors@R: c(
5 | person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", role = c("aut", "cre")),
6 | person("Fathi", "Boudra", role = "aut"),
7 | person("Rex", "Dieter", role = "aut"),
8 | person("Kevin", "Krammer", role = "aut"),
9 | person("Jeremy", "White", role = "aut"),
10 | person("Posit Software, PBC", role = c("cph", "fnd"),
11 | comment = c(ROR = "03wc8by49"))
12 | )
13 | Description: Cross platform solution to open files, directories or 'URLs'
14 | with their associated programs.
15 | License: MIT + file LICENSE
16 | URL: https://github.com/r-lib/xopen#readme, https://r-lib.github.io/xopen/
17 | BugReports: https://github.com/r-lib/xopen/issues
18 | Depends:
19 | R (>= 3.1)
20 | Imports:
21 | processx
22 | Suggests:
23 | ps,
24 | testthat (>= 3.0.0)
25 | Config/Needs/website: tidyverse/tidytemplate
26 | Config/testthat/edition: 3
27 | Config/usethis/last-upkeep: 2025-05-07
28 | Encoding: UTF-8
29 | Roxygen: list(markdown = TRUE)
30 | RoxygenNote: 7.2.3
31 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2025 xopen authors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/pkgdown.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 | release:
8 | types: [published]
9 | workflow_dispatch:
10 |
11 | name: pkgdown.yaml
12 |
13 | permissions: read-all
14 |
15 | jobs:
16 | pkgdown:
17 | runs-on: ubuntu-latest
18 | # Only restrict concurrency for non-PR jobs
19 | concurrency:
20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
21 | env:
22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
23 | permissions:
24 | contents: write
25 | steps:
26 | - uses: actions/checkout@v4
27 |
28 | - uses: r-lib/actions/setup-pandoc@v2
29 |
30 | - uses: r-lib/actions/setup-r@v2
31 | with:
32 | use-public-rspm: true
33 |
34 | - uses: r-lib/actions/setup-r-dependencies@v2
35 | with:
36 | extra-packages: any::pkgdown, local::.
37 | needs: website
38 |
39 | - name: Build site
40 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
41 | shell: Rscript {0}
42 |
43 | - name: Deploy to GitHub pages 🚀
44 | if: github.event_name != 'pull_request'
45 | uses: JamesIves/github-pages-deploy-action@v4.5.0
46 | with:
47 | clean: false
48 | branch: gh-pages
49 | folder: docs
50 |
--------------------------------------------------------------------------------
/tests/testthat/test.R:
--------------------------------------------------------------------------------
1 | test_that("xopen works", {
2 | if (Sys.getenv("TESTTHAT_INTERACTIVE") == "") {
3 | skip("Need to test this interactively")
4 | }
5 |
6 | ## File
7 | expect_error(xopen("test.R", quiet = TRUE), NA)
8 |
9 | ## URL
10 | expect_error(xopen("https://ps.r-lib.org", quiet = TRUE), NA)
11 |
12 | ## URL with given app
13 | expect_error(
14 | xopen("https://processx.r-lib.org", app = chrome(), quiet = TRUE),
15 | NA
16 | )
17 |
18 | ## App only, no target
19 | expect_error(xopen(app = chrome(), quiet = TRUE), NA)
20 |
21 | ## App and arguments (need to quit Chrome for this to work...)
22 | expect_error(
23 | xopen(
24 | app = c(chrome(), "--incognito", "https://github.com"),
25 | quiet = TRUE
26 | ),
27 | NA
28 | )
29 | })
30 |
31 | test_that("URLs with spaces", {
32 | if (Sys.getenv("TESTTHAT_INTERACTIVE") == "") {
33 | skip("Need to test this interactively")
34 | }
35 |
36 | expect_error(
37 | xopen("https://google.com/search?q=a b c", quiet = TRUE),
38 | NA
39 | )
40 | })
41 |
42 | test_that("errors", {
43 | skip_on_cran()
44 | expect_error(xopen2(tempfile(), quiet = TRUE, timeout1 = 10, timeout2 = 10))
45 | })
46 |
47 | test_that("wait_for_finish", {
48 | px <- get("get_tool", asNamespace("processx"))("px")
49 | proc <- processx::process$new(
50 | px,
51 | c("errln", "message", "sleep", "100"),
52 | stderr = tempfile()
53 | )
54 | on.exit(proc$kill(), add = TRUE)
55 |
56 | proc$poll_io(1000)
57 | expect_error(wait_for_finish(proc, "target", 10, 10), "Could not open")
58 | expect_error(wait_for_finish(proc, "target", 10, 10), "Standard error")
59 | })
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # xopen
3 |
4 | > Open System Files, URLs, Anything
5 |
6 |
7 | [](https://github.com/r-lib/xopen/actions/workflows/R-CMD-check.yaml)
8 | [](https://www.r-pkg.org/pkg/xopen)
9 | [](https://www.r-pkg.org/pkg/xopen)
10 | [](https://app.codecov.io/gh/r-lib/xopen)
11 |
12 |
13 | Cross platform solution to open files, directories or URLs with their
14 | associated programs. Inspired by `shell.exec()`,
15 | https://github.com/pwnall/node-open and
16 | https://github.com/sindresorhus/opn
17 |
18 | ## Installation
19 |
20 | Stable version:
21 |
22 | ```r
23 | install.packages("xopen")
24 | ```
25 |
26 | Development version:
27 |
28 | ```r
29 | pak::pak("r-lib/xopen")
30 | ```
31 |
32 | ## Usage
33 |
34 | ```r
35 | library(xopen)
36 | ```
37 |
38 | Open a file:
39 |
40 | ```r
41 | xopen("test.R")
42 | ```
43 |
44 | Open a URL:
45 |
46 | ```r
47 | xopen("https://ps.r-lib.org")
48 | ```
49 |
50 | URL with given app:
51 |
52 | ```r
53 | chrome <- function() {
54 | switch(
55 | get_os(),
56 | win = "Chrome",
57 | macos = "google chrome",
58 | other = "google-chrome")
59 | }
60 | xopen("https://processx.r-lib.org", app = chrome())
61 | ```
62 |
63 | Open a given app (or switch to it, if already open):
64 |
65 | ```r
66 | xopen(app = chrome())
67 | ```
68 |
69 | App and arguments. (You need to quit Chrome for this to work):
70 | ```r
71 | xopen(app = c(chrome(), "--incognito", "https://github.com"))
72 | ```
73 |
74 | ## License
75 |
76 | MIT © RStudio
77 |
--------------------------------------------------------------------------------
/.github/workflows/test-coverage.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 |
8 | name: test-coverage.yaml
9 |
10 | permissions: read-all
11 |
12 | jobs:
13 | test-coverage:
14 | runs-on: ubuntu-latest
15 | env:
16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
17 |
18 | steps:
19 | - uses: actions/checkout@v4
20 |
21 | - uses: r-lib/actions/setup-r@v2
22 | with:
23 | use-public-rspm: true
24 |
25 | - uses: r-lib/actions/setup-r-dependencies@v2
26 | with:
27 | extra-packages: any::covr, any::xml2
28 | needs: coverage
29 |
30 | - name: Test coverage
31 | run: |
32 | cov <- covr::package_coverage(
33 | quiet = FALSE,
34 | clean = FALSE,
35 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package")
36 | )
37 | print(cov)
38 | covr::to_cobertura(cov)
39 | shell: Rscript {0}
40 |
41 | - uses: codecov/codecov-action@v5
42 | with:
43 | # Fail if error if not on PR, or if on PR and token is given
44 | fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }}
45 | files: ./cobertura.xml
46 | plugins: noop
47 | disable_search: true
48 | token: ${{ secrets.CODECOV_TOKEN }}
49 |
50 | - name: Show testthat output
51 | if: always()
52 | run: |
53 | ## --------------------------------------------------------------------
54 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true
55 | shell: bash
56 |
57 | - name: Upload test results
58 | if: failure()
59 | uses: actions/upload-artifact@v4
60 | with:
61 | name: coverage-test-failures
62 | path: ${{ runner.temp }}/package
63 |
--------------------------------------------------------------------------------
/.github/workflows/R-CMD-check.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | #
4 | # NOTE: This workflow is overkill for most R packages and
5 | # check-standard.yaml is likely a better choice.
6 | # usethis::use_github_action("check-standard") will install it.
7 | on:
8 | push:
9 | branches: [main, master]
10 | pull_request:
11 |
12 | name: R-CMD-check.yaml
13 |
14 | permissions: read-all
15 |
16 | jobs:
17 | R-CMD-check:
18 | runs-on: ${{ matrix.config.os }}
19 |
20 | name: ${{ matrix.config.os }} (${{ matrix.config.r }})
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | config:
26 | - {os: macos-latest, r: 'release'}
27 |
28 | - {os: windows-latest, r: 'release'}
29 | # use 4.0 or 4.1 to check with rtools40's older compiler
30 | - {os: windows-latest, r: 'oldrel-4'}
31 |
32 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
33 | - {os: ubuntu-latest, r: 'release'}
34 | - {os: ubuntu-latest, r: 'oldrel-1'}
35 | - {os: ubuntu-latest, r: 'oldrel-2'}
36 | - {os: ubuntu-latest, r: 'oldrel-3'}
37 | - {os: ubuntu-latest, r: 'oldrel-4'}
38 |
39 | env:
40 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
41 | R_KEEP_PKG_SOURCE: yes
42 |
43 | steps:
44 | - uses: actions/checkout@v4
45 |
46 | - uses: r-lib/actions/setup-pandoc@v2
47 |
48 | - uses: r-lib/actions/setup-r@v2
49 | with:
50 | r-version: ${{ matrix.config.r }}
51 | http-user-agent: ${{ matrix.config.http-user-agent }}
52 | use-public-rspm: true
53 |
54 | - uses: r-lib/actions/setup-r-dependencies@v2
55 | with:
56 | extra-packages: any::rcmdcheck
57 | needs: check
58 |
59 | - uses: r-lib/actions/check-r-package@v2
60 | with:
61 | upload-snapshots: true
62 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
63 |
--------------------------------------------------------------------------------
/.github/workflows/pr-commands.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | issue_comment:
5 | types: [created]
6 |
7 | name: pr-commands.yaml
8 |
9 | permissions: read-all
10 |
11 | jobs:
12 | document:
13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }}
14 | name: document
15 | runs-on: ubuntu-latest
16 | env:
17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
18 | permissions:
19 | contents: write
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - uses: r-lib/actions/pr-fetch@v2
24 | with:
25 | repo-token: ${{ secrets.GITHUB_TOKEN }}
26 |
27 | - uses: r-lib/actions/setup-r@v2
28 | with:
29 | use-public-rspm: true
30 |
31 | - uses: r-lib/actions/setup-r-dependencies@v2
32 | with:
33 | extra-packages: any::roxygen2
34 | needs: pr-document
35 |
36 | - name: Document
37 | run: roxygen2::roxygenise()
38 | shell: Rscript {0}
39 |
40 | - name: commit
41 | run: |
42 | git config --local user.name "$GITHUB_ACTOR"
43 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
44 | git add man/\* NAMESPACE
45 | git commit -m 'Document'
46 |
47 | - uses: r-lib/actions/pr-push@v2
48 | with:
49 | repo-token: ${{ secrets.GITHUB_TOKEN }}
50 |
51 | style:
52 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }}
53 | name: style
54 | runs-on: ubuntu-latest
55 | env:
56 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
57 | permissions:
58 | contents: write
59 | steps:
60 | - uses: actions/checkout@v4
61 |
62 | - uses: r-lib/actions/pr-fetch@v2
63 | with:
64 | repo-token: ${{ secrets.GITHUB_TOKEN }}
65 |
66 | - uses: r-lib/actions/setup-r@v2
67 |
68 | - name: Install dependencies
69 | run: install.packages("styler")
70 | shell: Rscript {0}
71 |
72 | - name: Style
73 | run: styler::style_pkg()
74 | shell: Rscript {0}
75 |
76 | - name: commit
77 | run: |
78 | git config --local user.name "$GITHUB_ACTOR"
79 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
80 | git add \*.R
81 | git commit -m 'Style'
82 |
83 | - uses: r-lib/actions/pr-push@v2
84 | with:
85 | repo-token: ${{ secrets.GITHUB_TOKEN }}
86 |
--------------------------------------------------------------------------------
/R/package.R:
--------------------------------------------------------------------------------
1 | #' Open a file, directory or URL
2 | #'
3 | #' Open a file, directory or URL, using the local platforms conventions,
4 | #' i.e. associated applications, default programs, etc. This is usually
5 | #' equivalent to double-clicking on the file in the GUI.
6 | #'
7 | #' @param target String, the path or URL to open.
8 | #' @param app Specify the app to open `target` with, and its arguments,
9 | #' in a character vector. Note that app names are platform dependent.
10 | #' @param quiet Whether to echo the command to the screen, before
11 | #' running it.
12 | #' @param ... Additional arguments, not used currently.
13 | #'
14 | #' @section Examples:
15 | #' ```
16 | #' xopen("test.R")
17 | #' xopen("https://ps.r-lib.org")
18 | #' xopen(tempdir())
19 | #' ```
20 | #' @export
21 |
22 | xopen <- function(target = NULL, app = NULL, quiet = FALSE, ...)
23 | UseMethod("xopen")
24 |
25 | #' @export
26 |
27 | xopen.default <- function(target = NULL, app = NULL, quiet = FALSE, ...) {
28 | xopen2(target, app, quiet)
29 | }
30 |
31 | xopen2 <- function(target, app, quiet, timeout1 = 2000, timeout2 = 5000) {
32 | os <- get_os()
33 | fun <- switch(os, win = xopen_win, macos = xopen_macos, xopen_other)
34 | par <- fun(target, app)
35 |
36 | err <- tempfile()
37 | on.exit(unlink(err, recursive = TRUE), add = TRUE)
38 | px <- processx::process$new(
39 | par[[1]],
40 | par[[2]],
41 | stderr = err,
42 | echo_cmd = !quiet
43 | )
44 |
45 | ## Cleanup, if needed
46 | if (par[[3]]) wait_for_finish(px, target, timeout1, timeout2)
47 |
48 | invisible(px)
49 | }
50 |
51 | get_os <- function() {
52 | if (.Platform$OS.type == "windows") {
53 | "win"
54 | } else if (Sys.info()[["sysname"]] == "Darwin") {
55 | "macos"
56 | } else {
57 | "other"
58 | }
59 | }
60 |
61 | xopen_macos <- function(target, app) {
62 | cmd <- "open"
63 | args <- if (length(app)) c("-a", app[1])
64 | args <- c(args, target)
65 | if (length(app)) args <- c(args, "--args", app[-1])
66 | list(cmd, args, TRUE)
67 | }
68 |
69 | xopen_win <- function(target, app) {
70 | cmd <- "cmd"
71 | args <- c("/c", "start", "\"\"", "/b")
72 | target <- gsub("&", "^&", target)
73 | if (length(app)) args <- c(args, app)
74 | args <- c(args, target)
75 | list(cmd, args, TRUE)
76 | }
77 |
78 | xopen_other <- function(target, app) {
79 | if (length(app)) {
80 | cmd <- app[1]
81 | args <- app[-1]
82 | cleanup <- FALSE
83 | } else {
84 | cmd <- Sys.which("xdg-open")
85 | if (cmd == "") cmd <- system.file("xdg-open", package = "xopen")
86 | args <- character()
87 | cleanup <- TRUE
88 | }
89 | args <- c(args, target)
90 | list(cmd, args, cleanup)
91 | }
92 |
93 | #' Wait for a process to finish
94 | #'
95 | #' With timeout(s), and interaction, if the session is interactive.
96 | #'
97 | #' First we wait for 2s. If the process is still alive, then we give
98 | #' it another 5s, but first let the user know that they can interrupt
99 | #' the process.
100 | #'
101 | #' @param process The process. It should not have `stdout` or `stderr`
102 | #' pipes, because that can make it freeze.
103 | #' @param timeout1 Timeout before message.
104 | #' @param timeout2 Timeout after message.
105 | #'
106 | #' @keywords internal
107 |
108 | wait_for_finish <- function(process, target, timeout1 = 2000, timeout2 = 5000) {
109 | on.exit(process$kill(), add = TRUE)
110 | process$wait(timeout = timeout1)
111 | if (process$is_alive()) {
112 | message(
113 | "Still trying to open ",
114 | encodeString(target, quote = "'"),
115 | ", you can interrupt any time"
116 | )
117 | process$wait(timeout = timeout2)
118 | process$kill()
119 | }
120 | if (stat <- process$get_exit_status()) {
121 | err <- if (file.exists(ef <- process$get_error_file())) readLines(ef)
122 | stop(
123 | call. = FALSE,
124 | "Could not open ",
125 | encodeString(target, quote = "'"),
126 | "\n",
127 | "Exit status: ",
128 | stat,
129 | "\n",
130 | if (length(err) && nzchar(err))
131 | paste("Standard error:", err, collapse = "\n")
132 | )
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at codeofconduct@posit.co.
63 | All complaints will be reviewed and investigated promptly and fairly.
64 |
65 | All community leaders are obligated to respect the privacy and security of the
66 | reporter of any incident.
67 |
68 | ## Enforcement Guidelines
69 |
70 | Community leaders will follow these Community Impact Guidelines in determining
71 | the consequences for any action they deem in violation of this Code of Conduct:
72 |
73 | ### 1. Correction
74 |
75 | **Community Impact**: Use of inappropriate language or other behavior deemed
76 | unprofessional or unwelcome in the community.
77 |
78 | **Consequence**: A private, written warning from community leaders, providing
79 | clarity around the nature of the violation and an explanation of why the
80 | behavior was inappropriate. A public apology may be requested.
81 |
82 | ### 2. Warning
83 |
84 | **Community Impact**: A violation through a single incident or series of
85 | actions.
86 |
87 | **Consequence**: A warning with consequences for continued behavior. No
88 | interaction with the people involved, including unsolicited interaction with
89 | those enforcing the Code of Conduct, for a specified period of time. This
90 | includes avoiding interactions in community spaces as well as external channels
91 | like social media. Violating these terms may lead to a temporary or permanent
92 | ban.
93 |
94 | ### 3. Temporary Ban
95 |
96 | **Community Impact**: A serious violation of community standards, including
97 | sustained inappropriate behavior.
98 |
99 | **Consequence**: A temporary ban from any sort of interaction or public
100 | communication with the community for a specified period of time. No public or
101 | private interaction with the people involved, including unsolicited interaction
102 | with those enforcing the Code of Conduct, is allowed during this period.
103 | Violating these terms may lead to a permanent ban.
104 |
105 | ### 4. Permanent Ban
106 |
107 | **Community Impact**: Demonstrating a pattern of violation of community
108 | standards, including sustained inappropriate behavior, harassment of an
109 | individual, or aggression toward or disparagement of classes of individuals.
110 |
111 | **Consequence**: A permanent ban from any sort of public interaction within the
112 | community.
113 |
114 | ## Attribution
115 |
116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
117 | version 2.1, available at
118 | .
119 |
120 | Community Impact Guidelines were inspired by
121 | [Mozilla's code of conduct enforcement ladder][https://github.com/mozilla/inclusion].
122 |
123 | For answers to common questions about this code of conduct, see the FAQ at
124 | . Translations are available at .
125 |
126 | [homepage]: https://www.contributor-covenant.org
127 |
--------------------------------------------------------------------------------
/inst/xdg-open:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | #---------------------------------------------
3 | # xdg-open
4 | #
5 | # Utility script to open a URL in the registered default application.
6 | #
7 | # Refer to the usage() function below for usage.
8 | #
9 | # Copyright 2009-2010, Fathi Boudra
10 | # Copyright 2009-2010, Rex Dieter
11 | # Copyright 2006, Kevin Krammer
12 | # Copyright 2006, Jeremy White
13 | #
14 | # LICENSE:
15 | #
16 | # Permission is hereby granted, free of charge, to any person obtaining a
17 | # copy of this software and associated documentation files (the "Software"),
18 | # to deal in the Software without restriction, including without limitation
19 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
20 | # and/or sell copies of the Software, and to permit persons to whom the
21 | # Software is furnished to do so, subject to the following conditions:
22 | #
23 | # The above copyright notice and this permission notice shall be included
24 | # in all copies or substantial portions of the Software.
25 | #
26 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
27 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
29 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
30 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
31 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32 | # OTHER DEALINGS IN THE SOFTWARE.
33 | #
34 | #---------------------------------------------
35 |
36 | manualpage()
37 | {
38 | cat << _MANUALPAGE
39 | Name
40 |
41 | xdg-open - opens a file or URL in the user's preferred
42 | application
43 |
44 | Synopsis
45 |
46 | xdg-open { file | URL }
47 |
48 | xdg-open { --help | --manual | --version }
49 |
50 | Description
51 |
52 | xdg-open opens a file or URL in the user's preferred
53 | application. If a URL is provided the URL will be opened in the
54 | user's preferred web browser. If a file is provided the file
55 | will be opened in the preferred application for files of that
56 | type. xdg-open supports file, ftp, http and https URLs.
57 |
58 | xdg-open is for use inside a desktop session only. It is not
59 | recommended to use xdg-open as root.
60 |
61 | Options
62 |
63 | --help
64 | Show command synopsis.
65 |
66 | --manual
67 | Show this manual page.
68 |
69 | --version
70 | Show the xdg-utils version information.
71 |
72 | Exit Codes
73 |
74 | An exit code of 0 indicates success while a non-zero exit code
75 | indicates failure. The following failure codes can be returned:
76 |
77 | 1
78 | Error in command line syntax.
79 |
80 | 2
81 | One of the files passed on the command line did not
82 | exist.
83 |
84 | 3
85 | A required tool could not be found.
86 |
87 | 4
88 | The action failed.
89 |
90 | See Also
91 |
92 | xdg-mime(1), xdg-settings(1), MIME applications associations
93 | specification
94 |
95 | Examples
96 |
97 | xdg-open 'http://www.freedesktop.org/'
98 |
99 | Opens the freedesktop.org website in the user's default
100 | browser.
101 |
102 | xdg-open /tmp/foobar.png
103 |
104 | Opens the PNG image file /tmp/foobar.png in the user's default
105 | image viewing application.
106 | _MANUALPAGE
107 | }
108 |
109 | usage()
110 | {
111 | cat << _USAGE
112 | xdg-open - opens a file or URL in the user's preferred
113 | application
114 |
115 | Synopsis
116 |
117 | xdg-open { file | URL }
118 |
119 | xdg-open { --help | --manual | --version }
120 |
121 | _USAGE
122 | }
123 |
124 | #@xdg-utils-common@
125 |
126 | #----------------------------------------------------------------------------
127 | # Common utility functions included in all XDG wrapper scripts
128 | #----------------------------------------------------------------------------
129 |
130 | DEBUG()
131 | {
132 | [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && return 0;
133 | [ ${XDG_UTILS_DEBUG_LEVEL} -lt $1 ] && return 0;
134 | shift
135 | echo "$@" >&2
136 | }
137 |
138 | # This handles backslashes but not quote marks.
139 | first_word()
140 | {
141 | read first rest
142 | echo "$first"
143 | }
144 |
145 | #-------------------------------------------------------------
146 | # map a binary to a .desktop file
147 | binary_to_desktop_file()
148 | {
149 | search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
150 | binary="`which "$1"`"
151 | binary="`readlink -f "$binary"`"
152 | base="`basename "$binary"`"
153 | IFS=:
154 | for dir in $search; do
155 | unset IFS
156 | [ "$dir" ] || continue
157 | [ -d "$dir/applications" ] || [ -d "$dir/applnk" ] || continue
158 | for file in "$dir"/applications/*.desktop "$dir"/applications/*/*.desktop "$dir"/applnk/*.desktop "$dir"/applnk/*/*.desktop; do
159 | [ -r "$file" ] || continue
160 | # Check to make sure it's worth the processing.
161 | grep -q "^Exec.*$base" "$file" || continue
162 | # Make sure it's a visible desktop file (e.g. not "preferred-web-browser.desktop").
163 | grep -Eq "^(NoDisplay|Hidden)=true" "$file" && continue
164 | command="`grep -E "^Exec(\[[^]=]*])?=" "$file" | cut -d= -f 2- | first_word`"
165 | command="`which "$command"`"
166 | if [ x"`readlink -f "$command"`" = x"$binary" ]; then
167 | # Fix any double slashes that got added path composition
168 | echo "$file" | sed -e 's,//*,/,g'
169 | return
170 | fi
171 | done
172 | done
173 | }
174 |
175 | #-------------------------------------------------------------
176 | # map a .desktop file to a binary
177 | desktop_file_to_binary()
178 | {
179 | search="${XDG_DATA_HOME:-$HOME/.local/share}:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
180 | desktop="`basename "$1"`"
181 | IFS=:
182 | for dir in $search; do
183 | unset IFS
184 | [ "$dir" ] && [ -d "$dir/applications" ] || [ -d "$dir/applnk" ] || continue
185 | # Check if desktop file contains -
186 | if [ "${desktop#*-}" != "$desktop" ]; then
187 | vendor=${desktop%-*}
188 | app=${desktop#*-}
189 | if [ -r $dir/applications/$vendor/$app ]; then
190 | file_path=$dir/applications/$vendor/$app
191 | elif [ -r $dir/applnk/$vendor/$app ]; then
192 | file_path=$dir/applnk/$vendor/$app
193 | fi
194 | fi
195 | if test -z "$file_path" ; then
196 | for indir in "$dir"/applications/ "$dir"/applications/*/ "$dir"/applnk/ "$dir"/applnk/*/; do
197 | file="$indir/$desktop"
198 | if [ -r "$file" ]; then
199 | file_path=$file
200 | break
201 | fi
202 | done
203 | fi
204 | if [ -r "$file_path" ]; then
205 | # Remove any arguments (%F, %f, %U, %u, etc.).
206 | command="`grep -E "^Exec(\[[^]=]*])?=" "$file_path" | cut -d= -f 2- | first_word`"
207 | command="`which "$command"`"
208 | readlink -f "$command"
209 | return
210 | fi
211 | done
212 | }
213 |
214 | #-------------------------------------------------------------
215 | # Exit script on successfully completing the desired operation
216 |
217 | exit_success()
218 | {
219 | if [ $# -gt 0 ]; then
220 | echo "$@"
221 | echo
222 | fi
223 |
224 | exit 0
225 | }
226 |
227 |
228 | #-----------------------------------------
229 | # Exit script on malformed arguments, not enough arguments
230 | # or missing required option.
231 | # prints usage information
232 |
233 | exit_failure_syntax()
234 | {
235 | if [ $# -gt 0 ]; then
236 | echo "xdg-open: $@" >&2
237 | echo "Try 'xdg-open --help' for more information." >&2
238 | else
239 | usage
240 | echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info."
241 | fi
242 |
243 | exit 1
244 | }
245 |
246 | #-------------------------------------------------------------
247 | # Exit script on missing file specified on command line
248 |
249 | exit_failure_file_missing()
250 | {
251 | if [ $# -gt 0 ]; then
252 | echo "xdg-open: $@" >&2
253 | fi
254 |
255 | exit 2
256 | }
257 |
258 | #-------------------------------------------------------------
259 | # Exit script on failure to locate necessary tool applications
260 |
261 | exit_failure_operation_impossible()
262 | {
263 | if [ $# -gt 0 ]; then
264 | echo "xdg-open: $@" >&2
265 | fi
266 |
267 | exit 3
268 | }
269 |
270 | #-------------------------------------------------------------
271 | # Exit script on failure returned by a tool application
272 |
273 | exit_failure_operation_failed()
274 | {
275 | if [ $# -gt 0 ]; then
276 | echo "xdg-open: $@" >&2
277 | fi
278 |
279 | exit 4
280 | }
281 |
282 | #------------------------------------------------------------
283 | # Exit script on insufficient permission to read a specified file
284 |
285 | exit_failure_file_permission_read()
286 | {
287 | if [ $# -gt 0 ]; then
288 | echo "xdg-open: $@" >&2
289 | fi
290 |
291 | exit 5
292 | }
293 |
294 | #------------------------------------------------------------
295 | # Exit script on insufficient permission to write a specified file
296 |
297 | exit_failure_file_permission_write()
298 | {
299 | if [ $# -gt 0 ]; then
300 | echo "xdg-open: $@" >&2
301 | fi
302 |
303 | exit 6
304 | }
305 |
306 | check_input_file()
307 | {
308 | if [ ! -e "$1" ]; then
309 | exit_failure_file_missing "file '$1' does not exist"
310 | fi
311 | if [ ! -r "$1" ]; then
312 | exit_failure_file_permission_read "no permission to read file '$1'"
313 | fi
314 | }
315 |
316 | check_vendor_prefix()
317 | {
318 | file_label="$2"
319 | [ -n "$file_label" ] || file_label="filename"
320 | file=`basename "$1"`
321 | case "$file" in
322 | [[:alpha:]]*-*)
323 | return
324 | ;;
325 | esac
326 |
327 | echo "xdg-open: $file_label '$file' does not have a proper vendor prefix" >&2
328 | echo 'A vendor prefix consists of alpha characters ([a-zA-Z]) and is terminated' >&2
329 | echo 'with a dash ("-"). An example '"$file_label"' is '"'example-$file'" >&2
330 | echo "Use --novendor to override or 'xdg-open --manual' for additional info." >&2
331 | exit 1
332 | }
333 |
334 | check_output_file()
335 | {
336 | # if the file exists, check if it is writeable
337 | # if it does not exists, check if we are allowed to write on the directory
338 | if [ -e "$1" ]; then
339 | if [ ! -w "$1" ]; then
340 | exit_failure_file_permission_write "no permission to write to file '$1'"
341 | fi
342 | else
343 | DIR=`dirname "$1"`
344 | if [ ! -w "$DIR" ] || [ ! -x "$DIR" ]; then
345 | exit_failure_file_permission_write "no permission to create file '$1'"
346 | fi
347 | fi
348 | }
349 |
350 | #----------------------------------------
351 | # Checks for shared commands, e.g. --help
352 |
353 | check_common_commands()
354 | {
355 | while [ $# -gt 0 ] ; do
356 | parm="$1"
357 | shift
358 |
359 | case "$parm" in
360 | --help)
361 | usage
362 | echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info."
363 | exit_success
364 | ;;
365 |
366 | --manual)
367 | manualpage
368 | exit_success
369 | ;;
370 |
371 | --version)
372 | echo "xdg-open 1.1.2"
373 | exit_success
374 | ;;
375 | esac
376 | done
377 | }
378 |
379 | check_common_commands "$@"
380 |
381 | [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && unset XDG_UTILS_DEBUG_LEVEL;
382 | if [ ${XDG_UTILS_DEBUG_LEVEL-0} -lt 1 ]; then
383 | # Be silent
384 | xdg_redirect_output=" > /dev/null 2> /dev/null"
385 | else
386 | # All output to stderr
387 | xdg_redirect_output=" >&2"
388 | fi
389 |
390 | #--------------------------------------
391 | # Checks for known desktop environments
392 | # set variable DE to the desktop environments name, lowercase
393 |
394 | detectDE()
395 | {
396 | # see https://bugs.freedesktop.org/show_bug.cgi?id=34164
397 | unset GREP_OPTIONS
398 |
399 | if [ -n "${XDG_CURRENT_DESKTOP}" ]; then
400 | case "${XDG_CURRENT_DESKTOP}" in
401 | # only recently added to menu-spec, pre-spec X- still in use
402 | Cinnamon|X-Cinnamon)
403 | DE=cinnamon;
404 | ;;
405 | ENLIGHTENMENT)
406 | DE=enlightenment;
407 | ;;
408 | # GNOME, GNOME-Classic:GNOME, or GNOME-Flashback:GNOME
409 | GNOME*)
410 | DE=gnome;
411 | ;;
412 | KDE)
413 | DE=kde;
414 | ;;
415 | LXDE)
416 | DE=lxde;
417 | ;;
418 | LXQt)
419 | DE=lxqt;
420 | ;;
421 | MATE)
422 | DE=mate;
423 | ;;
424 | XFCE)
425 | DE=xfce
426 | ;;
427 | X-Generic)
428 | DE=generic
429 | ;;
430 | esac
431 | fi
432 |
433 | if [ x"$DE" = x"" ]; then
434 | # classic fallbacks
435 | if [ x"$KDE_FULL_SESSION" != x"" ]; then DE=kde;
436 | elif [ x"$GNOME_DESKTOP_SESSION_ID" != x"" ]; then DE=gnome;
437 | elif [ x"$MATE_DESKTOP_SESSION_ID" != x"" ]; then DE=mate;
438 | elif `dbus-send --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.gnome.SessionManager > /dev/null 2>&1` ; then DE=gnome;
439 | elif xprop -root _DT_SAVE_MODE 2> /dev/null | grep ' = \"xfce4\"$' >/dev/null 2>&1; then DE=xfce;
440 | elif xprop -root 2> /dev/null | grep -i '^xfce_desktop_window' >/dev/null 2>&1; then DE=xfce
441 | elif echo $DESKTOP | grep -q '^Enlightenment'; then DE=enlightenment;
442 | elif [ x"$LXQT_SESSION_CONFIG" != x"" ]; then DE=lxqt;
443 | fi
444 | fi
445 |
446 | if [ x"$DE" = x"" ]; then
447 | # fallback to checking $DESKTOP_SESSION
448 | case "$DESKTOP_SESSION" in
449 | gnome)
450 | DE=gnome;
451 | ;;
452 | LXDE|Lubuntu)
453 | DE=lxde;
454 | ;;
455 | MATE)
456 | DE=mate;
457 | ;;
458 | xfce|xfce4|'Xfce Session')
459 | DE=xfce;
460 | ;;
461 | esac
462 | fi
463 |
464 | if [ x"$DE" = x"" ]; then
465 | # fallback to uname output for other platforms
466 | case "$(uname 2>/dev/null)" in
467 | CYGWIN*)
468 | DE=cygwin;
469 | ;;
470 | Darwin)
471 | DE=darwin;
472 | ;;
473 | esac
474 | fi
475 |
476 | if [ x"$DE" = x"gnome" ]; then
477 | # gnome-default-applications-properties is only available in GNOME 2.x
478 | # but not in GNOME 3.x
479 | which gnome-default-applications-properties > /dev/null 2>&1 || DE="gnome3"
480 | fi
481 |
482 | if [ -f "$XDG_RUNTIME_DIR/flatpak-info" ]; then
483 | DE="flatpak"
484 | fi
485 | }
486 |
487 | #----------------------------------------------------------------------------
488 | # kfmclient exec/openURL can give bogus exit value in KDE <= 3.5.4
489 | # It also always returns 1 in KDE 3.4 and earlier
490 | # Simply return 0 in such case
491 |
492 | kfmclient_fix_exit_code()
493 | {
494 | version=`LC_ALL=C.UTF-8 kde-config --version 2>/dev/null | grep '^KDE'`
495 | major=`echo $version | sed 's/KDE.*: \([0-9]\).*/\1/'`
496 | minor=`echo $version | sed 's/KDE.*: [0-9]*\.\([0-9]\).*/\1/'`
497 | release=`echo $version | sed 's/KDE.*: [0-9]*\.[0-9]*\.\([0-9]\).*/\1/'`
498 | test "$major" -gt 3 && return $1
499 | test "$minor" -gt 5 && return $1
500 | test "$release" -gt 4 && return $1
501 | return 0
502 | }
503 |
504 | #----------------------------------------------------------------------------
505 | # Returns true if there is a graphical display attached.
506 |
507 | has_display()
508 | {
509 | if [ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ]; then
510 | return 0
511 | else
512 | return 1
513 | fi
514 | }
515 |
516 | # This handles backslashes but not quote marks.
517 | last_word()
518 | {
519 | read first rest
520 | echo "$rest"
521 | }
522 |
523 | # Get the value of a key in a desktop file's Desktop Entry group.
524 | # Example: Use get_key foo.desktop Exec
525 | # to get the values of the Exec= key for the Desktop Entry group.
526 | get_key()
527 | {
528 | local file="${1}"
529 | local key="${2}"
530 | local desktop_entry=""
531 |
532 | IFS_="${IFS}"
533 | IFS=""
534 | while read line
535 | do
536 | case "$line" in
537 | "[Desktop Entry]")
538 | desktop_entry="y"
539 | ;;
540 | # Reset match flag for other groups
541 | "["*)
542 | desktop_entry=""
543 | ;;
544 | "${key}="*)
545 | # Only match Desktop Entry group
546 | if [ -n "${desktop_entry}" ]
547 | then
548 | echo "${line}" | cut -d= -f 2-
549 | fi
550 | esac
551 | done < "${file}"
552 | IFS="${IFS_}"
553 | }
554 |
555 | # Returns true if argument is a file:// URL or path
556 | is_file_url_or_path()
557 | {
558 | if echo "$1" | grep -q '^file://' \
559 | || ! echo "$1" | egrep -q '^[[:alpha:]+\.\-]+:'; then
560 | return 0
561 | else
562 | return 1
563 | fi
564 | }
565 |
566 | # If argument is a file URL, convert it to a (percent-decoded) path.
567 | # If not, leave it as it is.
568 | file_url_to_path()
569 | {
570 | local file="$1"
571 | if echo "$file" | grep -q '^file:///'; then
572 | file=${file#file://}
573 | file=${file%%#*}
574 | file=$(echo "$file" | sed -r 's/\?.*$//')
575 | local printf=printf
576 | if [ -x /usr/bin/printf ]; then
577 | printf=/usr/bin/printf
578 | fi
579 | file=$($printf "$(echo "$file" | sed -e 's@%\([a-f0-9A-F]\{2\}\)@\\x\1@g')")
580 | fi
581 | echo "$file"
582 | }
583 |
584 | open_cygwin()
585 | {
586 | cygstart "$1"
587 |
588 | if [ $? -eq 0 ]; then
589 | exit_success
590 | else
591 | exit_failure_operation_failed
592 | fi
593 | }
594 |
595 | open_darwin()
596 | {
597 | open "$1"
598 |
599 | if [ $? -eq 0 ]; then
600 | exit_success
601 | else
602 | exit_failure_operation_failed
603 | fi
604 | }
605 |
606 | open_kde()
607 | {
608 | if [ -n "${KDE_SESSION_VERSION}" ]; then
609 | case "${KDE_SESSION_VERSION}" in
610 | 4)
611 | kde-open "$1"
612 | ;;
613 | 5)
614 | kde-open${KDE_SESSION_VERSION} "$1"
615 | ;;
616 | esac
617 | else
618 | kfmclient exec "$1"
619 | kfmclient_fix_exit_code $?
620 | fi
621 |
622 | if [ $? -eq 0 ]; then
623 | exit_success
624 | else
625 | exit_failure_operation_failed
626 | fi
627 | }
628 |
629 | open_gnome3()
630 | {
631 | if gio help open 2>/dev/null 1>&2; then
632 | gio open "$1"
633 | elif gvfs-open --help 2>/dev/null 1>&2; then
634 | gvfs-open "$1"
635 | else
636 | open_generic "$1"
637 | fi
638 |
639 | if [ $? -eq 0 ]; then
640 | exit_success
641 | else
642 | exit_failure_operation_failed
643 | fi
644 | }
645 |
646 | open_gnome()
647 | {
648 | if gio help open 2>/dev/null 1>&2; then
649 | gio open "$1"
650 | elif gvfs-open --help 2>/dev/null 1>&2; then
651 | gvfs-open "$1"
652 | elif gnome-open --help 2>/dev/null 1>&2; then
653 | gnome-open "$1"
654 | else
655 | open_generic "$1"
656 | fi
657 |
658 | if [ $? -eq 0 ]; then
659 | exit_success
660 | else
661 | exit_failure_operation_failed
662 | fi
663 | }
664 |
665 | open_mate()
666 | {
667 | if gio help open 2>/dev/null 1>&2; then
668 | gio open "$1"
669 | elif gvfs-open --help 2>/dev/null 1>&2; then
670 | gvfs-open "$1"
671 | elif mate-open --help 2>/dev/null 1>&2; then
672 | mate-open "$1"
673 | else
674 | open_generic "$1"
675 | fi
676 |
677 | if [ $? -eq 0 ]; then
678 | exit_success
679 | else
680 | exit_failure_operation_failed
681 | fi
682 | }
683 |
684 | open_xfce()
685 | {
686 | if exo-open --help 2>/dev/null 1>&2; then
687 | exo-open "$1"
688 | elif gio help open 2>/dev/null 1>&2; then
689 | gio open "$1"
690 | elif gvfs-open --help 2>/dev/null 1>&2; then
691 | gvfs-open "$1"
692 | else
693 | open_generic "$1"
694 | fi
695 |
696 | if [ $? -eq 0 ]; then
697 | exit_success
698 | else
699 | exit_failure_operation_failed
700 | fi
701 | }
702 |
703 | open_enlightenment()
704 | {
705 | if enlightenment_open --help 2>/dev/null 1>&2; then
706 | enlightenment_open "$1"
707 | else
708 | open_generic "$1"
709 | fi
710 |
711 | if [ $? -eq 0 ]; then
712 | exit_success
713 | else
714 | exit_failure_operation_failed
715 | fi
716 | }
717 |
718 | open_flatpak()
719 | {
720 | gdbus call --session \
721 | --dest org.freedesktop.portal.Desktop \
722 | --object-path /org/freedesktop/portal/desktop \
723 | --method org.freedesktop.portal.OpenURI.OpenURI \
724 | "" "$1" {}
725 |
726 | if [ $? -eq 0 ]; then
727 | exit_success
728 | else
729 | exit_failure_operation_failed
730 | fi
731 | }
732 |
733 | #-----------------------------------------
734 | # Recursively search .desktop file
735 |
736 | search_desktop_file()
737 | {
738 | local default="$1"
739 | local dir="$2"
740 | local target="$3"
741 |
742 | local file=""
743 | # look for both vendor-app.desktop, vendor/app.desktop
744 | if [ -r "$dir/$default" ]; then
745 | file="$dir/$default"
746 | elif [ -r "$dir/`echo $default | sed -e 's|-|/|'`" ]; then
747 | file="$dir/`echo $default | sed -e 's|-|/|'`"
748 | fi
749 |
750 | if [ -r "$file" ] ; then
751 | command="$(get_key "${file}" "Exec" | first_word)"
752 | command_exec=`which $command 2>/dev/null`
753 | icon="$(get_key "${file}" "Icon")"
754 | # FIXME: Actually LC_MESSAGES should be used as described in
755 | # http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s04.html
756 | localised_name="$(get_key "${file}" "Name")"
757 | set -- $(get_key "${file}" "Exec" | last_word)
758 | # We need to replace any occurrence of "%f", "%F" and
759 | # the like by the target file. We examine each
760 | # argument and append the modified argument to the
761 | # end then shift.
762 | local args=$#
763 | local replaced=0
764 | while [ $args -gt 0 ]; do
765 | case $1 in
766 | %[c])
767 | replaced=1
768 | arg="${localised_name}"
769 | shift
770 | set -- "$@" "$arg"
771 | ;;
772 | %[fFuU])
773 | replaced=1
774 | arg="$target"
775 | shift
776 | set -- "$@" "$arg"
777 | ;;
778 | %[i])
779 | replaced=1
780 | shift
781 | set -- "$@" "--icon" "$icon"
782 | ;;
783 | *)
784 | arg="$1"
785 | shift
786 | set -- "$@" "$arg"
787 | ;;
788 | esac
789 | args=$(( $args - 1 ))
790 | done
791 | [ $replaced -eq 1 ] || set -- "$@" "$target"
792 | "$command_exec" "$@"
793 |
794 | if [ $? -eq 0 ]; then
795 | exit_success
796 | fi
797 | fi
798 |
799 | for d in $dir/*/; do
800 | [ -d "$d" ] && search_desktop_file "$default" "$d" "$target"
801 | done
802 | }
803 |
804 |
805 | open_generic_xdg_mime()
806 | {
807 | filetype="$2"
808 | default=`xdg-mime query default "$filetype"`
809 | if [ -n "$default" ] ; then
810 | xdg_user_dir="$XDG_DATA_HOME"
811 | [ -n "$xdg_user_dir" ] || xdg_user_dir="$HOME/.local/share"
812 |
813 | xdg_system_dirs="$XDG_DATA_DIRS"
814 | [ -n "$xdg_system_dirs" ] || xdg_system_dirs=/usr/local/share/:/usr/share/
815 |
816 | DEBUG 3 "$xdg_user_dir:$xdg_system_dirs"
817 | for x in `echo "$xdg_user_dir:$xdg_system_dirs" | sed 's/:/ /g'`; do
818 | search_desktop_file "$default" "$x/applications/" "$1"
819 | done
820 | fi
821 | }
822 |
823 | open_generic_xdg_file_mime()
824 | {
825 | filetype=`xdg-mime query filetype "$1" | sed "s/;.*//"`
826 | open_generic_xdg_mime "$1" "$filetype"
827 | }
828 |
829 | open_generic_xdg_x_scheme_handler()
830 | {
831 | scheme="`echo $1 | sed -n 's/\(^[[:alnum:]+\.-]*\):.*$/\1/p'`"
832 | if [ -n $scheme ]; then
833 | filetype="x-scheme-handler/$scheme"
834 | open_generic_xdg_mime "$1" "$filetype"
835 | fi
836 | }
837 |
838 | open_envvar()
839 | {
840 | local oldifs="$IFS"
841 | local browser browser_with_arg
842 |
843 | IFS=":"
844 | for browser in $BROWSER; do
845 | IFS="$oldifs"
846 |
847 | if [ -z "$browser" ]; then
848 | continue
849 | fi
850 |
851 | if echo "$browser" | grep -q %s; then
852 | $(printf "$browser" "$1")
853 | else
854 | $browser "$1"
855 | fi
856 |
857 | if [ $? -eq 0 ]; then
858 | exit_success
859 | fi
860 | done
861 | }
862 |
863 | open_generic()
864 | {
865 | if is_file_url_or_path "$1"; then
866 | local file="$(file_url_to_path "$1")"
867 |
868 | check_input_file "$file"
869 |
870 | if has_display; then
871 | filetype=`xdg-mime query filetype "$file" | sed "s/;.*//"`
872 | open_generic_xdg_mime "$file" "$filetype"
873 | fi
874 |
875 | if which run-mailcap 2>/dev/null 1>&2; then
876 | run-mailcap --action=view "$file"
877 | if [ $? -eq 0 ]; then
878 | exit_success
879 | fi
880 | fi
881 |
882 | if has_display && mimeopen -v 2>/dev/null 1>&2; then
883 | mimeopen -L -n "$file"
884 | if [ $? -eq 0 ]; then
885 | exit_success
886 | fi
887 | fi
888 | fi
889 |
890 | if has_display; then
891 | open_generic_xdg_x_scheme_handler "$1"
892 | fi
893 |
894 | if [ -n "$BROWSER" ]; then
895 | open_envvar "$1"
896 | fi
897 |
898 | # if BROWSER variable is not set, check some well known browsers instead
899 | if [ x"$BROWSER" = x"" ]; then
900 | BROWSER=www-browser:links2:elinks:links:lynx:w3m
901 | if has_display; then
902 | BROWSER=x-www-browser:firefox:iceweasel:seamonkey:mozilla:epiphany:konqueror:chromium:chromium-browser:google-chrome:$BROWSER
903 | fi
904 | fi
905 |
906 | open_envvar "$1"
907 |
908 | exit_failure_operation_impossible "no method available for opening '$1'"
909 | }
910 |
911 | open_lxde()
912 | {
913 | # pcmanfm only knows how to handle file:// urls and filepaths, it seems.
914 | if is_file_url_or_path "$1"; then
915 | local file="$(file_url_to_path "$1")"
916 |
917 | # handle relative paths
918 | if ! echo "$file" | grep -q ^/; then
919 | file="$(pwd)/$file"
920 | fi
921 |
922 | pcmanfm "$file"
923 | else
924 | open_generic "$1"
925 | fi
926 |
927 | if [ $? -eq 0 ]; then
928 | exit_success
929 | else
930 | exit_failure_operation_failed
931 | fi
932 | }
933 |
934 | [ x"$1" != x"" ] || exit_failure_syntax
935 |
936 | url=
937 | while [ $# -gt 0 ] ; do
938 | parm="$1"
939 | shift
940 |
941 | case "$parm" in
942 | -*)
943 | exit_failure_syntax "unexpected option '$parm'"
944 | ;;
945 |
946 | *)
947 | if [ -n "$url" ] ; then
948 | exit_failure_syntax "unexpected argument '$parm'"
949 | fi
950 | url="$parm"
951 | ;;
952 | esac
953 | done
954 |
955 | if [ -z "${url}" ] ; then
956 | exit_failure_syntax "file or URL argument missing"
957 | fi
958 |
959 | detectDE
960 |
961 | if [ x"$DE" = x"" ]; then
962 | DE=generic
963 | fi
964 |
965 | DEBUG 2 "Selected DE $DE"
966 |
967 | # sanitize BROWSER (avoid caling ourselves in particular)
968 | case "${BROWSER}" in
969 | *:"xdg-open"|"xdg-open":*)
970 | BROWSER=$(echo $BROWSER | sed -e 's|:xdg-open||g' -e 's|xdg-open:||g')
971 | ;;
972 | "xdg-open")
973 | BROWSER=
974 | ;;
975 | esac
976 |
977 | case "$DE" in
978 | kde)
979 | open_kde "$url"
980 | ;;
981 |
982 | gnome3|cinnamon)
983 | open_gnome3 "$url"
984 | ;;
985 |
986 | gnome)
987 | open_gnome "$url"
988 | ;;
989 |
990 | mate)
991 | open_mate "$url"
992 | ;;
993 |
994 | xfce)
995 | open_xfce "$url"
996 | ;;
997 |
998 | lxde|lxqt)
999 | open_lxde "$url"
1000 | ;;
1001 |
1002 | enlightenment)
1003 | open_enlightenment "$url"
1004 | ;;
1005 |
1006 | cygwin)
1007 | open_cygwin "$url"
1008 | ;;
1009 |
1010 | darwin)
1011 | open_darwin "$url"
1012 | ;;
1013 |
1014 | flatpak)
1015 | open_flatpak "$url"
1016 | ;;
1017 |
1018 | generic)
1019 | open_generic "$url"
1020 | ;;
1021 |
1022 | *)
1023 | exit_failure_operation_impossible "no method available for opening '$url'"
1024 | ;;
1025 | esac
1026 |
--------------------------------------------------------------------------------