├── 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 | [![R-CMD-check](https://github.com/r-lib/xopen/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-lib/xopen/actions/workflows/R-CMD-check.yaml) 8 | [![](https://www.r-pkg.org/badges/version/xopen)](https://www.r-pkg.org/pkg/xopen) 9 | [![CRAN RStudio mirror downloads](https://cranlogs.r-pkg.org/badges/xopen)](https://www.r-pkg.org/pkg/xopen) 10 | [![Codecov test coverage](https://codecov.io/gh/r-lib/xopen/graph/badge.svg)](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 | --------------------------------------------------------------------------------