├── tests ├── assets │ ├── a.pdf │ ├── a.png │ ├── b.png │ ├── empty │ ├── empty.txt │ ├── cat │ ├── p.html │ ├── no_html_tags.html │ ├── mimeapps_no_added.list │ ├── mimeapps_duplicate.list │ ├── mimeapps_empty_entry.list │ ├── mimeapps_no_default.list │ ├── nonsense_binary_data │ ├── rust.vim │ ├── empty_exec.desktop │ ├── org.wezfurlong.wezterm.desktop │ ├── empty_name.desktop │ ├── cmus.desktop │ ├── mimeapps_sorted.list │ ├── mimeapps_anomalous_semicolons.list │ ├── vlc.desktop │ ├── Helix.desktop │ └── SettingsWidgetFdoSecrets.ui ├── snapshots │ ├── terminal_override__terminal_output_tests_force_false.snap │ └── terminal_override__terminal_output_tests_force_true.snap └── terminal_override.rs ├── .envrc ├── .gitattributes ├── .gitignore ├── .rustfmt.toml ├── src ├── apps │ ├── mod.rs │ ├── snapshots │ │ ├── handlr__apps__user__tests__remove_handlers_expand_wildcards.snap │ │ ├── handlr__apps__user__tests__unset_handlers_expand_wildcards.snap │ │ ├── handlr__apps__user__tests__set_handlers_expand_wildcards.snap │ │ └── handlr__apps__user__tests__add_handlers_expand_wildcards.snap │ └── system.rs ├── config │ ├── mod.rs │ ├── snapshots │ │ ├── handlr__config__main_config__tests__show_handler.snap │ │ ├── handlr__config__main_config__tests__show_handler_terminal.snap │ │ ├── handlr__config__main_config__tests__show_handler_json_terminal.snap │ │ ├── handlr__config__main_config__tests__show_handler_json.snap │ │ ├── handlr__config__main_config__tests__print_handlers_json-2.snap │ │ ├── handlr__config__main_config__tests__print_handlers_json.snap │ │ ├── handlr__config__main_config__tests__dont_override_selector.snap │ │ ├── handlr__config__main_config__tests__override_selector.snap │ │ ├── handlr__config__main_config__tests__print_handlers_detailed_json-2.snap │ │ ├── handlr__config__main_config__tests__print_handlers_detailed_json.snap │ │ ├── handlr__config__main_config__tests__print_handlers_piped.snap │ │ ├── handlr__config__main_config__tests__terminal_command_fallback.snap │ │ ├── handlr__config__main_config__tests__language_variable_parsing.snap │ │ ├── handlr__config__main_config__tests__print_handlers_detailed_piped.snap │ │ ├── handlr__config__main_config__tests__terminal_command_set.snap │ │ ├── handlr__config__main_config__tests__print_handlers_default.snap │ │ ├── handlr__config__main_config__tests__show_handler-2.snap │ │ ├── handlr__config__main_config__tests__show_handler_terminal-2.snap │ │ ├── handlr__config__main_config__tests__show_handler_json_terminal-2.snap │ │ ├── handlr__config__main_config__tests__print_handlers_detailed.snap │ │ ├── handlr__config__main_config__tests__show_handler_json-2.snap │ │ ├── handlr__config__main_config__tests__wildcard_mimes.snap │ │ ├── handlr__config__main_config__tests__set_and_unset_handlers.snap │ │ ├── handlr__config__main_config__tests__add_and_unset_handlers.snap │ │ ├── handlr__config__main_config__tests__set_and_remove_handlers.snap │ │ ├── handlr__config__main_config__tests__add_and_remove_handlers.snap │ │ ├── handlr__config__main_config__tests__print_handlers_piped-2.snap │ │ ├── handlr__config__main_config__tests__print_handlers_default-2.snap │ │ ├── handlr__config__main_config__tests__print_handlers_detailed-2.snap │ │ ├── handlr__config__main_config__tests__print_handlers_detailed_piped-2.snap │ │ ├── handlr__config__main_config__tests__properly_assign_files_to_handlers.snap │ │ ├── handlr__config__main_config__tests__print_handlers_json-3.snap │ │ └── handlr__config__main_config__tests__print_handlers_detailed_json-3.snap │ └── config_file.rs ├── common │ ├── mod.rs │ ├── snapshots │ │ ├── handlr__common__handler__tests__regex_handlers.snap │ │ ├── handlr__common__path__tests__mime_table_json-2.snap │ │ ├── handlr__common__path__tests__mime_table_json.snap │ │ ├── handlr__common__path__tests__mime_table_piped.snap │ │ ├── handlr__common__table__tests__piped_output.snap │ │ ├── handlr__common__path__tests__mime_table_terminal.snap │ │ └── handlr__common__table__tests__terminal_output.snap │ ├── db.rs │ ├── table.rs │ ├── path.rs │ ├── mime_types.rs │ ├── handler.rs │ └── desktop_entry.rs ├── testing.rs ├── main.rs ├── error.rs └── logging.rs ├── shell.nix ├── default.nix ├── .github └── workflows │ ├── build_nix.yml │ └── main.yml ├── LICENSE ├── flake.nix ├── Cargo.toml ├── flake.lock └── README.md /tests/assets/a.pdf: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/a.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/b.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /tests/assets/empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/cat: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | bat "$@" 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | completions/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /mutants.out* 3 | /result 4 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 80 2 | merge_imports = true 3 | -------------------------------------------------------------------------------- /tests/assets/p.html: -------------------------------------------------------------------------------- 1 | 2 |

asdf

3 | 4 | -------------------------------------------------------------------------------- /tests/assets/no_html_tags.html: -------------------------------------------------------------------------------- 1 |

Hello World!

2 | -------------------------------------------------------------------------------- /tests/assets/mimeapps_no_added.list: -------------------------------------------------------------------------------- 1 | [Default Applications] 2 | text/*=nvim.desktop;Helix.desktop; 3 | -------------------------------------------------------------------------------- /tests/assets/mimeapps_duplicate.list: -------------------------------------------------------------------------------- 1 | [Default Applications] 2 | text/*=nvim.desktop;Helix.desktop;nvim.desktop; 3 | -------------------------------------------------------------------------------- /tests/assets/mimeapps_empty_entry.list: -------------------------------------------------------------------------------- 1 | [Default Applications] 2 | text/*=nvim.desktop;Helix.desktop; 3 | text/plain= 4 | -------------------------------------------------------------------------------- /src/apps/mod.rs: -------------------------------------------------------------------------------- 1 | mod system; 2 | mod user; 3 | 4 | pub use system::SystemApps; 5 | pub use user::{DesktopList, MimeApps}; 6 | -------------------------------------------------------------------------------- /tests/assets/mimeapps_no_default.list: -------------------------------------------------------------------------------- 1 | [Added Associations] 2 | x-scheme-handler/terminal=org.codeberg.dnkl.foot.desktop; 3 | -------------------------------------------------------------------------------- /tests/assets/nonsense_binary_data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Anomalocaridid/handlr-regex/HEAD/tests/assets/nonsense_binary_data -------------------------------------------------------------------------------- /tests/assets/rust.vim: -------------------------------------------------------------------------------- 1 | highlight CocRustChainingHint ctermfg=0 2 | hi link rustDerive SpecialComment 3 | hi link rustDeriveTrait SpecialComment 4 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod config_file; 2 | mod main_config; 3 | 4 | pub use config_file::ConfigFile; 5 | pub use main_config::{get_languages, Config, Languages}; 6 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | tests/assets/Helix.desktop 6 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler_terminal.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | tests/assets/Helix.desktop 6 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler_json_terminal.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | {"cmd":"hx","handler":"tests/assets/Helix.desktop","name":"Helix"} 6 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; } 5 | ) { 6 | src = ./.; 7 | }).shellNix 8 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler_json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | {"cmd":"wezterm start --cwd . -e hx","handler":"tests/assets/Helix.desktop","name":"Helix"} 6 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import ( 2 | fetchTarball { 3 | url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; 4 | sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; } 5 | ) { 6 | src = ./.; 7 | }).defaultNix 8 | -------------------------------------------------------------------------------- /tests/assets/empty_exec.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Display=true 6 | Exec= 7 | Terminal=false 8 | Name=cmus-remote 9 | Comment=Music player cmus-remote control 10 | NoDisplay=true 11 | Icon=cmus 12 | MimeType=audio/mp3;audio/ogg; 13 | -------------------------------------------------------------------------------- /.github/workflows/build_nix.yml: -------------------------------------------------------------------------------- 1 | name: "Build legacy Nix package on Ubuntu" 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: cachix/install-nix-action@v26 12 | - name: Building package 13 | run: nix build 14 | -------------------------------------------------------------------------------- /tests/assets/org.wezfurlong.wezterm.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=WezTerm 3 | Comment=Wez's Terminal Emulator 4 | Keywords=shell;prompt;command;commandline;cmd; 5 | Icon=org.wezfurlong.wezterm 6 | StartupWMClass=org.wezfurlong.wezterm 7 | TryExec=wezterm 8 | Exec=wezterm start --cwd . 9 | Type=Application 10 | Categories=System;TerminalEmulator;Utility; 11 | Terminal=false 12 | -------------------------------------------------------------------------------- /tests/assets/empty_name.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Display=true 6 | Exec=bash -c "(! pgrep cmus && tilix -e cmus && tilix -a session-add-down -e cava); sleep 0.1 && cmus-remote -q %f" 7 | Terminal=false 8 | Name= 9 | Comment=Music player cmus-remote control 10 | NoDisplay=true 11 | Icon=cmus 12 | MimeType=audio/mp3;audio/ogg; 13 | -------------------------------------------------------------------------------- /tests/assets/cmus.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Version=1.0 4 | Type=Application 5 | Display=true 6 | Exec=bash -c "(! pgrep cmus && tilix -e cmus && tilix -a session-add-down -e cava); sleep 0.1 && cmus-remote -q %f" 7 | Terminal=false 8 | Name=cmus-remote 9 | Comment=Music player cmus-remote control 10 | NoDisplay=true 11 | Icon=cmus 12 | MimeType=audio/mp3;audio/ogg; 13 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | mod db; 2 | mod desktop_entry; 3 | mod handler; 4 | mod mime_types; 5 | mod path; 6 | mod table; 7 | 8 | pub use self::db::MIME_TYPES; 9 | pub use desktop_entry::{DesktopEntry, Mode as ExecMode}; 10 | pub use handler::{ 11 | DesktopHandler, Handleable, Handler, RegexApps, RegexHandler, 12 | }; 13 | pub use mime_types::MimeType; 14 | pub use path::{mime_table, UserPath}; 15 | pub use table::render_table; 16 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_json-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | [{"mime":"application/vnd.oasis.opendocument.*","handlers":["startcenter.desktop"]},{"mime":"application/vnd.openxmlformats-officedocument.*","handlers":["startcenter.desktop"]},{"mime":"text/plain","handlers":["helix.desktop","nvim.desktop","kakoune.desktop"]},{"mime":"video/asdf","handlers":["mpv.desktop"]},{"mime":"video/mp4","handlers":["mpv.desktop"]},{"mime":"video/webm","handlers":["brave.desktop"]}] 6 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer).unwrap()" 4 | --- 5 | [{"mime":"application/vnd.oasis.opendocument.*","handlers":["startcenter.desktop"]},{"mime":"application/vnd.openxmlformats-officedocument.*","handlers":["startcenter.desktop"]},{"mime":"text/plain","handlers":["helix.desktop","nvim.desktop","kakoune.desktop"]},{"mime":"video/asdf","handlers":["mpv.desktop"]},{"mime":"video/mp4","handlers":["mpv.desktop"]},{"mime":"video/webm","handlers":["brave.desktop"]}] 6 | -------------------------------------------------------------------------------- /src/common/snapshots/handlr__common__handler__tests__regex_handlers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/common/handler.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP] DEBUG handlr::common::handler: Regex matches found in `"freetube %u" (Regex Handler)` for `https://youtu.be/dQw4w9WgXcQ`: [Regex(Regex("(https://)?(www\\.)?youtu(be\\.com|\\.be)/*"))] 6 | [TIMESTAMP] DEBUG handlr::common::handler: No regex matches found in `"freetube %u" (Regex Handler)` for `https://en.wikipedia.org/` 7 | -------------------------------------------------------------------------------- /src/common/db.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | 3 | use itertools::Itertools; 4 | 5 | /// Comprehensive list of mimetypes 6 | pub static MIME_TYPES: LazyLock> = LazyLock::new(|| { 7 | // Mimes that are not in mime_db::TYPES 8 | [ 9 | "inode/directory", 10 | "x-scheme-handler/http", 11 | "x-scheme-handler/https", 12 | "x-scheme-handler/terminal", 13 | ] 14 | .iter() 15 | .map(|s| s.to_string()) 16 | .chain( 17 | mime_db::TYPES 18 | .into_iter() 19 | .map(|(mime, _, _)| mime.to_string()), 20 | ) 21 | .collect_vec() 22 | }); 23 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__dont_override_selector.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP] DEBUG handlr::config::config_file: Selector enabled: false 6 | [TIMESTAMP] DEBUG handlr::config::config_file: Selector enabled: false 7 | [TIMESTAMP] DEBUG handlr::config::config_file: Selector enabled: true 8 | [TIMESTAMP] DEBUG handlr::config::config_file: Selector enabled: true 9 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__override_selector.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP] DEBUG handlr::config::config_file: Overriding selector command: fzf 6 | [TIMESTAMP] DEBUG handlr::config::config_file: Selector enabled: true 7 | [TIMESTAMP] DEBUG handlr::config::config_file: Overriding selector command: fuzzel --dmenu --prompt='Open With: ' 8 | [TIMESTAMP] DEBUG handlr::config::config_file: Selector enabled: false 9 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_detailed_json-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | {"added_associations":[{"mime":"x-scheme-handler/terminal","handlers":["org.wezfurlong.wezterm.desktop"]}],"default_apps":[{"mime":"application/vnd.oasis.opendocument.*","handlers":["startcenter.desktop"]},{"mime":"application/vnd.openxmlformats-officedocument.*","handlers":["startcenter.desktop"]},{"mime":"text/plain","handlers":["helix.desktop","nvim.desktop","kakoune.desktop"]},{"mime":"video/asdf","handlers":["mpv.desktop"]},{"mime":"video/mp4","handlers":["mpv.desktop"]},{"mime":"video/webm","handlers":["brave.desktop"]}],"system_apps":[]} 6 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_detailed_json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | {"added_associations":[{"mime":"x-scheme-handler/terminal","handlers":["org.wezfurlong.wezterm.desktop"]}],"default_apps":[{"mime":"application/vnd.oasis.opendocument.*","handlers":["startcenter.desktop"]},{"mime":"application/vnd.openxmlformats-officedocument.*","handlers":["startcenter.desktop"]},{"mime":"text/plain","handlers":["helix.desktop","nvim.desktop","kakoune.desktop"]},{"mime":"video/asdf","handlers":["mpv.desktop"]},{"mime":"video/mp4","handlers":["mpv.desktop"]},{"mime":"video/webm","handlers":["brave.desktop"]}],"system_apps":[]} 6 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_piped.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | mime handlers 6 | application/vnd.oasis.opendocument.* startcenter.desktop 7 | application/vnd.openxmlformats-officedocument.* startcenter.desktop 8 | text/plain helix.desktop, nvim.desktop, kakoune.desktop 9 | video/asdf mpv.desktop 10 | video/mp4 mpv.desktop 11 | video/webm brave.desktop 12 | -------------------------------------------------------------------------------- /src/common/snapshots/handlr__common__path__tests__mime_table_json-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/common/path.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | [{"path":"tests/assets","mime":"inode/directory"},{"path":"tests/assets/cat","mime":"application/x-shellscript"},{"path":"tests/assets/cmus.desktop","mime":"application/x-desktop"},{"path":"tests/assets/empty.txt","mime":"text/plain"},{"path":"tests/assets/no_html_tags.html","mime":"text/html"},{"path":"tests/assets/org.wezfurlong.wezterm.desktop","mime":"application/x-desktop"},{"path":"tests/assets/p.html","mime":"text/html"},{"path":"tests/assets/rust.vim","mime":"text/plain"},{"path":"tests/assets/SettingsWidgetFdoSecrets.ui","mime":"application/x-designer"},{"path":"https://duckduckgo.com/","mime":"x-scheme-handler/https"},{"path":".","mime":"inode/directory"},{"path":"README.md","mime":"text/markdown"}] 6 | -------------------------------------------------------------------------------- /src/common/snapshots/handlr__common__path__tests__mime_table_json.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/common/path.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | [{"path":"tests/assets","mime":"inode/directory"},{"path":"tests/assets/cat","mime":"application/x-shellscript"},{"path":"tests/assets/cmus.desktop","mime":"application/x-desktop"},{"path":"tests/assets/empty.txt","mime":"text/plain"},{"path":"tests/assets/no_html_tags.html","mime":"text/html"},{"path":"tests/assets/org.wezfurlong.wezterm.desktop","mime":"application/x-desktop"},{"path":"tests/assets/p.html","mime":"text/html"},{"path":"tests/assets/rust.vim","mime":"text/plain"},{"path":"tests/assets/SettingsWidgetFdoSecrets.ui","mime":"application/x-designer"},{"path":"https://duckduckgo.com/","mime":"x-scheme-handler/https"},{"path":".","mime":"inode/directory"},{"path":"README.md","mime":"text/markdown"}] 6 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | name: Publish for ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | include: 15 | - os: ubuntu-latest 16 | asset_name: handlr 17 | 18 | steps: 19 | - uses: hecrj/setup-rust-action@v2 20 | with: 21 | rust-version: stable 22 | 23 | - uses: actions/checkout@v4 24 | 25 | - name: Test 26 | run: cargo test 27 | 28 | - name: Build 29 | run: cargo build --release --locked 30 | 31 | - name: Strip binary 32 | run: strip target/release/handlr 33 | 34 | - name: Upload binaries to release 35 | uses: svenstaro/upload-release-action@v2 36 | with: 37 | repo_token: ${{ secrets.GITHUB_TOKEN }} 38 | file: target/release/handlr 39 | asset_name: ${{ matrix.asset_name }} 40 | tag: ${{ github.ref }} 41 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__terminal_command_fallback.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::apps::user: No handlers configured for `x-scheme-handler/terminal` in mimeapps.list Default associations 6 | [TIMESTAMP]  INFO handlr::config::main_config: No match for `x-scheme-handler/terminal` in mimeapps.list Default Associations 7 | [TIMESTAMP]  INFO handlr::config::main_config: No matching entries for `x-scheme-handler/terminal` in mimeapps.list Added Associations 8 | [TIMESTAMP] DEBUG handlr::apps::system: No installed handlers found for `x-scheme-handler/terminal` 9 | [TIMESTAMP]  INFO handlr::config::main_config: No matching installed handlers found for `x-scheme-handler/terminal` 10 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__language_variable_parsing.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 6 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 7 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG set: 'es' 8 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 9 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 10 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANGUAGE set: 'ja:fr:nl' 11 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG set: 'bn' 12 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANGUAGE set: 'hu:pa:it:ru' 13 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_detailed_piped.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | Default Apps 6 | mime handlers 7 | application/vnd.oasis.opendocument.* startcenter.desktop 8 | application/vnd.openxmlformats-officedocument.* startcenter.desktop 9 | text/plain helix.desktop, nvim.desktop, kakoune.desktop 10 | video/asdf mpv.desktop 11 | video/mp4 mpv.desktop 12 | video/webm brave.desktop 13 | Added associations 14 | mime handlers 15 | x-scheme-handler/terminal org.wezfurlong.wezterm.desktop 16 | System Apps 17 | mime handlers 18 | -------------------------------------------------------------------------------- /tests/snapshots/terminal_override__terminal_output_tests_force_false.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/terminal_override.rs 3 | info: 4 | program: handlr 5 | args: 6 | - "--force-terminal-output=false" 7 | - "-vvv" 8 | - "--disable-notifications" 9 | - mime 10 | - "./tests/assets" 11 | --- 12 | success: true 13 | exit_code: 0 14 | ----- stdout ----- 15 | path mime 16 | ./tests/assets inode/directory 17 | 18 | ----- stderr ----- 19 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 20 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 21 | [TIMESTAMP] DEBUG handlr: Interactive terminal detected: false 22 | [TIMESTAMP]  INFO handlr::common::path: Printing mime information for paths: ["./tests/assets"] 23 | [TIMESTAMP] DEBUG handlr::common::path: JSON output: false 24 | [TIMESTAMP]  INFO handlr::common::path: Finished printing mime information 25 | -------------------------------------------------------------------------------- /src/common/snapshots/handlr__common__path__tests__mime_table_piped.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/common/path.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | path mime 6 | tests/assets inode/directory 7 | tests/assets/cat application/x-shellscript 8 | tests/assets/cmus.desktop application/x-desktop 9 | tests/assets/empty.txt text/plain 10 | tests/assets/no_html_tags.html text/html 11 | tests/assets/org.wezfurlong.wezterm.desktop application/x-desktop 12 | tests/assets/p.html text/html 13 | tests/assets/rust.vim text/plain 14 | tests/assets/SettingsWidgetFdoSecrets.ui application/x-designer 15 | https://duckduckgo.com/ x-scheme-handler/https 16 | . inode/directory 17 | README.md text/markdown 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gregory Petrosyan 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 | -------------------------------------------------------------------------------- /tests/terminal_override.rs: -------------------------------------------------------------------------------- 1 | #[path = "../src/testing.rs"] 2 | mod testing; 3 | 4 | use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; 5 | use std::process::Command; 6 | 7 | /// Helper function to test terminal output detection 8 | fn test_terminal_output(terminal_output: bool) -> Command { 9 | let mut cmd = Command::new(get_cargo_bin("handlr")); 10 | cmd.arg(format!("--force-terminal-output={}", terminal_output)) 11 | .arg("-vvv") // Maximum verbosity 12 | .arg("--disable-notifications") // Not much point showing these in tests 13 | .arg("mime") 14 | .arg("./tests/assets") 15 | .env_clear(); // Ensure no environment variables compromise the tests 16 | cmd 17 | } 18 | 19 | #[test] 20 | fn terminal_output_tests_force_true() { 21 | insta::with_settings!( 22 | { 23 | filters => testing::timestamp_filter() 24 | }, 25 | { assert_cmd_snapshot!(test_terminal_output(true)) } 26 | ) 27 | } 28 | 29 | #[test] 30 | fn terminal_output_tests_force_false() { 31 | insta::with_settings!( 32 | { 33 | filters => testing::timestamp_filter() 34 | }, 35 | { assert_cmd_snapshot!(test_terminal_output(false)) } 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/common/snapshots/handlr__common__table__tests__piped_output.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/common/table.rs 3 | expression: "render_table(&rows(LOREM_IPSUM), false)" 4 | --- 5 | col1 col2 6 | Lorem ipsum 7 | dolor sit 8 | amet, consectetur 9 | adipiscing elit, 10 | sed do 11 | eiusmod tempor 12 | incididunt ut 13 | labore et 14 | dolore magna 15 | aliqua. Ut 16 | enim ad 17 | minim veniam, 18 | quis nostrud 19 | exercitation ullamco 20 | laboris nisi 21 | ut aliquip 22 | ex ea 23 | commodo consequat. 24 | Duis aute 25 | irure dolor 26 | in reprehenderit 27 | in voluptate 28 | velit esse 29 | cillum dolore 30 | eu fugiat 31 | nulla pariatur. 32 | Excepteur sint 33 | occaecat cupidatat 34 | non proident, 35 | sunt in 36 | culpa qui 37 | officia deserunt 38 | mollit anim 39 | id est 40 | -------------------------------------------------------------------------------- /tests/snapshots/terminal_override__terminal_output_tests_force_true.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/terminal_override.rs 3 | info: 4 | program: handlr 5 | args: 6 | - "--force-terminal-output=true" 7 | - "-vvv" 8 | - "--disable-notifications" 9 | - mime 10 | - "./tests/assets" 11 | --- 12 | success: true 13 | exit_code: 0 14 | ----- stdout ----- 15 | ┌────────────────┬─────────────────┐ 16 | │ path  │ mime  │ 17 | ├────────────────┼─────────────────┤ 18 | │ ./tests/assets │ inode/directory │ 19 | └────────────────┴─────────────────┘ 20 | 21 | ----- stderr ----- 22 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 23 | [TIMESTAMP] DEBUG handlr::config::main_config: $LANG not set 24 | [TIMESTAMP] DEBUG handlr: Interactive terminal detected: true 25 | [TIMESTAMP]  INFO handlr::common::path: Printing mime information for paths: ["./tests/assets"] 26 | [TIMESTAMP] DEBUG handlr::common::path: JSON output: false 27 | [TIMESTAMP]  INFO handlr::common::path: Finished printing mime information 28 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__terminal_command_set.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/org.wezfurlong.wezterm.desktop to list of handlers for `x-scheme-handler/terminal` 6 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 7 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `x-scheme-handler/terminal`: tests/assets/org.wezfurlong.wezterm.desktop; 8 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 9 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `x-scheme-handler/terminal` in mimeapps.list Default Associations: tests/assets/org.wezfurlong.wezterm.desktop; 10 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 11 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 12 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `x-scheme-handler/terminal` in mimeapps.list Default Associations 13 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_default.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | ┌─────────────────────────────────────────────────┬─────────────────────┐ 6 | │ mime  │ handlers  │ 7 | ├─────────────────────────────────────────────────┼─────────────────────┤ 8 | │ application/vnd.oasis.opendocument.*  │ startcenter.desktop │ 9 | │ application/vnd.openxmlformats-officedocument.* │ startcenter.desktop │ 10 | │ text/plain  │ helix.desktop,   │ 11 | │ │ nvim.desktop,   │ 12 | │ │ kakoune.desktop  │ 13 | │ video/asdf  │ mpv.desktop  │ 14 | │ video/mp4  │ mpv.desktop  │ 15 | │ video/webm  │ brave.desktop  │ 16 | └─────────────────────────────────────────────────┴─────────────────────┘ 17 | -------------------------------------------------------------------------------- /tests/assets/mimeapps_sorted.list: -------------------------------------------------------------------------------- 1 | [Added Associations] 2 | video/vnd.youtube.yt=freetube.desktop; 3 | x-scheme-handler/terminal=org.wezfurlong.wezterm.desktop;org.codeberg.dnkl.foot.desktop; 4 | [Default Applications] 5 | application/msword=writer.desktop; 6 | application/pdf=org.pwmt.zathura-pdf-mupdf.desktop; 7 | application/vnd.ms-excel=calc.desktop; 8 | application/vnd.ms-powerpoint=impress.desktop; 9 | application/vnd.oasis.opendocument.database=base.desktop; 10 | application/vnd.oasis.opendocument.formula=math.desktop; 11 | application/vnd.oasis.opendocument.graphics=draw.desktop; 12 | application/vnd.oasis.opendocument.presentation=impress.desktop; 13 | application/vnd.oasis.opendocument.presentation-template=impress.desktop; 14 | application/vnd.oasis.opendocument.spreadsheet=calc.desktop; 15 | application/vnd.oasis.opendocument.spreadsheet-flat-xml=calc.desktop; 16 | application/vnd.oasis.opendocument.text=writer.desktop; 17 | application/vnd.oasis.opendocument.text-template=writer.desktop; 18 | application/vnd.openxmlformats-officedocument.presentationml.presentation=impress.desktop; 19 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=calc.desktop; 20 | application/vnd.openxmlformats-officedocument.wordprocessingml.document=writer.desktop; 21 | application/vnd.openxmlformats-officedocument.wordprocessingml.template=writer.desktop; 22 | application/x-yaml=Helix.desktop; 23 | audio/*=org.strawberrymusicplayer.strawberry.desktop; 24 | image/*=imv.desktop; 25 | inode/directory=yazi.desktop; 26 | text/*=Helix.desktop;nvim.desktop; 27 | text/html=firefox.desktop;Helix.desktop;nvim.desktop; 28 | video/*=mpv.desktop; 29 | x-scheme-handler/http=firefox.desktop; 30 | x-scheme-handler/https=firefox.desktop; 31 | x-scheme-handler/terminal=org.wezfurlong.wezterm.desktop; 32 | -------------------------------------------------------------------------------- /tests/assets/mimeapps_anomalous_semicolons.list: -------------------------------------------------------------------------------- 1 | [Added Associations] 2 | video/vnd.youtube.yt=freetube.desktop 3 | x-scheme-handler/terminal=org.wezfurlong.wezterm.desktop;;org.codeberg.dnkl.foot.desktop 4 | [Default Applications] 5 | application/msword=writer.desktop 6 | application/pdf=org.pwmt.zathura-pdf-mupdf.desktop; 7 | application/vnd.ms-excel=calc.desktop 8 | application/vnd.ms-powerpoint=impress.desktop 9 | application/vnd.oasis.opendocument.database=base.desktop; 10 | application/vnd.oasis.opendocument.formula=math.desktop 11 | application/vnd.oasis.opendocument.graphics=draw.desktop; 12 | application/vnd.oasis.opendocument.presentation=impress.desktop 13 | application/vnd.oasis.opendocument.presentation-template=impress.desktop; 14 | application/vnd.oasis.opendocument.spreadsheet=calc.desktop 15 | application/vnd.oasis.opendocument.spreadsheet-flat-xml=calc.desktop; 16 | application/vnd.oasis.opendocument.text=writer.desktop 17 | application/vnd.oasis.opendocument.text-template=writer.desktop; 18 | application/vnd.openxmlformats-officedocument.presentationml.presentation=impress.desktop 19 | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=calc.desktop; 20 | application/vnd.openxmlformats-officedocument.wordprocessingml.document=writer.desktop 21 | application/vnd.openxmlformats-officedocument.wordprocessingml.template=writer.desktop; 22 | application/x-yaml=Helix.desktop; 23 | audio/*=org.strawberrymusicplayer.strawberry.desktop; 24 | image/*=imv.desktop; 25 | inode/directory=yazi.desktop; 26 | text/*=Helix.desktop;;nvim.desktop; 27 | text/html=firefox.desktop;Helix.desktop;nvim.desktop; 28 | video/*=mpv.desktop; 29 | x-scheme-handler/http=firefox.desktop; 30 | x-scheme-handler/https=firefox.desktop 31 | x-scheme-handler/terminal=org.wezfurlong.wezterm.desktop; 32 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | naersk.url = "github:nix-community/naersk/master"; 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = 9 | { 10 | self, 11 | nixpkgs, 12 | utils, 13 | naersk, 14 | }: 15 | utils.lib.eachDefaultSystem ( 16 | system: 17 | let 18 | pkgs = import nixpkgs { inherit system; }; 19 | naersk-lib = pkgs.callPackage naersk { }; 20 | in 21 | { 22 | packages.default = naersk-lib.buildPackage { 23 | src = ./.; 24 | nativeBuildInputs = with pkgs; [ 25 | installShellFiles 26 | shared-mime-info 27 | ]; 28 | buildInputs = with pkgs; [ libiconv ]; 29 | 30 | precheck = '' 31 | export HOME=$TEMPDIR 32 | ''; 33 | 34 | postInstall = '' 35 | installShellCompletion --cmd handlr \ 36 | --zsh <(COMPLETE=zsh $out/bin/handlr) \ 37 | --bash <(COMPLETE=bash $out/bin/handlr) \ 38 | --fish <(COMPLETE=fish $out/bin/handlr) \ 39 | 40 | installManPage target/release/build/handlr-regex-*/out/manual/man1/* 41 | ''; 42 | }; 43 | devShell = 44 | with pkgs; 45 | mkShell { 46 | buildInputs = [ 47 | cargo 48 | cargo-insta # Adds `cargo insta` command for reviewing snapshots 49 | cargo-mutants # Adds `cargo mutants` command for mutation testing 50 | clippy # Adds more checks to cargo 51 | pre-commit 52 | rust-analyzer # Rust language server 53 | rustc 54 | rustfmt 55 | ]; 56 | RUST_SRC_PATH = rustPlatform.rustLibSrc; 57 | }; 58 | } 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "handlr-regex" 3 | version = "0.13.0" 4 | authors = ["Duncan Russell ", "Gregory "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Fork of handlr with regex support" 8 | repository = "https://github.com/Anomalocaridid/handlr-regex" 9 | 10 | [dependencies] 11 | clap = { version = "4.5.2", features = ["derive"] } 12 | url = "2.2.1" 13 | itertools = "0.10.0" 14 | thiserror = "1.0.24" 15 | xdg = "2.2.0" 16 | mime = "0.3.16" 17 | mime-db = "1.3.0" 18 | confy = "0.4.0" 19 | serde = { version = "1.0.125", features = ["derive"] } 20 | xdg-mime = "0.4.0" 21 | tabled = "0.15.0" 22 | serde_json = "1.0" 23 | enum_dispatch = "0.3.13" 24 | derive_more = { version = "0.99.18", default-features = false, features = ["deref", "deref_mut", "display"] } 25 | serde_ini = "0.2.0" 26 | serde_with = "3.8.3" 27 | wildmatch = "2.3.4" 28 | mutants = "0.0.3" 29 | clap_complete = { version = "4.5.33", features = ["unstable-dynamic"] } 30 | tracing = "0.1.41" 31 | tracing-subscriber = "0.3.19" 32 | tracing-appender = "0.2.3" 33 | tracing-unwrap = "1.0.1" 34 | clap-verbosity-flag = { version = "3.0.3", default-features = false, features = ["tracing"] } 35 | notify-rust = "4.11.7" 36 | execute = "0.2.13" 37 | lazy-regex = { version = "3.4.1", default-features = false, features = ["lite"] } 38 | freedesktop_entry_parser = "1.3.0" 39 | 40 | [[bin]] 41 | name = "handlr" 42 | path = "src/main.rs" 43 | 44 | [dev-dependencies] 45 | insta = { version = "1.42.2", features = ["filters"] } 46 | insta-cmd = "0.6.0" 47 | pipe = "0.4.0" 48 | similar-asserts = "1.7.0" 49 | temp-env = "0.3.6" 50 | 51 | [build-dependencies] 52 | clap = { version = "4.5.2", features = ["derive"] } 53 | clap_complete = { version = "4.5.33", features = ["unstable-dynamic"] } 54 | clap_mangen = "0.2.20" 55 | clap-verbosity-flag = { version = "3.0.3", default-features = false, features = ["tracing"] } 56 | 57 | [profile.release] 58 | opt-level = "z" 59 | lto = true 60 | strip = true 61 | codegen-units = 1 62 | panic = "abort" 63 | 64 | [profile.dev.package] 65 | insta.opt-level = 3 66 | -------------------------------------------------------------------------------- /src/testing.rs: -------------------------------------------------------------------------------- 1 | //! Testing helpers 2 | #![cfg(test)] 3 | 4 | /// Helper function for insta settings 5 | pub fn timestamp_filter() -> Vec<(&'static str, &'static str)> { 6 | vec![(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d*\.\d*Z", "[TIMESTAMP]")] 7 | } 8 | 9 | /// Helper macro to snapshot test logs 10 | #[macro_export] 11 | macro_rules! logs_snapshot_test { 12 | ($name:ident, $block:block) => { 13 | #[test] 14 | fn $name() -> $crate::error::Result<()> { 15 | use std::io::Read; 16 | 17 | use tracing::{subscriber, Level}; 18 | use tracing_subscriber::{ 19 | filter, fmt, layer::SubscriberExt, registry, 20 | }; 21 | 22 | let (mut pipe_read, pipe_write) = pipe::pipe(); 23 | 24 | // Create scope so guards are dropped before assertion and logs are flushed 25 | // Otherwise, the tests using this macro will freeze 26 | { 27 | let (writer, _writer_guard) = 28 | tracing_appender::non_blocking(pipe_write); 29 | let _default_guard = subscriber::set_default( 30 | registry() 31 | .with(fmt::Layer::new().with_writer(writer)) 32 | // Filter out all logs from other crates so the user is not overwhelmed looking at the logs 33 | .with( 34 | filter::Targets::new() 35 | .with_target("handlr", Level::TRACE) 36 | .with_target("tracing_unwrap", Level::WARN), 37 | ), 38 | ); 39 | $block 40 | } 41 | 42 | let mut buffer = Vec::::new(); 43 | pipe_read.read_to_end(&mut buffer)?; 44 | 45 | insta::with_settings!( 46 | { 47 | filters => $crate::testing::timestamp_filter() 48 | }, 49 | { insta::assert_snapshot!(String::from_utf8(buffer).expect("Buffer is invalid utf8")) } 50 | ); 51 | 52 | Ok(()) 53 | } 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/Helix.desktop to list of handlers for `text/plain` 6 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 7 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: tests/assets/Helix.desktop; 8 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 9 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/org.wezfurlong.wezterm.desktop to list of handlers for `x-scheme-handler/terminal` 10 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 11 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `x-scheme-handler/terminal`: tests/assets/org.wezfurlong.wezterm.desktop; 12 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Showing handler for `text/plain` 14 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: false 15 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: tests/assets/Helix.desktop; 16 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 17 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 18 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished showing handler 20 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler_terminal-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/Helix.desktop to list of handlers for `text/plain` 6 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 7 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: tests/assets/Helix.desktop; 8 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 9 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/org.wezfurlong.wezterm.desktop to list of handlers for `x-scheme-handler/terminal` 10 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 11 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `x-scheme-handler/terminal`: tests/assets/org.wezfurlong.wezterm.desktop; 12 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Showing handler for `text/plain` 14 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: false 15 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: tests/assets/Helix.desktop; 16 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 17 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 18 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished showing handler 20 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler_json_terminal-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/Helix.desktop to list of handlers for `text/plain` 6 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 7 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: tests/assets/Helix.desktop; 8 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 9 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/org.wezfurlong.wezterm.desktop to list of handlers for `x-scheme-handler/terminal` 10 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 11 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `x-scheme-handler/terminal`: tests/assets/org.wezfurlong.wezterm.desktop; 12 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Showing handler for `text/plain` 14 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: true 15 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: tests/assets/Helix.desktop; 16 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 17 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 18 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished showing handler 20 | -------------------------------------------------------------------------------- /src/common/table.rs: -------------------------------------------------------------------------------- 1 | use tabled::{ 2 | settings::{themes::Colorization, Alignment, Color, Padding, Style}, 3 | Table, Tabled, 4 | }; 5 | 6 | /// Render a table from a vector of instances of Tabled structs 7 | pub fn render_table(rows: &Vec, terminal_output: bool) -> String { 8 | let mut table = Table::new(rows); 9 | 10 | if terminal_output { 11 | // If output is going to a terminal, print as a table 12 | table 13 | .with(Style::sharp()) 14 | .with(Colorization::rows([Color::FG_WHITE, Color::BG_BLACK])) 15 | } else { 16 | // If output is being piped, print as tab-delimited text 17 | table 18 | .with(Style::empty().vertical('\t')) 19 | .with(Alignment::left()) 20 | .with(Padding::zero()) 21 | } 22 | .to_string() 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::*; 28 | use crate::error::Result; 29 | use itertools::Itertools; 30 | 31 | #[derive(Tabled)] 32 | struct TestRow<'a> { 33 | col1: &'a str, 34 | col2: &'a str, 35 | } 36 | 37 | // Arbitrary sample text 38 | const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; 39 | 40 | // Helper function to create test data 41 | fn rows(test_text: &str) -> Vec> { 42 | test_text 43 | .split(' ') 44 | .collect_vec() 45 | .chunks_exact(2) 46 | .map(|chunk| TestRow { 47 | col1: chunk[0], 48 | col2: chunk[1], 49 | }) 50 | .collect_vec() 51 | } 52 | 53 | #[test] 54 | fn terminal_output() -> Result<()> { 55 | insta::assert_snapshot!(render_table(&rows(LOREM_IPSUM), true)); 56 | Ok(()) 57 | } 58 | 59 | #[test] 60 | fn piped_output() -> Result<()> { 61 | insta::assert_snapshot!(render_table(&rows(LOREM_IPSUM), false)); 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_detailed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | Default Apps 6 | ┌─────────────────────────────────────────────────┬─────────────────────┐ 7 | │ mime  │ handlers  │ 8 | ├─────────────────────────────────────────────────┼─────────────────────┤ 9 | │ application/vnd.oasis.opendocument.*  │ startcenter.desktop │ 10 | │ application/vnd.openxmlformats-officedocument.* │ startcenter.desktop │ 11 | │ text/plain  │ helix.desktop,   │ 12 | │ │ nvim.desktop,   │ 13 | │ │ kakoune.desktop  │ 14 | │ video/asdf  │ mpv.desktop  │ 15 | │ video/mp4  │ mpv.desktop  │ 16 | │ video/webm  │ brave.desktop  │ 17 | └─────────────────────────────────────────────────┴─────────────────────┘ 18 | Added associations 19 | ┌───────────────────────────┬────────────────────────────────┐ 20 | │ mime  │ handlers  │ 21 | ├───────────────────────────┼────────────────────────────────┤ 22 | │ x-scheme-handler/terminal │ org.wezfurlong.wezterm.desktop │ 23 | └───────────────────────────┴────────────────────────────────┘ 24 | System Apps 25 | ┌──────┬──────────┐ 26 | │ mime │ handlers │ 27 | ├──────┼──────────┤ 28 | -------------------------------------------------------------------------------- /src/apps/snapshots/handlr__apps__user__tests__remove_handlers_expand_wildcards.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apps/user.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | [Default Applications] 6 | text/*=nvim.desktop; 7 | text/cache-manifest=nvim.desktop; 8 | text/calendar=nvim.desktop; 9 | text/coffeescript=nvim.desktop; 10 | text/css=nvim.desktop; 11 | text/csv=nvim.desktop; 12 | text/html=nvim.desktop; 13 | text/jade=nvim.desktop; 14 | text/javascript=nvim.desktop; 15 | text/jsx=nvim.desktop; 16 | text/less=nvim.desktop; 17 | text/markdown=nvim.desktop; 18 | text/mathml=nvim.desktop; 19 | text/mdx=nvim.desktop; 20 | text/n3=nvim.desktop; 21 | text/plain=nvim.desktop; 22 | text/prs.lines.tag=nvim.desktop; 23 | text/richtext=nvim.desktop; 24 | text/rtf=nvim.desktop; 25 | text/sgml=nvim.desktop; 26 | text/shex=nvim.desktop; 27 | text/slim=nvim.desktop; 28 | text/spdx=nvim.desktop; 29 | text/stylus=nvim.desktop; 30 | text/tab-separated-values=nvim.desktop; 31 | text/troff=nvim.desktop; 32 | text/turtle=nvim.desktop; 33 | text/uri-list=nvim.desktop; 34 | text/vcard=nvim.desktop; 35 | text/vnd.curl=nvim.desktop; 36 | text/vnd.curl.dcurl=nvim.desktop; 37 | text/vnd.curl.mcurl=nvim.desktop; 38 | text/vnd.curl.scurl=nvim.desktop; 39 | text/vnd.dvb.subtitle=nvim.desktop; 40 | text/vnd.familysearch.gedcom=nvim.desktop; 41 | text/vnd.fly=nvim.desktop; 42 | text/vnd.fmi.flexstor=nvim.desktop; 43 | text/vnd.graphviz=nvim.desktop; 44 | text/vnd.in3d.3dml=nvim.desktop; 45 | text/vnd.in3d.spot=nvim.desktop; 46 | text/vnd.sun.j2me.app-descriptor=nvim.desktop; 47 | text/vnd.wap.wml=nvim.desktop; 48 | text/vnd.wap.wmlscript=nvim.desktop; 49 | text/vtt=nvim.desktop; 50 | text/wgsl=nvim.desktop; 51 | text/x-asm=nvim.desktop; 52 | text/x-c=nvim.desktop; 53 | text/x-component=nvim.desktop; 54 | text/x-fortran=nvim.desktop; 55 | text/x-handlebars-template=nvim.desktop; 56 | text/x-java-source=nvim.desktop; 57 | text/x-lua=nvim.desktop; 58 | text/x-markdown=nvim.desktop; 59 | text/x-nfo=nvim.desktop; 60 | text/x-opml=nvim.desktop; 61 | text/x-org=nvim.desktop; 62 | text/x-pascal=nvim.desktop; 63 | text/x-processing=nvim.desktop; 64 | text/x-sass=nvim.desktop; 65 | text/x-scss=nvim.desktop; 66 | text/x-setext=nvim.desktop; 67 | text/x-sfv=nvim.desktop; 68 | text/x-suse-ymp=nvim.desktop; 69 | text/x-uuencode=nvim.desktop; 70 | text/x-vcalendar=nvim.desktop; 71 | text/x-vcard=nvim.desktop; 72 | text/xml=nvim.desktop; 73 | text/yaml=nvim.desktop; 74 | -------------------------------------------------------------------------------- /src/common/snapshots/handlr__common__path__tests__mime_table_terminal.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/common/path.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | ┌─────────────────────────────────────────────┬───────────────────────────┐ 6 | │ path  │ mime  │ 7 | ├─────────────────────────────────────────────┼───────────────────────────┤ 8 | │ tests/assets  │ inode/directory  │ 9 | │ tests/assets/cat  │ application/x-shellscript │ 10 | │ tests/assets/cmus.desktop  │ application/x-desktop  │ 11 | │ tests/assets/empty.txt  │ text/plain  │ 12 | │ tests/assets/no_html_tags.html  │ text/html  │ 13 | │ tests/assets/org.wezfurlong.wezterm.desktop │ application/x-desktop  │ 14 | │ tests/assets/p.html  │ text/html  │ 15 | │ tests/assets/rust.vim  │ text/plain  │ 16 | │ tests/assets/SettingsWidgetFdoSecrets.ui  │ application/x-designer  │ 17 | │ https://duckduckgo.com/  │ x-scheme-handler/https  │ 18 | │ .  │ inode/directory  │ 19 | │ README.md  │ text/markdown  │ 20 | └─────────────────────────────────────────────┴───────────────────────────┘ 21 | -------------------------------------------------------------------------------- /src/apps/snapshots/handlr__apps__user__tests__unset_handlers_expand_wildcards.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apps/user.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | [Default Applications] 6 | text/cache-manifest=Helix.desktop; 7 | text/calendar=Helix.desktop; 8 | text/coffeescript=Helix.desktop; 9 | text/css=Helix.desktop; 10 | text/csv=Helix.desktop; 11 | text/html=Helix.desktop; 12 | text/jade=Helix.desktop; 13 | text/javascript=Helix.desktop; 14 | text/jsx=Helix.desktop; 15 | text/less=Helix.desktop; 16 | text/markdown=Helix.desktop; 17 | text/mathml=Helix.desktop; 18 | text/mdx=Helix.desktop; 19 | text/n3=Helix.desktop; 20 | text/plain=Helix.desktop; 21 | text/prs.lines.tag=Helix.desktop; 22 | text/richtext=Helix.desktop; 23 | text/rtf=Helix.desktop; 24 | text/sgml=Helix.desktop; 25 | text/shex=Helix.desktop; 26 | text/slim=Helix.desktop; 27 | text/spdx=Helix.desktop; 28 | text/stylus=Helix.desktop; 29 | text/tab-separated-values=Helix.desktop; 30 | text/troff=Helix.desktop; 31 | text/turtle=Helix.desktop; 32 | text/uri-list=Helix.desktop; 33 | text/vcard=Helix.desktop; 34 | text/vnd.curl=Helix.desktop; 35 | text/vnd.curl.dcurl=Helix.desktop; 36 | text/vnd.curl.mcurl=Helix.desktop; 37 | text/vnd.curl.scurl=Helix.desktop; 38 | text/vnd.dvb.subtitle=Helix.desktop; 39 | text/vnd.familysearch.gedcom=Helix.desktop; 40 | text/vnd.fly=Helix.desktop; 41 | text/vnd.fmi.flexstor=Helix.desktop; 42 | text/vnd.graphviz=Helix.desktop; 43 | text/vnd.in3d.3dml=Helix.desktop; 44 | text/vnd.in3d.spot=Helix.desktop; 45 | text/vnd.sun.j2me.app-descriptor=Helix.desktop; 46 | text/vnd.wap.wml=Helix.desktop; 47 | text/vnd.wap.wmlscript=Helix.desktop; 48 | text/vtt=Helix.desktop; 49 | text/wgsl=Helix.desktop; 50 | text/x-asm=Helix.desktop; 51 | text/x-c=Helix.desktop; 52 | text/x-component=Helix.desktop; 53 | text/x-fortran=Helix.desktop; 54 | text/x-handlebars-template=Helix.desktop; 55 | text/x-java-source=Helix.desktop; 56 | text/x-lua=Helix.desktop; 57 | text/x-markdown=Helix.desktop; 58 | text/x-nfo=Helix.desktop; 59 | text/x-opml=Helix.desktop; 60 | text/x-org=Helix.desktop; 61 | text/x-pascal=Helix.desktop; 62 | text/x-processing=Helix.desktop; 63 | text/x-sass=Helix.desktop; 64 | text/x-scss=Helix.desktop; 65 | text/x-setext=Helix.desktop; 66 | text/x-sfv=Helix.desktop; 67 | text/x-suse-ymp=Helix.desktop; 68 | text/x-uuencode=Helix.desktop; 69 | text/x-vcalendar=Helix.desktop; 70 | text/x-vcard=Helix.desktop; 71 | text/xml=Helix.desktop; 72 | text/yaml=Helix.desktop; 73 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod apps; 2 | mod cli; 3 | mod common; 4 | mod config; 5 | mod error; 6 | mod logging; 7 | mod testing; 8 | 9 | use std::process::ExitCode; 10 | 11 | use cli::{Cli, Cmd}; 12 | use common::mime_table; 13 | use config::Config; 14 | use error::Result; 15 | use logging::init_tracing; 16 | 17 | use clap::{CommandFactory, Parser}; 18 | use clap_complete::CompleteEnv; 19 | use tracing::debug; 20 | 21 | #[mutants::skip] // Cannot test directly at the moment 22 | fn main() -> ExitCode { 23 | // Shell completions 24 | CompleteEnv::with_factory(|| Cli::command().name("handlr")) 25 | .completer("handlr") 26 | .complete(); 27 | 28 | let cli = Cli::parse(); 29 | 30 | let _guard = init_tracing(&cli) 31 | .expect("handlr error: Could not initialize global tracing subscriber"); 32 | 33 | error::handle(run(cli)) 34 | } 35 | 36 | /// Run main program logic 37 | #[mutants::skip] // Cannot test directly at the moment 38 | fn run(cli: Cli) -> Result<()> { 39 | let mut config = Config::new(cli.terminal_output())?; 40 | 41 | let mut stdout = std::io::stdout().lock(); 42 | 43 | debug!("Interactive terminal detected: {}", config.terminal_output); 44 | 45 | match cli.command { 46 | Cmd::Set { mime, handler } => config.set_handler(&mime, &handler), 47 | Cmd::Add { mime, handler } => config.add_handler(&mime, &handler), 48 | Cmd::Launch { 49 | mime, 50 | args, 51 | selector_args, 52 | } => { 53 | config.override_selector(selector_args); 54 | config.launch_handler(&mime, args) 55 | } 56 | Cmd::Get { 57 | mime, 58 | json, 59 | selector_args, 60 | } => { 61 | config.override_selector(selector_args); 62 | config.show_handler(&mut stdout, &mime, json) 63 | } 64 | Cmd::Open { 65 | paths, 66 | selector_args, 67 | } => { 68 | config.override_selector(selector_args); 69 | config.open_paths(&paths) 70 | } 71 | Cmd::Mime { paths, json } => { 72 | mime_table(&mut stdout, &paths, json, config.terminal_output) 73 | } 74 | Cmd::List { all, json } => config.print(&mut stdout, all, json), 75 | Cmd::Unset { mime } => config.unset_handler(&mime), 76 | Cmd::Remove { mime, handler } => config.remove_handler(&mime, &handler), 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/config/config_file.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::SelectorArgs, 3 | common::{RegexApps, RegexHandler, UserPath}, 4 | error::Result, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | use tracing::debug; 8 | 9 | /// The config file 10 | #[derive(Debug, Serialize, Deserialize)] 11 | #[serde(default)] 12 | pub struct ConfigFile { 13 | /// Whether to enable the selector when multiple handlers are set 14 | pub enable_selector: bool, 15 | /// The selector command to run 16 | pub selector: String, 17 | /// Extra arguments to pass to terminal application 18 | pub term_exec_args: Option, 19 | /// Whether to expand wildcards when saving mimeapps.list 20 | pub expand_wildcards: bool, 21 | /// Regex handlers 22 | // NOTE: Serializing is only necessary for generating a default config file 23 | #[serde(skip_serializing)] 24 | pub handlers: RegexApps, 25 | } 26 | 27 | impl Default for ConfigFile { 28 | fn default() -> Self { 29 | ConfigFile { 30 | enable_selector: false, 31 | selector: "rofi -dmenu -i -p 'Open With: '".into(), 32 | // Required for many xterm-compatible terminal emulators 33 | // Unfortunately, messes up emulators that don't accept it 34 | term_exec_args: Some("-e".into()), 35 | expand_wildcards: false, 36 | handlers: Default::default(), 37 | } 38 | } 39 | } 40 | 41 | impl ConfigFile { 42 | /// Get the handler associated with a given mime from the config file's regex handlers 43 | pub fn get_regex_handler(&self, path: &UserPath) -> Result { 44 | self.handlers.get_handler(path) 45 | } 46 | 47 | /// Load ~/.config/handlr/handlr.toml 48 | #[mutants::skip] // Cannot test directly, depends on system state 49 | pub fn load() -> Result { 50 | Ok(confy::load("handlr")?) 51 | } 52 | 53 | /// Override the set selector 54 | /// Currently assumes the config file will never be saved to 55 | pub fn override_selector(&mut self, selector_args: SelectorArgs) { 56 | if let Some(selector) = selector_args.selector { 57 | debug!("Overriding selector command: {}", selector); 58 | self.selector = selector; 59 | } 60 | 61 | self.enable_selector = selector_args 62 | .enable_selector 63 | .unwrap_or(self.enable_selector); 64 | 65 | debug!("Selector enabled: {}", self.enable_selector); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{path::PathBuf, process::ExitCode}; 2 | 3 | use tracing::{error, info}; 4 | 5 | /// Custom error type 6 | #[derive(Debug, thiserror::Error)] 7 | pub enum Error { 8 | #[error(transparent)] 9 | Io(#[from] std::io::Error), 10 | #[error(transparent)] 11 | Xdg(#[from] xdg::BaseDirectoriesError), 12 | #[error(transparent)] 13 | Config(#[from] confy::ConfyError), 14 | #[error("No handlers found for '{0}'")] 15 | NotFound(String), 16 | #[error( 17 | "Could not find a mimetype associated with the file extension: '{0}'" 18 | )] 19 | AmbiguousExtension(String), 20 | #[error(transparent)] 21 | BadMimeType(#[from] mime::FromStrError), 22 | #[error("Bad mime: {0}")] 23 | InvalidMime(mime::Mime), 24 | #[error("The desktop entry at '{0}' lacks a valid '{1}' field")] 25 | BadEntry(PathBuf, String), 26 | #[error("Error spawning selector process '{0}'")] 27 | Selector(String), 28 | #[error("Selection cancelled")] 29 | Cancelled, 30 | #[error("Please specify the default terminal with handlr set x-scheme-handler/terminal")] 31 | NoTerminal, 32 | #[error("Bad path: {0}")] 33 | BadPath(String), 34 | #[error(transparent)] 35 | SerdeJson(#[from] serde_json::Error), 36 | #[error(transparent)] 37 | SerdeIniDe(#[from] serde_ini::de::Error), 38 | #[error(transparent)] 39 | SerdeIniSer(#[from] serde_ini::ser::Error), 40 | #[error(transparent)] 41 | TracingGlobalDefault(#[from] tracing::dispatcher::SetGlobalDefaultError), 42 | #[error("Could not find file at path: '{0}'")] 43 | NonexistentFile(String), 44 | #[cfg(test)] 45 | #[error(transparent)] 46 | BadUrl(#[from] url::ParseError), 47 | #[cfg(test)] 48 | #[error(transparent)] 49 | FromUtf8(#[from] std::string::FromUtf8Error), 50 | } 51 | 52 | pub type Result = std::result::Result; 53 | 54 | #[mutants::skip] // Cannot completely test, relies on user input 55 | pub fn handle(result: Result<()>) -> ExitCode { 56 | if let Err(error) = result { 57 | match error { 58 | // Cancelling the selector is an acceptable outcome 59 | Error::Cancelled => { 60 | info!("{}", error); 61 | ExitCode::SUCCESS 62 | } 63 | _ => { 64 | error!("{}", error); 65 | ExitCode::FAILURE 66 | } 67 | } 68 | } else { 69 | ExitCode::SUCCESS 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__show_handler_json-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/Helix.desktop to list of handlers for `text/plain` 6 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 7 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: tests/assets/Helix.desktop; 8 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 9 | [TIMESTAMP]  INFO handlr::config::main_config: Adding tests/assets/org.wezfurlong.wezterm.desktop to list of handlers for `x-scheme-handler/terminal` 10 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 11 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `x-scheme-handler/terminal`: tests/assets/org.wezfurlong.wezterm.desktop; 12 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Showing handler for `text/plain` 14 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: true 15 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: tests/assets/Helix.desktop; 16 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 17 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 18 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 19 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `x-scheme-handler/terminal` in mimeapps.list Default Associations: tests/assets/org.wezfurlong.wezterm.desktop; 20 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 21 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 22 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `x-scheme-handler/terminal` in mimeapps.list Default Associations 23 | [TIMESTAMP]  INFO handlr::config::main_config: Finished showing handler 24 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "naersk": { 4 | "inputs": { 5 | "nixpkgs": "nixpkgs" 6 | }, 7 | "locked": { 8 | "lastModified": 1745925850, 9 | "narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=", 10 | "owner": "nix-community", 11 | "repo": "naersk", 12 | "rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "nix-community", 17 | "ref": "master", 18 | "repo": "naersk", 19 | "type": "github" 20 | } 21 | }, 22 | "nixpkgs": { 23 | "locked": { 24 | "lastModified": 1747426788, 25 | "narHash": "sha256-N4cp0asTsJCnRMFZ/k19V9akkxb7J/opG+K+jU57JGc=", 26 | "owner": "NixOS", 27 | "repo": "nixpkgs", 28 | "rev": "12a55407652e04dcf2309436eb06fef0d3713ef3", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "NixOS", 33 | "ref": "nixpkgs-unstable", 34 | "repo": "nixpkgs", 35 | "type": "github" 36 | } 37 | }, 38 | "nixpkgs_2": { 39 | "locked": { 40 | "lastModified": 1747426788, 41 | "narHash": "sha256-N4cp0asTsJCnRMFZ/k19V9akkxb7J/opG+K+jU57JGc=", 42 | "owner": "NixOS", 43 | "repo": "nixpkgs", 44 | "rev": "12a55407652e04dcf2309436eb06fef0d3713ef3", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "NixOS", 49 | "ref": "nixpkgs-unstable", 50 | "repo": "nixpkgs", 51 | "type": "github" 52 | } 53 | }, 54 | "root": { 55 | "inputs": { 56 | "naersk": "naersk", 57 | "nixpkgs": "nixpkgs_2", 58 | "utils": "utils" 59 | } 60 | }, 61 | "systems": { 62 | "locked": { 63 | "lastModified": 1681028828, 64 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 65 | "owner": "nix-systems", 66 | "repo": "default", 67 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 68 | "type": "github" 69 | }, 70 | "original": { 71 | "owner": "nix-systems", 72 | "repo": "default", 73 | "type": "github" 74 | } 75 | }, 76 | "utils": { 77 | "inputs": { 78 | "systems": "systems" 79 | }, 80 | "locked": { 81 | "lastModified": 1731533236, 82 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 83 | "owner": "numtide", 84 | "repo": "flake-utils", 85 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 86 | "type": "github" 87 | }, 88 | "original": { 89 | "owner": "numtide", 90 | "repo": "flake-utils", 91 | "type": "github" 92 | } 93 | } 94 | }, 95 | "root": "root", 96 | "version": 7 97 | } 98 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__wildcard_mimes.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/*` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/*`: mpv.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `video/mp4` in mimeapps.list Default Associations: mpv.desktop; 16 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 17 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 18 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `video/mp4` in mimeapps.list Default Associations 19 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `video/asdf` in mimeapps.list Default Associations: mpv.desktop; 20 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 21 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 22 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `video/asdf` in mimeapps.list Default Associations 23 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `video/webm` in mimeapps.list Default Associations: brave.desktop; 24 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 25 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 26 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `video/webm` in mimeapps.list Default Associations 27 | -------------------------------------------------------------------------------- /tests/assets/vlc.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=VLC media player 4 | Comment=Read, capture, broadcast your multimedia streams 5 | Name[bn]=VLC মিডিয়া প্লেয়ার 6 | Comment[bn]=আপনার মাল্টিমিডিয়া স্ট্রীম পড়ুন, ধরে রাখুন এবং ছড়িয়ে দিন 7 | Name[ca]=Reproductor multimèdia VLC 8 | Comment[ca]=Reproduïu, captureu i difoneu fluxos multimèdia 9 | Name[de]=VLC Media Player 10 | Comment[de]=Wiedergabe, Aufnahme und Verbreitung Ihrer Multimedia-Streams 11 | Name[es]=Reproductor multimedia VLC 12 | Comment[es]=Lea, capture y emita sus contenidos multimedia 13 | Name[et]=VLC meediaesitaja 14 | Comment[et]=Multimeediafailide taasesitamine, lindistamine ja edastamine 15 | Name[fi]=VLC-mediasoitin 16 | Comment[fi]=Toista, tallenna ja lähetä multimediaa 17 | Name[fr]=Lecteur multimédia VLC 18 | Comment[fr]=Lire, capturer, diffuser vos flux multimedia 19 | Name[gl]=Reprodutor multimedia VLC 20 | Comment[gl]=Lea, capture e emita os seus fluxos multimedia 21 | Name[hu]=VLC médialejátszó 22 | Comment[hu]=Multimédiás adatfolyamok olvasása, mentése, szórása 23 | Name[it]=Lettore multimediale VLC 24 | Comment[it]=Legge, acquisisce e trasmette i tuoi flussi multimediali 25 | Name[km]=កម្មវិធី​ចាក់​មេឌៀ VLC 26 | Comment[km]=អាន ចាប់យក ប្រកាស​ស្ទ្រីម​ពហុមេឌៀ​របស់​អ្នក 27 | Name[nl]=VLC Mediaspeler 28 | Comment[nl]=Uw multimediastreams afspelen, opnemen en uitzenden 29 | Name[nn]=VLC mediespelar 30 | Comment[nn]=Spel av, ta opp og send ut multimedia 31 | Name[pa]=VLC ਮੀਡਿਆ ਪਲੇਅਰ 32 | Comment[pa]=ਆਪਣੀ ਮਲਟੀਮੀਡਿਆ ਸਟਰੀਮ ਪੜ੍ਹੋ, ਕੈਪਚਰ ਤੇ ਬਰਾਡਕਾਸਟ ਕਰੋ 33 | Name[pt_BR]=Reprodutor de Mídias VLC 34 | Comment[pt_BR]=Reproduza, capture e transmita os seus fluxos multimídia 35 | Name[sv]=Mediaspelaren VLC 36 | Comment[sv]=Allmän uppspelare av film och musik 37 | Name[sk]=VLC media player 38 | Comment[sk]=Naèítavajte, zaznamenávajte, vysielajte svoje multimediálne streamy 39 | Name[ru]=Медиаплеер VLC 40 | Comment[ru]=Универсальный проигрыватель видео и аудио 41 | Name[pl]=VLC media player - odtwarzacz multimedialny 42 | Comment[pl]=Odczytywanie, przechwytywanie i nadawanie strumieni multimedialnych 43 | Name[lt]=VLC grotuvas 44 | Comment[lt]=Groti, įrašyti, siųsti daugialypės terpės kūrinius 45 | Name[ja]=VLCメディアプレイヤー 46 | Comment[ja]=マルチメディアストリームの読み込み、キャプチャー、ブロードキャスト 47 | Name[wa]=Djouweu d' media VLC 48 | Comment[wa]=Lét, egaloye, evoye vos floûs multimedia 49 | Name[zh_CN]=VLC media player 50 | Comment[zh_CN]=为您读取、捕获或发送多媒体流 51 | Exec=vlc %U 52 | Icon=vlc 53 | Terminal=false 54 | Type=Application 55 | Categories=AudioVideo;Player; 56 | MimeType=video/dv;video/mpeg;video/x-mpeg;video/msvideo;video/quicktime;video/x-anim;video/x-avi;video/x-ms-asf;video/x-ms-wmv;video/x-msvideo;video/x-nsv;video/x-flc;video/x-fli;video/x-flv;video/vnd.rn-realvideo;video/mp4;video/mp4v-es;video/mp2t;application/ogg;application/x-ogg;application/x-matroska;audio/x-mp3;audio/x-mpeg;audio/mpeg;audio/x-wav;audio/x-mpegurl;audio/x-scpls;audio/x-m4a;audio/x-ms-asf;audio/x-ms-asx;audio/x-ms-wax;application/vnd.rn-realmedia;audio/x-real-audio;audio/x-pn-realaudio;application/x-flac;audio/x-flac;application/x-shockwave-flash;misc/ultravox;audio/vnd.rn-realaudio;audio/x-pn-aiff;audio/x-pn-au;audio/x-pn-wav;audio/x-pn-windows-acm;image/vnd.rn-realpix;audio/x-pn-realaudio-plugin;application/x-extension-mp4;audio/mp4;x-content/video-vcd;x-content/video-svcd;x-content/video-dvd;x-content/audio-cdda;x-content/audio-player; 57 | -------------------------------------------------------------------------------- /tests/assets/Helix.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Helix 3 | GenericName=Text Editor 4 | GenericName[ar]=مُحَرِّرُ نُصُوص 5 | GenericName[de]=Texteditor 6 | GenericName[fr]=Éditeur de texte 7 | GenericName[ru]=Текстовый редактор 8 | GenericName[sr]=Едитор текст 9 | GenericName[tr]=Metin Düzenleyici 10 | Comment=Edit text files 11 | Comment[af]=Redigeer tekslêers 12 | Comment[am]=የጽሑፍ ፋይሎች ያስተካክሉ 13 | Comment[ar]=مُحَرِّرُ مِلَفَّاتٍ نَصِّيَّة 14 | Comment[az]=Mətn fayllarını redaktə edin 15 | Comment[be]=Рэдагаваньне тэкставых файлаў 16 | Comment[bg]=Редактиране на текстови файлове 17 | Comment[bn]=টেক্স্ট ফাইল এডিট করুন 18 | Comment[bs]=Izmijeni tekstualne datoteke 19 | Comment[ca]=Edita fitxers de text 20 | Comment[cs]=Úprava textových souborů 21 | Comment[cy]=Golygu ffeiliau testun 22 | Comment[da]=Redigér tekstfiler 23 | Comment[de]=Textdateien bearbeiten 24 | Comment[el]=Επεξεργασία αρχείων κειμένου 25 | Comment[en_CA]=Edit text files 26 | Comment[en_GB]=Edit text files 27 | Comment[es]=Edita archivos de texto 28 | Comment[et]=Redigeeri tekstifaile 29 | Comment[eu]=Editatu testu-fitxategiak 30 | Comment[fa]=ویرایش پرونده‌های متنی 31 | Comment[fi]=Muokkaa tekstitiedostoja 32 | Comment[fr]=Éditer des fichiers texte 33 | Comment[ga]=Eagar comhad Téacs 34 | Comment[gu]=લખાણ ફાઇલોમાં ફેરફાર કરો 35 | Comment[he]=ערוך קבצי טקסט 36 | Comment[hi]=पाठ फ़ाइलें संपादित करें 37 | Comment[hr]=Uređivanje tekstualne datoteke 38 | Comment[hu]=Szövegfájlok szerkesztése 39 | Comment[id]=Edit file teks 40 | Comment[it]=Modifica file di testo 41 | Comment[ja]=テキストファイルを編集します 42 | Comment[kn]=ಪಠ್ಯ ಕಡತಗಳನ್ನು ಸಂಪಾದಿಸು 43 | Comment[ko]=텍스트 파일을 편집합니다 44 | Comment[lt]=Redaguoti tekstines bylas 45 | Comment[lv]=Rediģēt teksta failus 46 | Comment[mk]=Уреди текстуални фајлови 47 | Comment[ml]=വാചക രചനകള് തിരുത്തുക 48 | Comment[mn]=Текст файл боловсруулах 49 | Comment[mr]=गद्य फाइल संपादित करा 50 | Comment[ms]=Edit fail teks 51 | Comment[nb]=Rediger tekstfiler 52 | Comment[ne]=पाठ फाइललाई संशोधन गर्नुहोस् 53 | Comment[nl]=Tekstbestanden bewerken 54 | Comment[nn]=Rediger tekstfiler 55 | Comment[no]=Rediger tekstfiler 56 | Comment[or]=ପାଠ୍ଯ ଫାଇଲଗୁଡ଼ିକୁ ସମ୍ପାଦନ କରନ୍ତୁ 57 | Comment[pa]=ਪਾਠ ਫਾਇਲਾਂ ਸੰਪਾਦਨ 58 | Comment[pl]=Edytor plików tekstowych 59 | Comment[pt]=Editar ficheiros de texto 60 | Comment[pt_BR]=Edite arquivos de texto 61 | Comment[ro]=Editare fişiere text 62 | Comment[ru]=Редактирование текстовых файлов 63 | Comment[sk]=Úprava textových súborov 64 | Comment[sl]=Urejanje datotek z besedili 65 | Comment[sq]=Përpuno files teksti 66 | Comment[sr]=Уређујте текст фајлове 67 | Comment[sr@Latn]=Izmeni tekstualne datoteke 68 | Comment[sv]=Redigera textfiler 69 | Comment[ta]=உரை கோப்புகளை தொகுக்கவும் 70 | Comment[th]=แก้ไขแฟ้มข้อความ 71 | Comment[tk]=Metin faýllary editle 72 | Comment[tr]=Metin dosyaları düzenleyin 73 | Comment[uk]=Редактор текстових файлів 74 | Comment[vi]=Soạn thảo tập tin văn bản 75 | Comment[wa]=Asspougnî des fitchîs tecses 76 | Comment[zh_CN]=编辑文本文件 77 | Comment[zh_TW]=編輯文字檔 78 | TryExec=hx 79 | Exec=hx %F 80 | Terminal=true 81 | Type=Application 82 | Keywords=Text;editor; 83 | Keywords[ar]=نص;نصوص;محرر; 84 | Keywords[fr]=Texte;éditeur; 85 | Keywords[ru]=текст;текстовый редактор; 86 | Keywords[sr]=Текст;едитор; 87 | Keywords[tr]=Metin;düzenleyici; 88 | Icon=helix 89 | Categories=Utility;TextEditor; 90 | StartupNotify=false 91 | MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++; 92 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__set_and_unset_handlers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Setting `Helix.desktop` as handler for `text/plain` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `Helix.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: Helix.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished setting handler 10 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: Helix.desktop; 11 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 12 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 14 | [TIMESTAMP]  INFO handlr::config::main_config: Setting `nvim.desktop` as handler for `text/plain` 15 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 16 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 17 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: nvim.desktop; 18 | [TIMESTAMP]  INFO handlr::config::main_config: Finished setting handler 19 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: nvim.desktop; 20 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 21 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 22 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 23 | [TIMESTAMP]  INFO handlr::config::main_config: Unsetting handler for `text/plain` 24 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: 25 | [TIMESTAMP]  INFO handlr::config::main_config: Finished unsetting handler 26 | [TIMESTAMP]  INFO handlr::apps::user: No handlers configured for `text/plain` in mimeapps.list Default associations 27 | [TIMESTAMP]  INFO handlr::config::main_config: No match for `text/plain` in mimeapps.list Default Associations 28 | [TIMESTAMP]  INFO handlr::config::main_config: No matching entries for `text/plain` in mimeapps.list Added Associations 29 | [TIMESTAMP] DEBUG handlr::apps::system: No installed handlers found for `text/plain` 30 | [TIMESTAMP]  INFO handlr::config::main_config: No matching installed handlers found for `text/plain` 31 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__add_and_unset_handlers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding Helix.desktop to list of handlers for `text/plain` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `Helix.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: Helix.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: Helix.desktop; 11 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 12 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 14 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 15 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 16 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 17 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: Helix.desktop;nvim.desktop; 18 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 19 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: Helix.desktop;nvim.desktop; 20 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 2 21 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 22 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 23 | [TIMESTAMP]  INFO handlr::config::main_config: Unsetting handler for `text/plain` 24 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: 25 | [TIMESTAMP]  INFO handlr::config::main_config: Finished unsetting handler 26 | [TIMESTAMP]  INFO handlr::apps::user: No handlers configured for `text/plain` in mimeapps.list Default associations 27 | [TIMESTAMP]  INFO handlr::config::main_config: No match for `text/plain` in mimeapps.list Default Associations 28 | [TIMESTAMP]  INFO handlr::config::main_config: No matching entries for `text/plain` in mimeapps.list Added Associations 29 | [TIMESTAMP] DEBUG handlr::apps::system: No installed handlers found for `text/plain` 30 | [TIMESTAMP]  INFO handlr::config::main_config: No matching installed handlers found for `text/plain` 31 | -------------------------------------------------------------------------------- /src/apps/snapshots/handlr__apps__user__tests__set_handlers_expand_wildcards.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apps/user.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | [Default Applications] 6 | application/vnd.oasis.opendocument.chart=startcenter.desktop; 7 | application/vnd.oasis.opendocument.chart-template=startcenter.desktop; 8 | application/vnd.oasis.opendocument.database=startcenter.desktop; 9 | application/vnd.oasis.opendocument.formula=startcenter.desktop; 10 | application/vnd.oasis.opendocument.formula-template=startcenter.desktop; 11 | application/vnd.oasis.opendocument.graphics=startcenter.desktop; 12 | application/vnd.oasis.opendocument.graphics-template=startcenter.desktop; 13 | application/vnd.oasis.opendocument.image=startcenter.desktop; 14 | application/vnd.oasis.opendocument.image-template=startcenter.desktop; 15 | application/vnd.oasis.opendocument.presentation=startcenter.desktop; 16 | application/vnd.oasis.opendocument.presentation-template=startcenter.desktop; 17 | application/vnd.oasis.opendocument.spreadsheet=startcenter.desktop; 18 | application/vnd.oasis.opendocument.spreadsheet-template=startcenter.desktop; 19 | application/vnd.oasis.opendocument.text=startcenter.desktop; 20 | application/vnd.oasis.opendocument.text-master=startcenter.desktop; 21 | application/vnd.oasis.opendocument.text-template=startcenter.desktop; 22 | application/vnd.oasis.opendocument.text-web=startcenter.desktop; 23 | text/cache-manifest=Helix.desktop; 24 | text/calendar=Helix.desktop; 25 | text/coffeescript=Helix.desktop; 26 | text/css=Helix.desktop; 27 | text/csv=Helix.desktop; 28 | text/html=Helix.desktop; 29 | text/jade=Helix.desktop; 30 | text/javascript=Helix.desktop; 31 | text/jsx=Helix.desktop; 32 | text/less=Helix.desktop; 33 | text/markdown=Helix.desktop; 34 | text/mathml=Helix.desktop; 35 | text/mdx=Helix.desktop; 36 | text/n3=Helix.desktop; 37 | text/plain=Helix.desktop; 38 | text/prs.lines.tag=Helix.desktop; 39 | text/richtext=Helix.desktop; 40 | text/rtf=Helix.desktop; 41 | text/sgml=Helix.desktop; 42 | text/shex=Helix.desktop; 43 | text/slim=Helix.desktop; 44 | text/spdx=Helix.desktop; 45 | text/stylus=Helix.desktop; 46 | text/tab-separated-values=Helix.desktop; 47 | text/troff=Helix.desktop; 48 | text/turtle=Helix.desktop; 49 | text/uri-list=Helix.desktop; 50 | text/vcard=Helix.desktop; 51 | text/vnd.curl=Helix.desktop; 52 | text/vnd.curl.dcurl=Helix.desktop; 53 | text/vnd.curl.mcurl=Helix.desktop; 54 | text/vnd.curl.scurl=Helix.desktop; 55 | text/vnd.dvb.subtitle=Helix.desktop; 56 | text/vnd.familysearch.gedcom=Helix.desktop; 57 | text/vnd.fly=Helix.desktop; 58 | text/vnd.fmi.flexstor=Helix.desktop; 59 | text/vnd.graphviz=Helix.desktop; 60 | text/vnd.in3d.3dml=Helix.desktop; 61 | text/vnd.in3d.spot=Helix.desktop; 62 | text/vnd.sun.j2me.app-descriptor=Helix.desktop; 63 | text/vnd.wap.wml=Helix.desktop; 64 | text/vnd.wap.wmlscript=Helix.desktop; 65 | text/vtt=Helix.desktop; 66 | text/wgsl=Helix.desktop; 67 | text/x-asm=Helix.desktop; 68 | text/x-c=Helix.desktop; 69 | text/x-component=Helix.desktop; 70 | text/x-fortran=Helix.desktop; 71 | text/x-handlebars-template=Helix.desktop; 72 | text/x-java-source=Helix.desktop; 73 | text/x-lua=Helix.desktop; 74 | text/x-markdown=Helix.desktop; 75 | text/x-nfo=Helix.desktop; 76 | text/x-opml=Helix.desktop; 77 | text/x-org=Helix.desktop; 78 | text/x-pascal=Helix.desktop; 79 | text/x-processing=Helix.desktop; 80 | text/x-sass=Helix.desktop; 81 | text/x-scss=Helix.desktop; 82 | text/x-setext=Helix.desktop; 83 | text/x-sfv=Helix.desktop; 84 | text/x-suse-ymp=Helix.desktop; 85 | text/x-uuencode=Helix.desktop; 86 | text/x-vcalendar=Helix.desktop; 87 | text/x-vcard=Helix.desktop; 88 | text/xml=Helix.desktop; 89 | text/yaml=Helix.desktop; 90 | video/mp4=mpv.desktop; 91 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use notify_rust::{Notification, Timeout, Urgency}; 2 | use tracing::{field::Visit, Level}; 3 | use tracing_appender::non_blocking::WorkerGuard; 4 | use tracing_subscriber::{filter, fmt, layer::SubscriberExt, Layer}; 5 | 6 | use crate::{cli::Cli, error::Result}; 7 | 8 | /// Init global tracing subscriber 9 | pub fn init_tracing(cli: &Cli) -> Result { 10 | let (file_writer, guard) = 11 | tracing_appender::non_blocking(tracing_appender::rolling::never( 12 | xdg::BaseDirectories::new()?.create_cache_directory("handlr")?, 13 | "handlr.log", 14 | )); 15 | 16 | // Filter logs based on `$RUST_LOG` and cli args 17 | let env_filter = std::env::var("RUST_LOG") 18 | .ok() 19 | .and_then(|var| var.parse::().ok()) 20 | .unwrap_or_else(|| filter::Targets::new().with_default(cli.verbosity)); 21 | 22 | tracing::subscriber::set_global_default( 23 | tracing_subscriber::registry() 24 | // Send filtered logs to stdout 25 | .with( 26 | fmt::Layer::new() 27 | .with_writer(std::io::stderr) 28 | .with_filter(env_filter.clone()), 29 | ) 30 | // Send all logs to a log file 31 | .with(fmt::Layer::new().with_writer(file_writer).with_ansi(false)) 32 | // Notify for filtered logs 33 | .with( 34 | cli.show_notifications() 35 | .then_some(NotificationLayer.with_filter(env_filter)), 36 | ) 37 | // Never any logs from other crates so the user is not overwhelmed by the output 38 | .with( 39 | filter::Targets::new() 40 | .with_target("handlr", Level::TRACE) 41 | .with_target("tracing_unwrap", Level::WARN), 42 | ), 43 | )?; 44 | 45 | Ok(guard) 46 | } 47 | 48 | /// Custom tracing layer for running a notification on relevant events 49 | struct NotificationLayer; 50 | 51 | impl Layer for NotificationLayer 52 | where 53 | S: tracing::Subscriber, 54 | { 55 | #[mutants::skip] // Cannot test, relies on dbus 56 | fn on_event( 57 | &self, 58 | event: &tracing::Event, 59 | _ctx: tracing_subscriber::layer::Context<'_, S>, 60 | ) { 61 | let mut message = String::new(); 62 | event.record(&mut NotificationVisitor(&mut message)); 63 | 64 | // Just in case a message has no message, but some other information 65 | if message.is_empty() { 66 | message = "No message, see ~/.cache/handlr/handlr.log for details" 67 | .to_string() 68 | } 69 | 70 | let (level, icon, urgency) = match *event.metadata().level() { 71 | tracing::Level::ERROR => { 72 | ("error".to_string(), "dialog-error", Urgency::Critical) 73 | } 74 | tracing::Level::WARN => { 75 | ("warning".to_string(), "dialog-warning", Urgency::Normal) 76 | } 77 | l => ( 78 | l.as_str().to_lowercase(), 79 | "dialog-information", 80 | Urgency::Low, 81 | ), 82 | }; 83 | 84 | Notification::new() 85 | .summary(&format!("handlr {}", level)) 86 | .body(&message) 87 | .icon(icon) 88 | .appname("handlr") 89 | .timeout(Timeout::Milliseconds(10_000)) 90 | .urgency(urgency) 91 | .show() 92 | .expect("handlr error: Could not issue dbus notification"); 93 | } 94 | } 95 | 96 | struct NotificationVisitor<'a>(&'a mut String); 97 | 98 | impl Visit for NotificationVisitor<'_> { 99 | #[mutants::skip] // Cannot test independently of NotificationLayer 100 | fn record_debug( 101 | &mut self, 102 | field: &tracing::field::Field, 103 | value: &dyn std::fmt::Debug, 104 | ) { 105 | if field.name() == "message" { 106 | self.0.push_str(&format!("{:?}", value)); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/common/snapshots/handlr__common__table__tests__terminal_output.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/common/table.rs 3 | expression: "render_table(&rows(LOREM_IPSUM), true)" 4 | --- 5 | ┌──────────────┬───────────────┐ 6 | │ col1  │ col2  │ 7 | ├──────────────┼───────────────┤ 8 | │ Lorem  │ ipsum  │ 9 | │ dolor  │ sit  │ 10 | │ amet,  │ consectetur  │ 11 | │ adipiscing  │ elit,  │ 12 | │ sed  │ do  │ 13 | │ eiusmod  │ tempor  │ 14 | │ incididunt  │ ut  │ 15 | │ labore  │ et  │ 16 | │ dolore  │ magna  │ 17 | │ aliqua.  │ Ut  │ 18 | │ enim  │ ad  │ 19 | │ minim  │ veniam,  │ 20 | │ quis  │ nostrud  │ 21 | │ exercitation │ ullamco  │ 22 | │ laboris  │ nisi  │ 23 | │ ut  │ aliquip  │ 24 | │ ex  │ ea  │ 25 | │ commodo  │ consequat.  │ 26 | │ Duis  │ aute  │ 27 | │ irure  │ dolor  │ 28 | │ in  │ reprehenderit │ 29 | │ in  │ voluptate  │ 30 | │ velit  │ esse  │ 31 | │ cillum  │ dolore  │ 32 | │ eu  │ fugiat  │ 33 | │ nulla  │ pariatur.  │ 34 | │ Excepteur  │ sint  │ 35 | │ occaecat  │ cupidatat  │ 36 | │ non  │ proident,  │ 37 | │ sunt  │ in  │ 38 | │ culpa  │ qui  │ 39 | │ officia  │ deserunt  │ 40 | │ mollit  │ anim  │ 41 | │ id  │ est  │ 42 | └──────────────┴───────────────┘ 43 | -------------------------------------------------------------------------------- /src/apps/snapshots/handlr__apps__user__tests__add_handlers_expand_wildcards.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/apps/user.rs 3 | expression: "String::from_utf8(buffer)?" 4 | --- 5 | [Default Applications] 6 | application/vnd.oasis.opendocument.chart=startcenter.desktop; 7 | application/vnd.oasis.opendocument.chart-template=startcenter.desktop; 8 | application/vnd.oasis.opendocument.database=startcenter.desktop; 9 | application/vnd.oasis.opendocument.formula=startcenter.desktop; 10 | application/vnd.oasis.opendocument.formula-template=startcenter.desktop; 11 | application/vnd.oasis.opendocument.graphics=startcenter.desktop; 12 | application/vnd.oasis.opendocument.graphics-template=startcenter.desktop; 13 | application/vnd.oasis.opendocument.image=startcenter.desktop; 14 | application/vnd.oasis.opendocument.image-template=startcenter.desktop; 15 | application/vnd.oasis.opendocument.presentation=startcenter.desktop; 16 | application/vnd.oasis.opendocument.presentation-template=startcenter.desktop; 17 | application/vnd.oasis.opendocument.spreadsheet=startcenter.desktop; 18 | application/vnd.oasis.opendocument.spreadsheet-template=startcenter.desktop; 19 | application/vnd.oasis.opendocument.text=startcenter.desktop; 20 | application/vnd.oasis.opendocument.text-master=startcenter.desktop; 21 | application/vnd.oasis.opendocument.text-template=startcenter.desktop; 22 | application/vnd.oasis.opendocument.text-web=startcenter.desktop; 23 | text/cache-manifest=Helix.desktop;nvim.desktop; 24 | text/calendar=Helix.desktop;nvim.desktop; 25 | text/coffeescript=Helix.desktop;nvim.desktop; 26 | text/css=Helix.desktop;nvim.desktop; 27 | text/csv=Helix.desktop;nvim.desktop; 28 | text/html=Helix.desktop;nvim.desktop; 29 | text/jade=Helix.desktop;nvim.desktop; 30 | text/javascript=Helix.desktop;nvim.desktop; 31 | text/jsx=Helix.desktop;nvim.desktop; 32 | text/less=Helix.desktop;nvim.desktop; 33 | text/markdown=Helix.desktop;nvim.desktop; 34 | text/mathml=Helix.desktop;nvim.desktop; 35 | text/mdx=Helix.desktop;nvim.desktop; 36 | text/n3=Helix.desktop;nvim.desktop; 37 | text/plain=Helix.desktop;nvim.desktop; 38 | text/prs.lines.tag=Helix.desktop;nvim.desktop; 39 | text/richtext=Helix.desktop;nvim.desktop; 40 | text/rtf=Helix.desktop;nvim.desktop; 41 | text/sgml=Helix.desktop;nvim.desktop; 42 | text/shex=Helix.desktop;nvim.desktop; 43 | text/slim=Helix.desktop;nvim.desktop; 44 | text/spdx=Helix.desktop;nvim.desktop; 45 | text/stylus=Helix.desktop;nvim.desktop; 46 | text/tab-separated-values=Helix.desktop;nvim.desktop; 47 | text/troff=Helix.desktop;nvim.desktop; 48 | text/turtle=Helix.desktop;nvim.desktop; 49 | text/uri-list=Helix.desktop;nvim.desktop; 50 | text/vcard=Helix.desktop;nvim.desktop; 51 | text/vnd.curl=Helix.desktop;nvim.desktop; 52 | text/vnd.curl.dcurl=Helix.desktop;nvim.desktop; 53 | text/vnd.curl.mcurl=Helix.desktop;nvim.desktop; 54 | text/vnd.curl.scurl=Helix.desktop;nvim.desktop; 55 | text/vnd.dvb.subtitle=Helix.desktop;nvim.desktop; 56 | text/vnd.familysearch.gedcom=Helix.desktop;nvim.desktop; 57 | text/vnd.fly=Helix.desktop;nvim.desktop; 58 | text/vnd.fmi.flexstor=Helix.desktop;nvim.desktop; 59 | text/vnd.graphviz=Helix.desktop;nvim.desktop; 60 | text/vnd.in3d.3dml=Helix.desktop;nvim.desktop; 61 | text/vnd.in3d.spot=Helix.desktop;nvim.desktop; 62 | text/vnd.sun.j2me.app-descriptor=Helix.desktop;nvim.desktop; 63 | text/vnd.wap.wml=Helix.desktop;nvim.desktop; 64 | text/vnd.wap.wmlscript=Helix.desktop;nvim.desktop; 65 | text/vtt=Helix.desktop;nvim.desktop; 66 | text/wgsl=Helix.desktop;nvim.desktop; 67 | text/x-asm=Helix.desktop;nvim.desktop; 68 | text/x-c=Helix.desktop;nvim.desktop; 69 | text/x-component=Helix.desktop;nvim.desktop; 70 | text/x-fortran=Helix.desktop;nvim.desktop; 71 | text/x-handlebars-template=Helix.desktop;nvim.desktop; 72 | text/x-java-source=Helix.desktop;nvim.desktop; 73 | text/x-lua=Helix.desktop;nvim.desktop; 74 | text/x-markdown=Helix.desktop;nvim.desktop; 75 | text/x-nfo=Helix.desktop;nvim.desktop; 76 | text/x-opml=Helix.desktop;nvim.desktop; 77 | text/x-org=Helix.desktop;nvim.desktop; 78 | text/x-pascal=Helix.desktop;nvim.desktop; 79 | text/x-processing=Helix.desktop;nvim.desktop; 80 | text/x-sass=Helix.desktop;nvim.desktop; 81 | text/x-scss=Helix.desktop;nvim.desktop; 82 | text/x-setext=Helix.desktop;nvim.desktop; 83 | text/x-sfv=Helix.desktop;nvim.desktop; 84 | text/x-suse-ymp=Helix.desktop;nvim.desktop; 85 | text/x-uuencode=Helix.desktop;nvim.desktop; 86 | text/x-vcalendar=Helix.desktop;nvim.desktop; 87 | text/x-vcard=Helix.desktop;nvim.desktop; 88 | text/xml=Helix.desktop;nvim.desktop; 89 | text/yaml=Helix.desktop;nvim.desktop; 90 | video/mp4=mpv.desktop; 91 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__set_and_remove_handlers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Setting `Helix.desktop` as handler for `text/plain` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `Helix.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: Helix.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished setting handler 10 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: Helix.desktop; 11 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 12 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 14 | [TIMESTAMP]  INFO handlr::config::main_config: Setting `nvim.desktop` as handler for `text/plain` 15 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 16 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 17 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: nvim.desktop; 18 | [TIMESTAMP]  INFO handlr::config::main_config: Finished setting handler 19 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: nvim.desktop; 20 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 21 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 22 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 23 | [TIMESTAMP]  INFO handlr::config::main_config: Removing `Helix.desktop` from list of handlers for `text/plain` 24 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: nvim.desktop; 25 | [TIMESTAMP]  INFO handlr::config::main_config: Finished removing handler 26 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: nvim.desktop; 27 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 28 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 29 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 30 | [TIMESTAMP]  INFO handlr::config::main_config: Removing `nvim.desktop` from list of handlers for `text/plain` 31 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: 32 | [TIMESTAMP]  INFO handlr::config::main_config: Finished removing handler 33 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: ; 34 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 0 35 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 36 | [TIMESTAMP]  INFO handlr::config::main_config: No match for `text/plain` in mimeapps.list Default Associations 37 | [TIMESTAMP]  INFO handlr::config::main_config: No matching entries for `text/plain` in mimeapps.list Added Associations 38 | [TIMESTAMP] DEBUG handlr::apps::system: No installed handlers found for `text/plain` 39 | [TIMESTAMP]  INFO handlr::config::main_config: No matching installed handlers found for `text/plain` 40 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__add_and_remove_handlers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding Helix.desktop to list of handlers for `text/plain` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `Helix.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: Helix.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: Helix.desktop; 11 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 12 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 13 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 14 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 15 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 16 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 17 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: Helix.desktop;nvim.desktop; 18 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 19 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: Helix.desktop;nvim.desktop; 20 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 2 21 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 22 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 23 | [TIMESTAMP]  INFO handlr::config::main_config: Removing `Helix.desktop` from list of handlers for `text/plain` 24 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: nvim.desktop; 25 | [TIMESTAMP]  INFO handlr::config::main_config: Finished removing handler 26 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: nvim.desktop; 27 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 28 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 29 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `text/plain` in mimeapps.list Default Associations 30 | [TIMESTAMP]  INFO handlr::config::main_config: Removing `nvim.desktop` from list of handlers for `text/plain` 31 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: 32 | [TIMESTAMP]  INFO handlr::config::main_config: Finished removing handler 33 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `text/plain` in mimeapps.list Default Associations: ; 34 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 0 35 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 36 | [TIMESTAMP]  INFO handlr::config::main_config: No match for `text/plain` in mimeapps.list Default Associations 37 | [TIMESTAMP]  INFO handlr::config::main_config: No matching entries for `text/plain` in mimeapps.list Added Associations 38 | [TIMESTAMP] DEBUG handlr::apps::system: No installed handlers found for `text/plain` 39 | [TIMESTAMP]  INFO handlr::config::main_config: No matching installed handlers found for `text/plain` 40 | -------------------------------------------------------------------------------- /src/apps/system.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | apps::DesktopList, 3 | common::{DesktopEntry, DesktopHandler, Handleable}, 4 | config::Languages, 5 | error::Result, 6 | }; 7 | use mime::Mime; 8 | use std::{collections::BTreeMap, ffi::OsString}; 9 | use tracing::debug; 10 | 11 | #[derive(Debug, Default, Clone)] 12 | pub struct SystemApps { 13 | /// Associations of mimes and lists of apps 14 | pub associations: BTreeMap, 15 | /// Apps with no associated mime 16 | unassociated: DesktopList, 17 | } 18 | 19 | impl SystemApps { 20 | /// Get the list of handlers associated with a given mime 21 | pub fn get_handlers(&self, mime: &Mime) -> Option { 22 | let associations = self.associations.get(mime); 23 | 24 | if associations.is_none() { 25 | debug!("No installed handlers found for `{}`", mime); 26 | } else { 27 | debug!( 28 | "Installed handlers found for `{}`: {}", 29 | mime, associations? 30 | ); 31 | } 32 | 33 | Some(associations?.clone()) 34 | } 35 | 36 | /// Get the primary of handler associated with a given mime 37 | pub fn get_handler(&self, mime: &Mime) -> Option { 38 | let handler = self.get_handlers(mime)?.front()?.clone(); 39 | debug!("Installed handler chosen for `{}`: {}", mime, handler); 40 | Some(handler) 41 | } 42 | 43 | /// Get all system-level desktop entries on the system 44 | #[mutants::skip] // Cannot test directly, depends on system state 45 | pub fn get_entries( 46 | languages: &Languages, 47 | ) -> Result> { 48 | // ) -> Result + use<'_>> { 49 | Ok(xdg::BaseDirectories::new()? 50 | .list_data_files_once("applications") 51 | .into_iter() 52 | .filter(|p| { 53 | p.extension().and_then(|x| x.to_str()) == Some("desktop") 54 | }) 55 | .filter_map(|p| { 56 | Some(( 57 | p.file_name()?.to_owned(), 58 | DesktopEntry::parse_file(&p.clone(), languages).ok()?, 59 | )) 60 | }) 61 | .collect()) 62 | } 63 | 64 | /// Create a new instance of `SystemApps` 65 | #[mutants::skip] // Cannot test directly, depends on system state 66 | pub fn populate(languages: &Languages) -> Result { 67 | let mut associations = BTreeMap::::new(); 68 | let mut unassociated = DesktopList::default(); 69 | 70 | Self::get_entries(languages)? 71 | .into_iter() 72 | .for_each(|(_, entry)| { 73 | let (file_name, mimes) = (entry.file_name, entry.mime_type); 74 | let desktop_handler = 75 | DesktopHandler::assume_valid(file_name.to_owned()); 76 | 77 | if mimes.is_empty() { 78 | unassociated.push_back(desktop_handler); 79 | } else { 80 | mimes.into_iter().for_each(|mime| { 81 | associations 82 | .entry(mime) 83 | .or_default() 84 | .push_back(desktop_handler.clone()); 85 | }); 86 | } 87 | }); 88 | 89 | Ok(Self { 90 | associations, 91 | unassociated, 92 | }) 93 | } 94 | 95 | /// Get an installed terminal emulator 96 | pub fn terminal_emulator( 97 | &self, 98 | languages: &Languages, 99 | ) -> Option { 100 | self.unassociated 101 | .iter() 102 | .filter_map(|h| h.get_entry(languages).ok()) 103 | .find(|h| h.is_terminal_emulator()) 104 | } 105 | 106 | #[cfg(test)] 107 | /// Internal helper function for testing 108 | pub fn add_unassociated(&mut self, handler: DesktopHandler) { 109 | self.unassociated.push_front(handler) 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::*; 116 | 117 | #[test] 118 | fn get_handlers() -> Result<()> { 119 | let mut expected_handlers = DesktopList::default(); 120 | expected_handlers 121 | .push_back(DesktopHandler::assume_valid("helix.desktop".into())); 122 | expected_handlers 123 | .push_back(DesktopHandler::assume_valid("nvim.desktop".into())); 124 | 125 | let mut associations: BTreeMap = BTreeMap::new(); 126 | 127 | associations.insert(mime::TEXT_PLAIN, expected_handlers.clone()); 128 | 129 | let system_apps = SystemApps { 130 | associations, 131 | ..Default::default() 132 | }; 133 | 134 | assert_eq!( 135 | system_apps 136 | .get_handler(&mime::TEXT_PLAIN) 137 | .expect("Could not get handler") 138 | .to_string(), 139 | "helix.desktop" 140 | ); 141 | assert_eq!( 142 | system_apps 143 | .get_handlers(&mime::TEXT_PLAIN) 144 | .expect("Could not get handler"), 145 | expected_handlers 146 | ); 147 | 148 | Ok(()) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/common/path.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{render_table, MimeType}, 3 | error::{Error, Result}, 4 | }; 5 | use itertools::Itertools; 6 | use mime::Mime; 7 | use serde::Serialize; 8 | use std::{ 9 | convert::{TryFrom, TryInto}, 10 | fmt::{Display, Formatter}, 11 | io::Write, 12 | path::PathBuf, 13 | str::FromStr, 14 | }; 15 | use tabled::Tabled; 16 | use tracing::{debug, info}; 17 | use url::Url; 18 | 19 | #[derive(Debug, Clone)] 20 | pub enum UserPath { 21 | Url(Url), 22 | File(PathBuf), 23 | } 24 | 25 | impl UserPath { 26 | pub fn get_mime(&self) -> Result { 27 | Ok(match self { 28 | Self::Url(url) => Ok(url.try_into()?), 29 | Self::File(f) => MimeType::try_from(f.as_path()), 30 | }? 31 | .0) 32 | } 33 | } 34 | 35 | impl FromStr for UserPath { 36 | type Err = Error; 37 | fn from_str(s: &str) -> Result { 38 | let normalized = match url::Url::parse(s) { 39 | Ok(url) if url.scheme() == "file" => { 40 | let path = url 41 | .to_file_path() 42 | .map_err(|_| Error::BadPath(url.path().to_owned()))?; 43 | 44 | Self::File(path) 45 | } 46 | Ok(url) => Self::Url(url), 47 | _ => Self::File(PathBuf::from(s)), 48 | }; 49 | 50 | Ok(normalized) 51 | } 52 | } 53 | 54 | impl Display for UserPath { 55 | fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 56 | match self { 57 | Self::File(f) => fmt.write_str(&f.to_string_lossy()), 58 | Self::Url(u) => fmt.write_str(u.as_ref()), 59 | } 60 | } 61 | } 62 | 63 | /// Internal helper struct for turning a UserPath into tabular data 64 | #[derive(Tabled, Serialize)] 65 | struct UserPathTable { 66 | path: String, 67 | mime: String, 68 | } 69 | 70 | impl UserPathTable { 71 | fn new(path: &UserPath) -> Result { 72 | Ok(Self { 73 | path: path.to_string(), 74 | mime: path.get_mime()?.essence_str().to_owned(), 75 | }) 76 | } 77 | } 78 | 79 | /// Render a table of mime types from a list of paths 80 | /// and write it to the given writer 81 | pub fn mime_table( 82 | writer: &mut W, 83 | paths: &[UserPath], 84 | output_json: bool, 85 | terminal_output: bool, 86 | ) -> Result<()> { 87 | info!( 88 | "Printing mime information for paths: [{}]", 89 | paths 90 | .iter() 91 | .format_with(", ", |str, f| f(&format!("\"{}\"", str))) 92 | .to_string() 93 | ); 94 | debug!("JSON output: {}", output_json); 95 | 96 | let rows = paths 97 | .iter() 98 | .map(UserPathTable::new) 99 | .collect::>>()?; 100 | 101 | let table = if output_json { 102 | serde_json::to_string(&rows)? 103 | } else { 104 | render_table(&rows, terminal_output) 105 | }; 106 | 107 | writeln!(writer, "{table}")?; 108 | 109 | info!("Finished printing mime information"); 110 | Ok(()) 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use super::*; 116 | 117 | // Helper function to create a vector of UserPaths for testing `mime_table` 118 | fn paths() -> Result> { 119 | [ 120 | "tests/assets", 121 | "tests/assets/cat", 122 | "tests/assets/cmus.desktop", 123 | "tests/assets/empty.txt", 124 | "tests/assets/no_html_tags.html", 125 | "tests/assets/org.wezfurlong.wezterm.desktop", 126 | "tests/assets/p.html", 127 | "tests/assets/rust.vim", 128 | "tests/assets/SettingsWidgetFdoSecrets.ui", 129 | "https://duckduckgo.com", 130 | ".", 131 | "README.md", 132 | ] 133 | .iter() 134 | .map(|p| UserPath::from_str(p)) 135 | .collect() 136 | } 137 | 138 | #[test] 139 | fn file_url() -> Result<()> { 140 | let path = UserPath::from_str("file:///test.txt")?; 141 | assert_eq!(path.to_string(), "/test.txt"); 142 | Ok(()) 143 | } 144 | 145 | #[test] 146 | fn mime_table_terminal() -> Result<()> { 147 | let mut buffer = Vec::new(); 148 | mime_table(&mut buffer, &paths()?, false, true)?; 149 | insta::assert_snapshot!(String::from_utf8(buffer)?); 150 | Ok(()) 151 | } 152 | 153 | #[test] 154 | fn test_mime_table_piped() -> Result<()> { 155 | let mut buffer = Vec::new(); 156 | mime_table(&mut buffer, &paths()?, false, false)?; 157 | insta::assert_snapshot!(String::from_utf8(buffer)?); 158 | Ok(()) 159 | } 160 | 161 | #[test] 162 | fn test_mime_table_json() -> Result<()> { 163 | //NOTE: both calls should have the same result 164 | // JSON output and terminal output 165 | let mut buffer = Vec::new(); 166 | mime_table(&mut buffer, &paths()?, true, true)?; 167 | insta::assert_snapshot!(String::from_utf8(buffer)?); 168 | 169 | // JSON output and no terminal output 170 | let mut buffer = Vec::new(); 171 | mime_table(&mut buffer, &paths()?, true, false)?; 172 | insta::assert_snapshot!(String::from_utf8(buffer)?); 173 | 174 | Ok(()) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_piped-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 16 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 17 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 18 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 20 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 21 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 22 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 23 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 24 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 25 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 26 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 27 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 28 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 29 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 30 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 31 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 32 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 33 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 34 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 35 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 36 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 37 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 38 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 39 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 40 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 41 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 42 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 43 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 44 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 45 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 46 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: false 47 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 48 | -------------------------------------------------------------------------------- /src/common/mime_types.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use derive_more::Deref; 3 | use mime::Mime; 4 | use std::{convert::TryFrom, path::Path, str::FromStr, sync::LazyLock}; 5 | use tracing_unwrap::{OptionExt, ResultExt}; 6 | use url::Url; 7 | 8 | /// A mime derived from a path or URL 9 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deref)] 10 | pub struct MimeType(pub Mime); 11 | 12 | impl MimeType { 13 | /// Gets the `Mime` from a given file extension 14 | fn from_ext(ext: &str) -> Result { 15 | match xdg_mime::SharedMimeInfo::new() 16 | .get_mime_types_from_file_name(ext) 17 | .into_iter() 18 | .nth(0) 19 | .expect_or_log("The function xdg_mime::get_mime_types_from_file_type should always return a non-empty Vec") 20 | { 21 | // If the file extension is ambiguous, then error 22 | // Otherwise, the user may not expect this mimetype being assigned 23 | mime if mime == mime::APPLICATION_OCTET_STREAM => { 24 | Err(Error::AmbiguousExtension(ext.into())) 25 | } 26 | mime => Ok(mime) 27 | } 28 | } 29 | } 30 | 31 | impl TryFrom<&Url> for MimeType { 32 | type Error = Error; 33 | fn try_from(url: &Url) -> Result { 34 | Ok(Self( 35 | format!("x-scheme-handler/{}", url.scheme()).parse::()?, 36 | )) 37 | } 38 | } 39 | 40 | impl TryFrom<&Path> for MimeType { 41 | type Error = Error; 42 | fn try_from(path: &Path) -> Result { 43 | if !path.try_exists()? { 44 | return Err(Error::NonexistentFile( 45 | path.to_string_lossy().to_string(), 46 | )); 47 | } 48 | 49 | let db = xdg_mime::SharedMimeInfo::new(); 50 | 51 | let mut guess = db.guess_mime_type(); 52 | guess.file_name(&path.to_string_lossy()); 53 | 54 | let mut mime = guess.guess().mime_type().clone(); 55 | 56 | static APPLICATION_X_ZEROSIZE: LazyLock = LazyLock::new(|| { 57 | "application/x-zerosize" 58 | .parse::() 59 | .expect_or_log("Hardcoded mime should be valid") 60 | }); 61 | 62 | // TODO: remove this check once xdg-mime crate makes a new release (currently v0.4.0) 63 | if mime == *APPLICATION_X_ZEROSIZE { 64 | mime = guess.path(path).guess().mime_type().clone(); 65 | } 66 | 67 | Ok(Self(mime.clone())) 68 | } 69 | } 70 | 71 | impl FromStr for MimeType { 72 | type Err = Error; 73 | fn from_str(s: &str) -> Result { 74 | let mime = if s.starts_with('.') { 75 | Self::from_ext(s)? 76 | } else { 77 | match Mime::from_str(s)? { 78 | m if m.subtype() == "" => return Err(Error::InvalidMime(m)), 79 | proper_mime => proper_mime, 80 | } 81 | }; 82 | 83 | Ok(Self(mime)) 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | 91 | #[test] 92 | fn user_input() -> Result<()> { 93 | assert_eq!(MimeType::from_str(".pdf")?.0, mime::APPLICATION_PDF); 94 | assert_eq!(MimeType::from_str("image/jpeg")?.0, mime::IMAGE_JPEG); 95 | 96 | assert!("image//jpg".parse::().is_err()); 97 | assert!("image".parse::().is_err()); 98 | 99 | Ok(()) 100 | } 101 | 102 | #[test] 103 | fn from_path() -> Result<()> { 104 | assert_eq!( 105 | MimeType::try_from(Path::new("."))?.0.essence_str(), 106 | "inode/directory" 107 | ); 108 | assert_eq!( 109 | MimeType::try_from(Path::new("./tests/assets/rust.vim"))?.0, 110 | "text/plain" 111 | ); 112 | assert_eq!( 113 | MimeType::try_from(Path::new("./tests/assets/cat"))?.0, 114 | "application/x-shellscript" 115 | ); 116 | assert_eq!( 117 | MimeType::try_from(Path::new( 118 | "./tests/assets/SettingsWidgetFdoSecrets.ui" 119 | ))? 120 | .0, 121 | "application/x-designer" 122 | ); 123 | assert_eq!( 124 | MimeType::try_from(Path::new("./tests/assets/empty.txt"))?.0, 125 | "text/plain" 126 | ); 127 | assert_eq!( 128 | MimeType::try_from(Path::new("./tests/assets/p.html"))?.0, 129 | "text/html" 130 | ); 131 | assert_eq!( 132 | MimeType::try_from(Path::new("./tests/assets/no_html_tags.html"))? 133 | .0, 134 | "text/html" 135 | ); 136 | assert_eq!( 137 | MimeType::try_from(Path::new("./tests/assets/empty"))?.0, 138 | "application/x-zerosize" 139 | ); 140 | assert_eq!( 141 | MimeType::try_from(Path::new( 142 | "./tests/assets/nonsense_binary_data" 143 | ))? 144 | .0, 145 | "application/octet-stream" 146 | ); 147 | 148 | Ok(()) 149 | } 150 | 151 | #[test] 152 | fn from_str() -> Result<()> { 153 | assert_eq!(".mp3".parse::()?.0, "audio/mpeg"); 154 | assert_eq!("audio/mpeg".parse::()?.0, "audio/mpeg"); 155 | assert!(".".parse::().is_err()); 156 | assert!("audio/".parse::().is_err()); 157 | assert_eq!( 158 | "application/octet-stream".parse::()?.0, 159 | "application/octet-stream" 160 | ); 161 | 162 | Ok(()) 163 | } 164 | 165 | #[test] 166 | fn nonexistent_file() { 167 | assert!(MimeType::try_from(Path::new("nonexistent_file")).is_err()); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_default-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 16 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 17 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 18 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 20 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 21 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 22 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 23 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 24 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 25 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 26 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 27 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 28 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 29 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 30 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 31 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 32 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 33 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 34 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 35 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 36 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 37 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 38 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 39 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 40 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 41 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 42 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 43 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 44 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 45 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 46 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: false 47 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 48 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_detailed-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 16 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 17 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 18 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 20 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 21 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 22 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 23 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 24 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 25 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 26 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 27 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 28 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 29 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 30 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 31 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 32 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 33 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 34 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 35 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 36 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 37 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 38 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 39 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 40 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 41 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 42 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 43 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 44 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 45 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 46 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: false 47 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 48 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_detailed_piped-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 16 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 17 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 18 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 20 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 21 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 22 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 23 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 24 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 25 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 26 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 27 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 28 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 29 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 30 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 31 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 32 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 33 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 34 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 35 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 36 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 37 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 38 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 39 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 40 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 41 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 42 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 43 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 44 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 45 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 46 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: false 47 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 48 | -------------------------------------------------------------------------------- /tests/assets/SettingsWidgetFdoSecrets.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsWidgetFdoSecrets 4 | 5 | 6 | 7 | 0 8 | 0 9 | 525 10 | 457 11 | 12 | 13 | 14 | Options 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Enable KeepassXC Freedesktop.org Secret Service integration 36 | 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 44 | 45 | 46 | General 47 | 48 | 49 | 50 | 51 | 52 | <html><head/><body><p>If enabled, clients can delete exposed entries without confirmation. Otherwise, attempts to delete an entry may need to be confirmed interactively (subject to global Security/Convenience settings).</p></body></html> 53 | 54 | 55 | Skip confirmation when entries are deleted by clients 56 | 57 | 58 | 59 | 60 | 61 | 62 | Show notification when passwords are retrieved by clients 63 | 64 | 65 | 66 | 67 | 68 | 69 | <html><head/><body><p>If enabled, any attempt to read a password must be confirmed. Otherwise, clients can read passwords without confirmation when the database is unlocked.</p><p>This option only covers the access to the password of an entry. Clients can always enumerate the items of exposed databases and query their attributes.</p></body></html> 70 | 71 | 72 | Confirm when passwords are retrieved by clients 73 | 74 | 75 | 76 | 77 | 78 | 79 | Qt::Vertical 80 | 81 | 82 | QSizePolicy::Preferred 83 | 84 | 85 | 86 | 20 87 | 40 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | Exposed database groups: 96 | 97 | 98 | 99 | 100 | 101 | 102 | Qt::NoFocus 103 | 104 | 105 | QAbstractItemView::NoEditTriggers 106 | 107 | 108 | QAbstractItemView::NoSelection 109 | 110 | 111 | QAbstractItemView::SelectRows 112 | 113 | 114 | false 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | Authorization 123 | 124 | 125 | 126 | 127 | 128 | These applications are currently connected: 129 | 130 | 131 | 132 | 133 | 134 | 135 | Qt::NoFocus 136 | 137 | 138 | QAbstractItemView::NoEditTriggers 139 | 140 | 141 | QAbstractItemView::NoSelection 142 | 143 | 144 | QAbstractItemView::SelectRows 145 | 146 | 147 | false 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | MessageWidget 160 | QWidget 161 |
gui/MessageWidget.h
162 | 1 163 |
164 |
165 | 166 | 167 |
168 | -------------------------------------------------------------------------------- /src/common/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{DesktopEntry, ExecMode, UserPath}, 3 | config::{Config, Languages}, 4 | error::{Error, Result}, 5 | }; 6 | use derive_more::{Deref, Display}; 7 | use enum_dispatch::enum_dispatch; 8 | use itertools::Itertools; 9 | use serde::{Deserialize, Serialize}; 10 | use serde_with::{serde_as, DisplayFromStr}; 11 | use std::{ 12 | ffi::OsString, 13 | fmt::Display, 14 | hash::{Hash, Hasher}, 15 | path::PathBuf, 16 | str::FromStr, 17 | }; 18 | use tracing::{debug, info, warn}; 19 | 20 | /// Represents a program or command that is used to open a file 21 | #[enum_dispatch(Handleable)] 22 | #[derive(Display, Debug, PartialEq, Eq, Hash)] 23 | pub enum Handler { 24 | DesktopHandler, 25 | RegexHandler, 26 | } 27 | 28 | #[cfg(test)] 29 | impl Handler { 30 | /// Helper function for testing 31 | pub fn new(name: &str) -> Self { 32 | Handler::DesktopHandler(DesktopHandler::assume_valid(name.into())) 33 | } 34 | } 35 | 36 | /// Trait providing common functionality for handlers 37 | #[enum_dispatch] 38 | pub trait Handleable { 39 | /// Get the desktop entry associated with the handler 40 | fn get_entry(&self, languages: &Languages) -> Result; 41 | /// Open the given paths with the handler 42 | #[mutants::skip] // Cannot test directly, runs commands 43 | fn open(&self, config: &Config, args: Vec) -> Result<()> { 44 | self.get_entry(&config.languages)? 45 | .exec(config, ExecMode::Open, args) 46 | } 47 | } 48 | 49 | /// Represents a handler defined in a desktop file 50 | #[derive( 51 | Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, 52 | )] 53 | pub struct DesktopHandler(OsString); 54 | 55 | impl Display for DesktopHandler { 56 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 57 | f.write_str(&self.0.to_string_lossy()) 58 | } 59 | } 60 | 61 | impl FromStr for DesktopHandler { 62 | type Err = Error; 63 | fn from_str(s: &str) -> Result { 64 | Ok(DesktopHandler(s.into())) 65 | } 66 | } 67 | 68 | impl Handleable for DesktopHandler { 69 | fn get_entry(&self, languages: &Languages) -> Result { 70 | DesktopEntry::parse_file(&Self::get_path(&self.0)?, languages) 71 | } 72 | } 73 | 74 | impl DesktopHandler { 75 | /// Create a DesktopHandler, skipping validity checks 76 | pub fn assume_valid(name: OsString) -> Self { 77 | Self(name) 78 | } 79 | 80 | /// Get the path of a given desktop entry file 81 | pub fn get_path(name: &std::ffi::OsStr) -> Result { 82 | if cfg!(test) { 83 | Ok(PathBuf::from(name)) 84 | } else { 85 | let mut path = PathBuf::from("applications"); 86 | path.push(name); 87 | Ok(xdg::BaseDirectories::new()? 88 | .find_data_file(path) 89 | .ok_or_else(|| { 90 | Error::NotFound(name.to_string_lossy().into()) 91 | })?) 92 | } 93 | } 94 | 95 | /// Launch a DesktopHandler's desktop entry 96 | #[mutants::skip] // Cannot test directly, runs command 97 | pub fn launch(&self, config: &Config, args: Vec) -> Result<()> { 98 | info!("Launching `{}` with args: {:?}", self, args); 99 | self.get_entry(&config.languages)? 100 | .exec(config, ExecMode::Launch, args) 101 | } 102 | 103 | /// Issue a warning if the given handler is invalid 104 | pub fn warn_if_invalid(&self) { 105 | // This is just checking that the handler is valid 106 | // so we can assume that access to the language settings is unecessary 107 | if let Err(e) = self.get_entry(&Vec::new()) { 108 | warn!("The desktop entry `{}` is invalid: {}", self, e); 109 | } 110 | } 111 | } 112 | 113 | /// Represents a regex handler from the config 114 | #[derive(Display, Debug, Clone, PartialEq, Eq, Hash, Deserialize)] 115 | #[display(fmt = "\"{}\" (Regex Handler)", exec)] 116 | pub struct RegexHandler { 117 | exec: String, 118 | #[serde(default)] 119 | terminal: bool, 120 | regexes: Vec, 121 | } 122 | 123 | impl RegexHandler { 124 | /// Test if a given path matches the handler's regex 125 | fn is_match(&self, path: &str) -> bool { 126 | let matches = self 127 | .regexes 128 | .iter() 129 | .filter(|regex| regex.is_match(path)) 130 | .collect_vec(); 131 | 132 | let is_match = !matches.is_empty(); 133 | 134 | if is_match { 135 | debug!( 136 | "Regex matches found in `{}` for `{}`: {:?}", 137 | self, path, matches 138 | ); 139 | } else { 140 | debug!("No regex matches found in `{}` for `{}`", self, path); 141 | } 142 | 143 | is_match 144 | } 145 | } 146 | 147 | impl Handleable for RegexHandler { 148 | fn get_entry(&self, _languages: &Languages) -> Result { 149 | Ok(DesktopEntry::fake_entry(&self.exec, self.terminal)) 150 | } 151 | } 152 | 153 | /// Wrapper struct needed because `Regex` does not implement Eq, Hash, or Deserialize 154 | #[serde_as] 155 | #[derive(Debug, Clone, Deref, Deserialize)] 156 | struct Regex(#[serde_as(as = "DisplayFromStr")] lazy_regex::Regex); 157 | 158 | impl PartialEq for Regex { 159 | #[mutants::skip] // Trivial 160 | fn eq(&self, other: &Self) -> bool { 161 | self.as_str() == other.as_str() 162 | } 163 | } 164 | 165 | impl Eq for Regex {} 166 | 167 | impl Hash for Regex { 168 | #[mutants::skip] // Trivial 169 | fn hash(&self, state: &mut H) { 170 | self.as_str().hash(state); 171 | } 172 | } 173 | 174 | /// A collection of all of the defined RegexHandlers 175 | #[derive(Debug, Clone, Default, Deserialize)] 176 | pub struct RegexApps(Vec); 177 | 178 | impl RegexApps { 179 | /// Get a handler matching a given path 180 | pub fn get_handler(&self, path: &UserPath) -> Result { 181 | Ok(self 182 | .0 183 | .iter() 184 | .find(|app| app.is_match(&path.to_string())) 185 | .ok_or_else(|| Error::NotFound(path.to_string()))? 186 | .clone()) 187 | } 188 | } 189 | 190 | #[cfg(test)] 191 | mod tests { 192 | use super::*; 193 | use crate::common::DesktopEntry; 194 | use url::Url; 195 | 196 | crate::logs_snapshot_test!(regex_handlers, { 197 | let exec: &str = "freetube %u"; 198 | let regex_handler = RegexHandler { 199 | exec: String::from(exec), 200 | terminal: false, 201 | regexes: [String::from( 202 | r"(https://)?(www\.)?youtu(be\.com|\.be)/*", 203 | )] 204 | .iter() 205 | .map(|s| { 206 | Regex( 207 | lazy_regex::Regex::new(s) 208 | .expect("Hardcoded regex should be valid"), 209 | ) 210 | }) 211 | .collect(), 212 | }; 213 | 214 | println!("{:#?}", regex_handler); 215 | 216 | let regex_apps = RegexApps(vec![regex_handler.clone()]); 217 | 218 | assert_eq!( 219 | regex_apps 220 | .get_handler(&UserPath::Url(Url::parse( 221 | "https://youtu.be/dQw4w9WgXcQ" 222 | )?))? 223 | .get_entry(&Vec::new())?, 224 | DesktopEntry { 225 | exec: exec.to_string(), 226 | terminal: false, 227 | ..Default::default() 228 | } 229 | ); 230 | 231 | assert!(regex_apps 232 | .get_handler(&UserPath::Url(Url::parse( 233 | "https://en.wikipedia.org", 234 | )?)) 235 | .is_err()); 236 | }); 237 | } 238 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__properly_assign_files_to_handlers.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding swayimg.desktop to list of handlers for `image/png` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `swayimg.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `image/png`: swayimg.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mupdf.desktop to list of handlers for `application/pdf` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mupdf.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/pdf`: mupdf.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.png` 16 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `image/png` in mimeapps.list Default Associations: swayimg.desktop; 17 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 18 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 19 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `image/png` in mimeapps.list Default Associations 20 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.pdf` 21 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `application/pdf` in mimeapps.list Default Associations: mupdf.desktop; 22 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 23 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 24 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `application/pdf` in mimeapps.list Default Associations 25 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.pdf` 26 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `application/pdf` in mimeapps.list Default Associations: mupdf.desktop; 27 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 28 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 29 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `application/pdf` in mimeapps.list Default Associations 30 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.png` 31 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `image/png` in mimeapps.list Default Associations: swayimg.desktop; 32 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 33 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 34 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `image/png` in mimeapps.list Default Associations 35 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.png` 36 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `image/png` in mimeapps.list Default Associations: swayimg.desktop; 37 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 38 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 39 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `image/png` in mimeapps.list Default Associations 40 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/b.png` 41 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `image/png` in mimeapps.list Default Associations: swayimg.desktop; 42 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 43 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 44 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `image/png` in mimeapps.list Default Associations 45 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.pdf` 46 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `application/pdf` in mimeapps.list Default Associations: mupdf.desktop; 47 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 48 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 49 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `application/pdf` in mimeapps.list Default Associations 50 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.pdf` 51 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `application/pdf` in mimeapps.list Default Associations: mupdf.desktop; 52 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 53 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 54 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `application/pdf` in mimeapps.list Default Associations 55 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/a.png` 56 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `image/png` in mimeapps.list Default Associations: swayimg.desktop; 57 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 58 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 59 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `image/png` in mimeapps.list Default Associations 60 | [TIMESTAMP]  INFO handlr::config::main_config: No matching regex handlers found for `tests/assets/b.png` 61 | [TIMESTAMP] DEBUG handlr::apps::user: Configured handlers for `image/png` in mimeapps.list Default Associations: swayimg.desktop; 62 | [TIMESTAMP] DEBUG handlr::apps::user: Selector enabled: false, number of set handlers: 1 63 | [TIMESTAMP]  INFO handlr::apps::user: Not running selector, choosing first handler 64 | [TIMESTAMP]  INFO handlr::config::main_config: Match found for `image/png` in mimeapps.list Default Associations 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |


handlr-regex

2 | 3 | Manage your default applications with ease using `handlr`! Now with the power of [regular expressions](#setting-regex-handlers)! 4 | 5 | Fork of the original [handlr](https://github.com/chmln/handlr) 6 | 7 | ## Features 8 | 9 | - Set default handler by extension or mime-type 10 | - Set arbitrary commands as handlers based on regular expressions 11 | - Intelligent mime type detection from files based on extension and content 12 | - Open multiple files at once 13 | - Set multiple handlers for mime/extension and use `rofi`/`dmenu` to pick one 14 | - Optional wildcard support like `text/*` 15 | - Automatically removes invalid/wrong `.desktop` entries from `mimeapps.list` 16 | - Helper commands like `launch`, `get --json`, `mime --json` for your scripting needs 17 | - Unnecessarily fast (written in Rust) 18 | - Single compiled binary with no dependencies 19 | 20 | ## Usage 21 | 22 | ```sh 23 | # Open a file/URL 24 | handlr open ~/.dotfiles/pacman/packages.txt 25 | handlr open https://google.ca 26 | 27 | # Set default handler for png files 28 | handlr set .png feh.desktop 29 | 30 | # Set wildcard handler for all text files 31 | handlr set 'text/*' nvim.desktop 32 | # Set wildcard handler for all OpenDocument formats 33 | # NOTE: startcenter.desktop is usually LibreOffice's main desktop entry 34 | handlr set 'application/vnd.oasis.opendocument.*' startcenter.desktop 35 | 36 | # Set default handler based on mime 37 | handlr set application/pdf evince.desktop 38 | 39 | # List default apps 40 | handlr list 41 | 42 | # Get the handler for a mime/extension 43 | $ handlr get .png 44 | feh.desktop 45 | 46 | # Launch a handler with given path/URL 47 | handlr launch x-scheme-handler/https -- https://google.ca 48 | 49 | # Get the mimetypes of given paths/URLs 50 | handlr mime https://duckduckgo.com . README.md 51 | ``` 52 | 53 | ## Compared to `xdg-utils` 54 | 55 | - Can open multiple files/URLs at once 56 | - Can have multiple handlers and use rofi/dmenu to pick one at runtime 57 | - Far easier to use with simple commands like `get`, `set`, `list` 58 | - Can operate on extensions, **no need to look up or remember mime types** 59 | - useful for common tasks like setting a handler for png/docx/etc files 60 | - Superb autocomplete (currently fish, zsh and bash), including mimes, extensions, and `.desktop` files 61 | - Optional json output for scripting 62 | - Properly supports `Terminal=true` entries 63 | 64 | ## Setting default terminal 65 | 66 | Unfortunately, there isn't an XDG spec and thus a standardized way for `handlr` to get your default terminal emulator to run `Terminal=true` desktop entries. There was a proposal floating around a few years ago to use `x-scheme-handler/terminal` for this purpose. It seems to me the least worst option, compared to handling quirks of N+1 distros or using a handlr-specific config option. 67 | 68 | Now if `x-scheme-handler/terminal` is present, `handlr` will use it. 69 | 70 | Otherwise, `handlr` will find an app with `TerminalEmulator` category and use it instead. 71 | 72 | On the upside, `Terminal=true` entries will now work outside of interactive terminals, unlike `xdg-utils`. 73 | 74 | > [!NOTE] 75 | > For `handlr-regex` v0.11.2 and older, when `x-scheme-handler/terminal` is not present, this process is used instead: 76 | > 77 | > 1. Find an app with `TerminalEmulator` category 78 | > 2. Set it as the default for `x-scheme-handler/terminal` 79 | > 3. Send you a notification to let you know it guessed your terminal and provide instructions to change it if necessary 80 | > 81 | > This was changed in order to match how other mimetypes are handled and also so that `mimeapps.list` is not edited without the user's permission. 82 | 83 | 84 | ### Terminal emulator compatibility 85 | `handlr` should work with pretty much any terminal emulator. 86 | 87 | However, some slight configuration may be necessary depending on what command line arguments your terminal emulator supports or required to directly run a command in it. 88 | 89 | If it uses/supports `-e` (i.e. `xterm`, `xfce4-terminal`, `foot`, etc.) then you do not need to do anything. 90 | 91 | If it requires something else, then set `term_exec_args` in `~/.config/handlr/handlr.toml` to the necessary arguments like so: 92 | 93 | ``` 94 | // Replace 'run' with whatever arguments you need 95 | term_exec_args = 'run' 96 | ``` 97 | 98 | If it does not require any arguments or if its arguments are already included in its .desktop file, but it does not use `-e`, (i.e. `wezterm`, `kitty`, etc.) set `term_exec_args` to `''`. 99 | 100 | Feel free to open an issue or pull request if there's a better way to handle this. 101 | 102 | ## Setting multiple handlers 103 | 104 | 1) Open `~/.config/handlr/handlr.toml` and set `enable_selector = true`. Optionally, you can also tweak the `selector` to your selector command (using e.g. rofi or dmenu). 105 | 106 | 2) Add a second/third/whatever handler using `handlr add`, for example 107 | ``` 108 | handlr add x-scheme-handler/https firefox-developer-edition.desktop 109 | ``` 110 | 111 | 3) Now in this example when you open a URL, you will be prompted to select the desired application. 112 | 113 | ![](https://user-images.githubusercontent.com/11352152/85187445-c4bb2580-b26d-11ea-80a6-679e494ab062.png) 114 | 115 | ## Setting regex handlers 116 | 117 | Inspired by a similar feature in [mimeo](https://xyne.dev/projects/mimeo/) 118 | 119 | Open `~/.config/handlr/handlr.toml` and add something like this: 120 | ``` 121 | [[handlers]] 122 | exec = "freetube %u" # Uses desktop entry field codes 123 | terminal = false # Set to true for terminal apps, false for GUI apps (optional; defaults to false) 124 | regexes = ['(https://)?(www\.)?youtu(be\.com|\.be)/*.'] # Use single-quote literal strings 125 | ``` 126 | 127 | For more information: 128 | * [desktop entry field codes](https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#exec-variables) 129 | * [regex reference](https://docs.rs/regex/latest/regex/#syntax) 130 | 131 | ## Smart table output 132 | 133 | Starting with v0.10.0, commands with table output (i.e. `handlr list` and `handlr mime`) switch to outputting tab-separated values when piped for use with commands like `cut`. 134 | 135 | ## Optional wildcards 136 | 137 | When `expand_wildcards` is set to `true` in `~/.config/handlr/handlr.toml`, rather than wildcard mimes being saved directly to `mimeapps.list`, they will be expanded into all matching mimetypes. 138 | 139 | This is off by default and will not automatically expand wildcards already present in `mimeapps.list` when enabled. Simply use `handlr remove` and `handlr add` to manually expand them. 140 | 141 | In addition, regardless of settings, literal wildcards are preferred when using `handlr remove` and `handlr unset`. (e.g. When using `handlr remove text/*`, if `text/*` is present, it will be removed, but `text/plain`, etc. will not be.) 142 | 143 | ## Completion scripts 144 | 145 | To generate a shell completion script, run `COMPLETE= handlr`, where `` is the name of the target shell (e.g. bash, zsh, fish, elvish, powershell, etc.). Note that this will only print it to stdout rather than creating a file or installing the script automatically. 146 | 147 | If you usually install `handlr-regex` from your distribution's repository, and you are not involved with packaging it, you probably do not need to worry about this. 148 | 149 | See [`clap_complete`'s documentation](https://docs.rs/clap_complete/latest/clap_complete/aot/enum.Shell.html) for the list of currently supported shells. 150 | 151 | > [!NOTE] 152 | > This currently relies on unstable features of the `clap_complete` crate and may potentially change in the future. 153 | 154 | ## Logging 155 | 156 | Since v0.13.0, `handlr-regex` supports logs to help with troubleshooting. 157 | 158 | Logs are output in three ways: 159 | - stderr 160 | - notifications (only when neither running in an interactive terminal nor with `--disable-notifications` active) 161 | - log file at `$XDG_CACHE_HOME/handlr/handlr.log` 162 | 163 | By default, for stderr and notifications, only warning and error logs are shown. This can be configured with `--verbose` and `--quiet` as well as with `$RUST_LOG`. All logs are printed to the log file regardless. 164 | 165 | For more information on the syntax for `$RUST_LOG`, see [the documentation for `tracing_subscriber::Envfilter`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives) 166 | 167 | ## Screenshots 168 | 169 |
170 | 171 | 172 | 173 |
174 | 175 | ## Installation 176 | 177 | ### Arch Linux 178 | 179 | ```sh 180 | sudo pacman -S handlr-regex 181 | ``` 182 | 183 | Optionally, you can also use `handlr-regex` as a replacement for `xdg-open` by shadowing it with a script in `$HOME/.local/bin/` (or any other user-scoped `$PATH` directory). Use the following script as an example: 184 | 185 | ```sh 186 | #!/bin/sh 187 | 188 | handlr open "$@" 189 | ``` 190 | 191 | ### Rust/Cargo 192 | 193 | ```sh 194 | cargo install handlr-regex 195 | ``` 196 | 197 | ### Binaries 198 | 199 | 1. Download the latest [release binary](https://github.com/Anomalocaridid/handlr/releases) and put it somewhere in `$PATH` 200 | 2. Download completions for fish: 201 | ```sh 202 | curl https://raw.githubusercontent.com/Anomalocaridid/handlr/master/completions/handlr.fish --create-dirs -o ~/.config/fish/completions/handlr.fish 203 | ``` 204 | 205 | ## Attribution 206 | Icons made by Eucalyp from www.flaticon.com 207 | 208 | Cover photo by [creativebloq.com](https://creativebloq.com) 209 | -------------------------------------------------------------------------------- /src/common/desktop_entry.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{Config, Languages}, 3 | error::{Error, Result}, 4 | }; 5 | use freedesktop_entry_parser::Entry; 6 | use itertools::Itertools; 7 | use mime::Mime; 8 | use std::{ffi::OsString, path::Path, process::Stdio, str::FromStr}; 9 | use tracing::debug; 10 | 11 | /// Represents a desktop entry file for an application 12 | #[derive(Debug, Clone, Default, PartialEq, Eq)] 13 | pub struct DesktopEntry { 14 | /// Name of the application 15 | pub name: String, 16 | /// Command to execute 17 | pub exec: String, 18 | /// Name of the desktop entry file 19 | pub file_name: OsString, 20 | /// Whether the program runs in a terminal window 21 | pub terminal: bool, 22 | /// The MIME type(s) supported by this application 23 | pub mime_type: Vec, 24 | /// Categories in which the entry should be shown in a menu 25 | pub categories: Vec, 26 | } 27 | 28 | /// Modes for running a DesktopFile's `exec` command 29 | #[derive(PartialEq, Eq, Copy, Clone)] 30 | pub enum Mode { 31 | /// Launch the command directly, passing arguments given to `handlr` 32 | Launch, 33 | /// Open files/urls passed to `handler` with the command 34 | Open, 35 | } 36 | 37 | impl DesktopEntry { 38 | /// Execute the command in `exec` in the given mode and with the given arguments 39 | #[mutants::skip] // Cannot test directly, runs external command 40 | pub fn exec( 41 | &self, 42 | config: &Config, 43 | mode: Mode, 44 | arguments: Vec, 45 | ) -> Result<()> { 46 | let supports_multiple = 47 | self.exec.contains("%F") || self.exec.contains("%U"); 48 | if arguments.is_empty() { 49 | self.exec_inner(config, vec![])? 50 | } else if supports_multiple || mode == Mode::Launch { 51 | self.exec_inner(config, arguments)?; 52 | } else { 53 | for arg in arguments { 54 | self.exec_inner(config, vec![arg])?; 55 | } 56 | }; 57 | 58 | Ok(()) 59 | } 60 | 61 | /// Internal helper function for `exec` 62 | #[mutants::skip] // Cannot test directly, runs command 63 | fn exec_inner(&self, config: &Config, args: Vec) -> Result<()> { 64 | let cmd = self.get_cmd(config, args)?; 65 | debug!("Executing command: \"{}\"", cmd); 66 | 67 | let mut cmd = execute::command(cmd); 68 | 69 | if self.terminal && config.terminal_output { 70 | cmd.spawn()?.wait()?; 71 | } else { 72 | cmd.stdout(Stdio::null()).stderr(Stdio::null()).spawn()?; 73 | } 74 | 75 | Ok(()) 76 | } 77 | 78 | /// Get the `exec` command, formatted with given arguments 79 | pub fn get_cmd( 80 | &self, 81 | config: &Config, 82 | args: Vec, 83 | ) -> Result { 84 | let special = lazy_regex::regex!("%(f|u)"i); 85 | 86 | let mut exec = self.exec.clone(); 87 | let args = args.join(" "); 88 | 89 | if special.is_match(&exec) { 90 | exec = special.replace_all(&exec, args).to_string(); 91 | } else { 92 | // The desktop entry doesn't contain arguments - we make best effort and append them at the end 93 | exec.push(' '); 94 | exec.push_str(&args); 95 | } 96 | 97 | // If the entry expects a terminal (emulator), but this process is not running in one, we launch a new one. 98 | if self.terminal && !config.terminal_output { 99 | let mut term_cmd = config.terminal()?; 100 | term_cmd.push(' '); 101 | term_cmd.push_str(&exec); 102 | exec = term_cmd; 103 | } 104 | 105 | Ok(exec.trim().to_string()) 106 | } 107 | 108 | /// Parse a desktop entry file, given a path 109 | pub fn parse_file( 110 | path: &Path, 111 | languages: &Languages, 112 | ) -> Result { 113 | let fd_entry = Entry::parse_file(path)?; 114 | let fd_entry = fd_entry.section("Desktop Entry"); 115 | 116 | let entry_error = |field_name: &str| -> Error { 117 | Error::BadEntry(path.to_path_buf(), field_name.to_string()) 118 | }; 119 | 120 | let entry = DesktopEntry { 121 | name: languages 122 | .iter() 123 | .filter_map(|lang| fd_entry.attr_with_param("Name", lang)) 124 | .next() 125 | .or_else(|| fd_entry.attr("Name")) 126 | .ok_or(entry_error("Name"))? 127 | .to_string(), 128 | exec: fd_entry 129 | .attr("Exec") 130 | .ok_or(entry_error("Exec"))? 131 | .to_string(), 132 | file_name: path.file_name().unwrap_or_default().to_owned(), 133 | terminal: fd_entry 134 | .attr("Terminal") 135 | .and_then(|t| t.parse().ok()) 136 | .unwrap_or(false), 137 | mime_type: fd_entry 138 | .attr("MimeType") 139 | .map(|m| { 140 | m.split(';') 141 | .filter(|s| !s.is_empty()) // Account for ending/duplicated semicolons 142 | .unique() // Remove duplicate entries 143 | .filter_map(|m| Mime::from_str(m).ok()) 144 | .collect_vec() 145 | }) 146 | .unwrap_or_default(), 147 | categories: fd_entry 148 | .attr("Categories") 149 | .map(|c| { 150 | c.split(';') 151 | .filter(|s| !s.is_empty()) // Account for ending/duplicated semicolons 152 | .unique() // Remove duplicate entries 153 | .map(|c| c.to_string()) 154 | .collect_vec() 155 | }) 156 | .unwrap_or_default(), 157 | }; 158 | 159 | if entry.name.is_empty() { 160 | Err(entry_error("Name")) 161 | } else if entry.exec.is_empty() { 162 | Err(entry_error("Exec")) 163 | } else { 164 | Ok(entry) 165 | } 166 | } 167 | 168 | /// Make a fake DesktopEntry given only a value for exec and terminal. 169 | /// All other keys will have default values. 170 | pub fn fake_entry(exec: &str, terminal: bool) -> DesktopEntry { 171 | DesktopEntry { 172 | exec: exec.to_owned(), 173 | terminal, 174 | ..Default::default() 175 | } 176 | } 177 | 178 | /// Check if the given desktop entry represents a terminal emulator 179 | pub fn is_terminal_emulator(&self) -> bool { 180 | self.categories.contains(&"TerminalEmulator".to_string()) 181 | } 182 | } 183 | 184 | #[cfg(test)] 185 | mod tests { 186 | use std::path::PathBuf; 187 | 188 | use super::*; 189 | use crate::common::DesktopHandler; 190 | use similar_asserts::assert_eq; 191 | 192 | // Helper function to test getting the command from the Exec field 193 | fn test_get_cmd( 194 | entry: &DesktopEntry, 195 | config: &Config, 196 | expected_command: &str, 197 | ) -> Result<()> { 198 | assert_eq!( 199 | entry.get_cmd(config, vec!["test".to_string()])?, 200 | expected_command 201 | ); 202 | Ok(()) 203 | } 204 | 205 | #[test] 206 | fn complex_exec() -> Result<()> { 207 | // Note that this entry also has no category key 208 | let entry = DesktopEntry::parse_file( 209 | &PathBuf::from("tests/assets/cmus.desktop"), 210 | &Vec::new(), 211 | )?; 212 | assert_eq!(entry.mime_type.len(), 2); 213 | assert_eq!(entry.mime_type[0].essence_str(), "audio/mp3"); 214 | assert_eq!(entry.mime_type[1].essence_str(), "audio/ogg"); 215 | assert!(!entry.is_terminal_emulator()); 216 | 217 | test_get_cmd( 218 | &entry, 219 | &Config::default(), 220 | "bash -c \"(! pgrep cmus && tilix -e cmus && tilix -a session-add-down -e cava); sleep 0.1 && cmus-remote -q test\"" 221 | ) 222 | } 223 | 224 | #[test] 225 | fn terminal_emulator() -> Result<()> { 226 | let entry = DesktopEntry::parse_file( 227 | &PathBuf::from("tests/assets/org.wezfurlong.wezterm.desktop"), 228 | &Vec::new(), 229 | )?; 230 | assert!(entry.mime_type.is_empty()); 231 | assert!(entry.is_terminal_emulator()); 232 | 233 | test_get_cmd(&entry, &Config::default(), "wezterm start --cwd . test") 234 | } 235 | 236 | #[test] 237 | fn invalid_desktop_entries() -> Result<()> { 238 | let languages = Vec::new(); 239 | 240 | let empty_name = DesktopEntry::parse_file( 241 | &PathBuf::from("tests/assets/empty_name.desktop"), 242 | &languages, 243 | ); 244 | 245 | assert!(empty_name.is_err()); 246 | 247 | let empty_exec = DesktopEntry::parse_file( 248 | &PathBuf::from("tests/assets/empty_exec.desktop"), 249 | &languages, 250 | ); 251 | 252 | assert!(empty_exec.is_err()); 253 | 254 | Ok(()) 255 | } 256 | 257 | #[test] 258 | fn terminal_application_command() -> Result<()> { 259 | let mut config = Config::default(); 260 | 261 | config.terminal_output = false; 262 | 263 | config.add_handler( 264 | &Mime::from_str("x-scheme-handler/terminal")?, 265 | &DesktopHandler::assume_valid( 266 | "tests/assets/org.wezfurlong.wezterm.desktop".into(), 267 | ), 268 | )?; 269 | 270 | let entry = DesktopEntry::parse_file( 271 | &PathBuf::from("tests/assets/Helix.desktop"), 272 | &Vec::new(), 273 | )?; 274 | 275 | test_get_cmd(&entry, &config, "wezterm start --cwd . -e hx test") 276 | } 277 | 278 | /// Helper function for testing language support 279 | fn lang_test(languages: &[&str], expected_name: &str) -> Result<()> { 280 | let entry = DesktopEntry::parse_file( 281 | &PathBuf::from("tests/assets/vlc.desktop"), 282 | &languages.iter().map(|s| s.to_string()).collect_vec(), 283 | )?; 284 | 285 | assert_eq!(entry.name, expected_name); 286 | 287 | Ok(()) 288 | } 289 | 290 | #[test] 291 | fn language_support() -> Result<()> { 292 | // No languages 293 | lang_test(&[], "VLC media player")?; 294 | 295 | // Just one language 296 | lang_test(&["es"], "Reproductor multimedia VLC")?; 297 | 298 | // Multiple languages 299 | lang_test(&["ja", "fr", "nl"], "VLCメディアプレイヤー")?; 300 | 301 | // No valid languages 302 | lang_test(&["qwert", "yuiop"], "VLC media player")?; 303 | 304 | // Some invalid languages 305 | lang_test(&["asdfg", "hjkl?;", "bn", "hu"], "VLC মিডিয়া প্লেয়ার")?; 306 | lang_test(&["zxcv", "pa", "it", "ru"], "VLC ਮੀਡਿਆ ਪਲੇਅਰ")?; 307 | 308 | Ok(()) 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_json-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 16 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 17 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 18 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 20 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 21 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 22 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 23 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 24 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 25 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 26 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 27 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 28 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 29 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 30 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 31 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 32 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 33 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 34 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 35 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 36 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 37 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 38 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 39 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 40 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 41 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 42 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 43 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 44 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 45 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 46 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: true 47 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 48 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 49 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 50 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 51 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 52 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 53 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 54 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 55 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 56 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 57 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 58 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 59 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 60 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 61 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 62 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 63 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 64 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 65 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 66 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 67 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 68 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 69 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 70 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 71 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 72 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 73 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 74 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 75 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 76 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 77 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 78 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 79 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 80 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 81 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 82 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 83 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 84 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 85 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 86 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 87 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 88 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 89 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: true 90 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 91 | -------------------------------------------------------------------------------- /src/config/snapshots/handlr__config__main_config__tests__print_handlers_detailed_json-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/config/main_config.rs 3 | expression: "String :: from_utf8(buffer).expect(\"Buffer is invalid utf8\")" 4 | --- 5 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 6 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 7 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 8 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 9 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 10 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 11 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 12 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 13 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 14 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 15 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 16 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 17 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 18 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 19 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 20 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 21 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 22 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 23 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 24 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 25 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 26 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 27 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 28 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 29 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 30 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 31 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 32 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 33 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 34 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 35 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 36 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 37 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 38 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 39 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 40 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 41 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 42 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 43 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 44 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 45 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 46 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: true 47 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 48 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/mp4` 49 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 50 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 51 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/mp4`: mpv.desktop; 52 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 53 | [TIMESTAMP]  INFO handlr::config::main_config: Adding mpv.desktop to list of handlers for `video/asdf` 54 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `mpv.desktop` is invalid: No such file or directory (os error 2) 55 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 56 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/asdf`: mpv.desktop; 57 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 58 | [TIMESTAMP]  INFO handlr::config::main_config: Adding brave.desktop to list of handlers for `video/webm` 59 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `brave.desktop` is invalid: No such file or directory (os error 2) 60 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 61 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `video/webm`: brave.desktop; 62 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 63 | [TIMESTAMP]  INFO handlr::config::main_config: Adding helix.desktop to list of handlers for `text/plain` 64 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `helix.desktop` is invalid: No such file or directory (os error 2) 65 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 66 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop; 67 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 68 | [TIMESTAMP]  INFO handlr::config::main_config: Adding nvim.desktop to list of handlers for `text/plain` 69 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `nvim.desktop` is invalid: No such file or directory (os error 2) 70 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 71 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop; 72 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 73 | [TIMESTAMP]  INFO handlr::config::main_config: Adding kakoune.desktop to list of handlers for `text/plain` 74 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `kakoune.desktop` is invalid: No such file or directory (os error 2) 75 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 76 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `text/plain`: helix.desktop;nvim.desktop;kakoune.desktop; 77 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 78 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.oasis.opendocument.*` 79 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 80 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 81 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.oasis.opendocument.*`: startcenter.desktop; 82 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 83 | [TIMESTAMP]  INFO handlr::config::main_config: Adding startcenter.desktop to list of handlers for `application/vnd.openxmlformats-officedocument.*` 84 | [TIMESTAMP]  WARN handlr::common::handler: The desktop entry `startcenter.desktop` is invalid: No such file or directory (os error 2) 85 | [TIMESTAMP] DEBUG handlr::apps::user: Expanding wildcards in mimeapps.list: false 86 | [TIMESTAMP] DEBUG handlr::apps::user: New handlers for `application/vnd.openxmlformats-officedocument.*`: startcenter.desktop; 87 | [TIMESTAMP]  INFO handlr::config::main_config: Finished adding handler 88 | [TIMESTAMP]  INFO handlr::config::main_config: Printing associations 89 | [TIMESTAMP] DEBUG handlr::config::main_config: JSON output: true 90 | [TIMESTAMP]  INFO handlr::config::main_config: Finished printing associations 91 | --------------------------------------------------------------------------------