Implement desktop menus and shortcuts
This commit is contained in:
540
Cargo.lock
generated
540
Cargo.lock
generated
@@ -473,6 +473,29 @@ dependencies = [
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"glib",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk-sys"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.1"
|
||||
@@ -621,6 +644,9 @@ name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
@@ -772,6 +798,31 @@ version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"cairo-sys-rs",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-sys-rs"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "calloop"
|
||||
version = "0.13.0"
|
||||
@@ -958,6 +1009,16 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.15.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -1028,7 +1089,7 @@ version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.3",
|
||||
@@ -2005,6 +2066,15 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
@@ -2022,6 +2092,16 @@ version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
@@ -2334,6 +2414,64 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "312d2295c7302019c395cfb90dacd00a82a2eabd700429bba9c7a3f38dbbe11b"
|
||||
|
||||
[[package]]
|
||||
name = "gdk"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
|
||||
dependencies = [
|
||||
"cairo-rs",
|
||||
"gdk-pixbuf",
|
||||
"gdk-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
"pango",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
|
||||
dependencies = [
|
||||
"gdk-pixbuf-sys",
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-pixbuf-sys"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
|
||||
dependencies = [
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gdk-sys"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
|
||||
dependencies = [
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"pkg-config",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -2404,6 +2542,38 @@ dependencies = [
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.18.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio-sys"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gix"
|
||||
version = "0.86.0"
|
||||
@@ -3248,6 +3418,53 @@ version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
"gio-sys",
|
||||
"glib-macros",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-macros"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro-crate 2.0.2",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib-sys"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.4"
|
||||
@@ -3275,6 +3492,17 @@ dependencies = [
|
||||
"gl_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
version = "0.8.2"
|
||||
@@ -3348,6 +3576,58 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
|
||||
dependencies = [
|
||||
"atk",
|
||||
"cairo-rs",
|
||||
"field-offset",
|
||||
"futures-channel",
|
||||
"gdk",
|
||||
"gdk-pixbuf",
|
||||
"gio",
|
||||
"glib",
|
||||
"gtk-sys",
|
||||
"gtk3-macros",
|
||||
"libc",
|
||||
"pango",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk-sys"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
|
||||
dependencies = [
|
||||
"atk-sys",
|
||||
"cairo-sys-rs",
|
||||
"gdk-pixbuf-sys",
|
||||
"gdk-sys",
|
||||
"gio-sys",
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"pango-sys",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gtk3-macros"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "guillotiere"
|
||||
version = "0.6.2"
|
||||
@@ -3458,6 +3738,12 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -4037,6 +4323,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"iced",
|
||||
"ironstorage",
|
||||
"muda",
|
||||
"tempfile",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -4239,6 +4526,17 @@ dependencies = [
|
||||
"cpufeatures 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"serde",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring-core"
|
||||
version = "1.0.0"
|
||||
@@ -4324,6 +4622,25 @@ dependencies = [
|
||||
"redox_syscall 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libxdo"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db"
|
||||
dependencies = [
|
||||
"libxdo-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libxdo-sys"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"x11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lilt"
|
||||
version = "0.8.2"
|
||||
@@ -4525,6 +4842,27 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.19.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"keyboard-types",
|
||||
"libxdo",
|
||||
"objc2 0.6.4",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"once_cell",
|
||||
"png",
|
||||
"thiserror 2.0.19",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mundy"
|
||||
version = "0.2.3"
|
||||
@@ -4779,7 +5117,7 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
@@ -5308,6 +5646,31 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
|
||||
dependencies = [
|
||||
"gio",
|
||||
"glib",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pango-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango-sys"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
|
||||
dependencies = [
|
||||
"glib-sys",
|
||||
"gobject-sys",
|
||||
"libc",
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -5619,6 +5982,19 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
@@ -5699,13 +6075,57 @@ dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
|
||||
dependencies = [
|
||||
"toml_datetime 0.6.3",
|
||||
"toml_edit 0.20.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
"toml_edit 0.25.13+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6589,6 +7009,15 @@ dependencies = [
|
||||
"syn 3.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.1.1"
|
||||
@@ -6882,7 +7311,7 @@ version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "287f59010008f0d7cf5e3b03196d666c1acc46c8d3e9cf34c28a1a7157601e72"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
@@ -6993,7 +7422,7 @@ version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
@@ -7084,12 +7513,31 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.2",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -7361,6 +7809,18 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.3",
|
||||
"toml_edit 0.20.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.12+spec-1.1.0"
|
||||
@@ -7369,7 +7829,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 0.7.5+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
@@ -7384,13 +7844,22 @@ checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"serde_spanned 1.1.1",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 1.0.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.5+spec-1.1.0"
|
||||
@@ -7409,6 +7878,30 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.6.3",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.3",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.13+spec-1.1.0"
|
||||
@@ -7666,7 +8159,7 @@ dependencies = [
|
||||
"fs-err",
|
||||
"glob",
|
||||
"goblin",
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"serde",
|
||||
@@ -7740,7 +8233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f8201bb1907ed8a42d80e11cbc25c8a033e7a31c3cff1d911f56eedb81d4948"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"heck 0.5.0",
|
||||
"indexmap",
|
||||
"tempfile",
|
||||
"uniffi_internal_macros",
|
||||
@@ -7811,6 +8304,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -8699,6 +9198,15 @@ dependencies = [
|
||||
"xkbcommon-dl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
@@ -8763,6 +9271,16 @@ dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11"
|
||||
version = "2.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11-dl"
|
||||
version = "2.21.0"
|
||||
@@ -8920,7 +9438,7 @@ version = "5.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
@@ -9072,7 +9590,7 @@ version = "5.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
|
||||
@@ -30,6 +30,7 @@ hmac = "0.12"
|
||||
iced = { version = "0.14", features = ["tokio"] }
|
||||
ironstorage = { path = "crates/storage" }
|
||||
keyring-core = "1.0"
|
||||
muda = "0.19"
|
||||
pgp = { version = "0.20", default-features = false }
|
||||
qrcode = { version = "0.14", default-features = false }
|
||||
rand = "0.8"
|
||||
|
||||
@@ -18,6 +18,7 @@ The current direct dependencies are:
|
||||
| [crossterm 0.29](https://crates.io/crates/crossterm/0.29.0) | Terminal I/O | MIT |
|
||||
| [Ratatui 0.30](https://crates.io/crates/ratatui/0.30.2) | TUI | MIT |
|
||||
| [Iced 0.14](https://crates.io/crates/iced/0.14.0) | Desktop UI | MIT |
|
||||
| [muda 0.19](https://crates.io/crates/muda/0.19.3) | Safe native macOS application menus and standard roles | MIT OR Apache-2.0 |
|
||||
| [gix 0.86](https://crates.io/crates/gix/0.86.0), [gix-config 0.59](https://crates.io/crates/gix-config/0.59.0) | Embedded Git objects, index, references, fetch, and merge | MIT OR Apache-2.0 |
|
||||
| [reqwest 0.13](https://crates.io/crates/reqwest/0.13.4) | HTTPS smart-Git transport with Rustls | MIT OR Apache-2.0 |
|
||||
| [flate2 1.1](https://crates.io/crates/flate2/1.1.9), [sha1 0.10](https://crates.io/crates/sha1/0.10.7) | Git pack compression and checksums | MIT OR Apache-2.0 |
|
||||
@@ -53,6 +54,7 @@ decision.
|
||||
| Password generation | [`rand`](https://crates.io/crates/rand) | MIT OR Apache-2.0 | Preferred using the operating-system CSPRNG. |
|
||||
| TOTP and HOTP | [`hmac` 0.12](https://crates.io/crates/hmac/0.12.1), [`sha1` 0.10](https://crates.io/crates/sha1/0.10.7), [`sha2` 0.10](https://crates.io/crates/sha2/0.10.9), [`data-encoding` 2.11](https://crates.io/crates/data-encoding/2.11.1) | MIT or MIT OR Apache-2.0 | Selected for a small storage-owned implementation with RFC 4226/6238 vectors. Handled URIs retain exact bytes while decoded secrets zeroize; `totp-rs` rejects HOTP URIs and cannot cover all of `pass-otp`. |
|
||||
| Native desktop clipboard | [`arboard` 3.6](https://crates.io/crates/arboard/3.6.1) | MIT OR Apache-2.0 | Selected with image support disabled and Wayland data-control enabled. Storage owns timeout, cleanup, and newer-content race policy; the safe adapter provides macOS, Windows, X11, and Wayland text access without helper processes. |
|
||||
| Native macOS menu bar | [`muda` 0.19](https://crates.io/crates/muda/0.19.3) | MIT OR Apache-2.0 | Selected only on macOS. Its safe `NSApp` adapter supplies standard roles and accelerators; Linux and Windows use the same action registry through an in-window Iced menu bar, avoiding GTK and Win32 integration dependencies. |
|
||||
| CLI cancellation | [`ctrlc` 3.5](https://crates.io/crates/ctrlc/3.5.2) | MIT OR Apache-2.0 | Selected for cross-platform interruption of the blocking clipboard lease. Ctrl-C requests storage cleanup before the CLI returns cancellation. |
|
||||
| Hidden CLI input | [`rpassword` 7.5](https://crates.io/crates/rpassword/7.5.4) | Apache-2.0 | Selected for portable terminal input with echo disabled. The CLI immediately moves returned strings into storage-owned zeroizing OTP input objects; it does not own validation or confirmation policy. |
|
||||
| QR output and desktop image input | [`qrcode` 0.14](https://crates.io/crates/qrcode/0.14.1), [`rqrr` 0.10](https://crates.io/crates/rqrr/0.10.1) | MIT OR Apache-2.0; second crate also includes ISC | `qrcode` selected without image features for storage-owned matrices and terminal rendering. `rqrr` is test-only round-trip verification. Apple camera scanning should use AVFoundation and pass only decoded bytes to Rust. |
|
||||
|
||||
@@ -15,5 +15,8 @@ iced.workspace = true
|
||||
ironstorage.workspace = true
|
||||
zeroize.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
376
apps/desktop/src/action.rs
Normal file
376
apps/desktop/src/action.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
//! One desktop action registry shared by every presentation entry point.
|
||||
|
||||
use iced::keyboard::{self, key::Named};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum UiAction {
|
||||
About,
|
||||
Settings,
|
||||
NewEntry,
|
||||
OpenEntry,
|
||||
Save,
|
||||
CloseWindow,
|
||||
Quit,
|
||||
Undo,
|
||||
Redo,
|
||||
Cut,
|
||||
CopyField,
|
||||
CopyEditedField,
|
||||
Paste,
|
||||
Find,
|
||||
TogglePaneFocus,
|
||||
Refresh,
|
||||
ReloadEntry,
|
||||
EditEntry,
|
||||
ToggleReveal,
|
||||
Lock,
|
||||
Minimize,
|
||||
Help,
|
||||
}
|
||||
|
||||
impl UiAction {
|
||||
pub const fn id(self) -> &'static str {
|
||||
match self {
|
||||
Self::About => "about",
|
||||
Self::Settings => "settings",
|
||||
Self::NewEntry => "new-entry",
|
||||
Self::OpenEntry => "open-entry",
|
||||
Self::Save => "save",
|
||||
Self::CloseWindow => "close-window",
|
||||
Self::Quit => "quit",
|
||||
Self::Undo => "undo",
|
||||
Self::Redo => "redo",
|
||||
Self::Cut => "cut",
|
||||
Self::CopyField => "copy-field",
|
||||
Self::CopyEditedField => "copy-edited-field",
|
||||
Self::Paste => "paste",
|
||||
Self::Find => "find",
|
||||
Self::TogglePaneFocus => "toggle-pane-focus",
|
||||
Self::Refresh => "refresh",
|
||||
Self::ReloadEntry => "reload-entry",
|
||||
Self::EditEntry => "edit-entry",
|
||||
Self::ToggleReveal => "toggle-reveal",
|
||||
Self::Lock => "lock",
|
||||
Self::Minimize => "minimize",
|
||||
Self::Help => "help",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum MenuGroup {
|
||||
App,
|
||||
File,
|
||||
Edit,
|
||||
View,
|
||||
Entry,
|
||||
Tools,
|
||||
Window,
|
||||
Help,
|
||||
}
|
||||
|
||||
impl MenuGroup {
|
||||
pub const ALL: [Self; 8] = [
|
||||
Self::App,
|
||||
Self::File,
|
||||
Self::Edit,
|
||||
Self::View,
|
||||
Self::Entry,
|
||||
Self::Tools,
|
||||
Self::Window,
|
||||
Self::Help,
|
||||
];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::App => "IronStorage",
|
||||
Self::File => "File",
|
||||
Self::Edit => "Edit",
|
||||
Self::View => "View",
|
||||
Self::Entry => "Entry",
|
||||
Self::Tools => "Tools",
|
||||
Self::Window => "Window",
|
||||
Self::Help => "Help",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ActionContext {
|
||||
pub storage_ready: bool,
|
||||
pub tree_loading: bool,
|
||||
pub unlocked: bool,
|
||||
pub document_open: bool,
|
||||
pub editing: bool,
|
||||
pub dirty: bool,
|
||||
pub saving: bool,
|
||||
pub focused_field: bool,
|
||||
pub focused_sensitive: bool,
|
||||
pub entry_path: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ActionSpec {
|
||||
pub action: UiAction,
|
||||
pub group: MenuGroup,
|
||||
pub label: &'static str,
|
||||
pub shortcut: Option<&'static str>,
|
||||
}
|
||||
|
||||
pub const ACTIONS: &[ActionSpec] = &[
|
||||
spec(UiAction::About, MenuGroup::App, "About IronStorage", None),
|
||||
spec(UiAction::Settings, MenuGroup::App, "Settings…", Some("⌘,")),
|
||||
spec(
|
||||
UiAction::Quit,
|
||||
MenuGroup::App,
|
||||
"Quit IronStorage",
|
||||
Some("⌘Q"),
|
||||
),
|
||||
spec(UiAction::NewEntry, MenuGroup::File, "New Entry", Some("⌘N")),
|
||||
spec(UiAction::OpenEntry, MenuGroup::File, "Open", Some("⌘O")),
|
||||
spec(UiAction::Save, MenuGroup::File, "Save", Some("⌘S")),
|
||||
spec(
|
||||
UiAction::CloseWindow,
|
||||
MenuGroup::File,
|
||||
"Close Window",
|
||||
Some("⌘W"),
|
||||
),
|
||||
spec(UiAction::Undo, MenuGroup::Edit, "Undo", Some("⌘Z")),
|
||||
spec(UiAction::Redo, MenuGroup::Edit, "Redo", Some("⇧⌘Z")),
|
||||
spec(UiAction::Cut, MenuGroup::Edit, "Cut", Some("⌘X")),
|
||||
spec(UiAction::CopyField, MenuGroup::Entry, "Copy Field", None),
|
||||
spec(
|
||||
UiAction::CopyEditedField,
|
||||
MenuGroup::Entry,
|
||||
"Copy Edited Field",
|
||||
None,
|
||||
),
|
||||
spec(UiAction::Paste, MenuGroup::Edit, "Paste", Some("⌘V")),
|
||||
spec(UiAction::Find, MenuGroup::Edit, "Find", Some("⌘F")),
|
||||
spec(
|
||||
UiAction::TogglePaneFocus,
|
||||
MenuGroup::View,
|
||||
"Next Pane",
|
||||
Some("Tab"),
|
||||
),
|
||||
spec(UiAction::Refresh, MenuGroup::View, "Refresh", Some("⌘R")),
|
||||
spec(
|
||||
UiAction::ReloadEntry,
|
||||
MenuGroup::Entry,
|
||||
"Reload Entry",
|
||||
None,
|
||||
),
|
||||
spec(UiAction::EditEntry, MenuGroup::Entry, "Edit Entry", None),
|
||||
spec(
|
||||
UiAction::ToggleReveal,
|
||||
MenuGroup::Entry,
|
||||
"Reveal or Hide Field",
|
||||
None,
|
||||
),
|
||||
spec(UiAction::Lock, MenuGroup::Tools, "Lock", Some("⌘L")),
|
||||
spec(
|
||||
UiAction::Minimize,
|
||||
MenuGroup::Window,
|
||||
"Minimize",
|
||||
Some("⌘M"),
|
||||
),
|
||||
spec(
|
||||
UiAction::Help,
|
||||
MenuGroup::Help,
|
||||
"IronStorage Help",
|
||||
Some("F1"),
|
||||
),
|
||||
];
|
||||
|
||||
const fn spec(
|
||||
action: UiAction,
|
||||
group: MenuGroup,
|
||||
label: &'static str,
|
||||
shortcut: Option<&'static str>,
|
||||
) -> ActionSpec {
|
||||
ActionSpec {
|
||||
action,
|
||||
group,
|
||||
label,
|
||||
shortcut,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spec_for(action: UiAction) -> &'static ActionSpec {
|
||||
ACTIONS
|
||||
.iter()
|
||||
.find(|spec| spec.action == action)
|
||||
.expect("every UI action is registered")
|
||||
}
|
||||
|
||||
pub fn actions_in(group: MenuGroup) -> impl Iterator<Item = &'static ActionSpec> {
|
||||
ACTIONS.iter().filter(move |spec| spec.group == group)
|
||||
}
|
||||
|
||||
pub fn shortcut_label(action: UiAction) -> Option<String> {
|
||||
let shortcut = spec_for(action).shortcut?;
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(shortcut.to_owned())
|
||||
} else {
|
||||
Some(shortcut.replace('⇧', "Shift+").replace('⌘', "Ctrl+"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
||||
match action {
|
||||
UiAction::About | UiAction::Settings | UiAction::Help => true,
|
||||
// Entry creation is not valid until the dedicated workflow exists.
|
||||
UiAction::NewEntry => false,
|
||||
UiAction::OpenEntry => context.storage_ready && context.entry_path && !context.saving,
|
||||
UiAction::Save => context.unlocked && context.editing && context.dirty && !context.saving,
|
||||
UiAction::CloseWindow | UiAction::Quit | UiAction::Minimize => true,
|
||||
UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste | UiAction::Find => false,
|
||||
UiAction::CopyField => {
|
||||
context.unlocked && context.document_open && !context.editing && context.focused_field
|
||||
}
|
||||
UiAction::CopyEditedField => context.unlocked && context.editing && context.focused_field,
|
||||
UiAction::TogglePaneFocus => true,
|
||||
UiAction::Refresh => context.storage_ready && !context.tree_loading,
|
||||
UiAction::ReloadEntry => context.unlocked && context.document_open && !context.saving,
|
||||
UiAction::EditEntry => context.unlocked && context.document_open && !context.editing,
|
||||
UiAction::ToggleReveal => {
|
||||
context.unlocked && context.document_open && context.focused_sensitive
|
||||
}
|
||||
UiAction::Lock => context.unlocked,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shortcut_action(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<UiAction> {
|
||||
if key.as_ref() == keyboard::Key::Named(Named::Tab) && modifiers.is_empty() {
|
||||
return Some(UiAction::TogglePaneFocus);
|
||||
}
|
||||
if key.as_ref() == keyboard::Key::Named(Named::F1) {
|
||||
return Some(UiAction::Help);
|
||||
}
|
||||
if !modifiers.command() {
|
||||
return None;
|
||||
}
|
||||
match key.as_ref() {
|
||||
keyboard::Key::Character(",") => Some(UiAction::Settings),
|
||||
keyboard::Key::Character("n" | "N") => Some(UiAction::NewEntry),
|
||||
keyboard::Key::Character("o" | "O") => Some(UiAction::OpenEntry),
|
||||
keyboard::Key::Character("s" | "S") => Some(UiAction::Save),
|
||||
keyboard::Key::Character("w" | "W") => Some(UiAction::CloseWindow),
|
||||
keyboard::Key::Character("q" | "Q") => Some(UiAction::Quit),
|
||||
keyboard::Key::Character("z" | "Z") if modifiers.shift() => Some(UiAction::Redo),
|
||||
keyboard::Key::Character("z" | "Z") => Some(UiAction::Undo),
|
||||
keyboard::Key::Character("x" | "X") => Some(UiAction::Cut),
|
||||
keyboard::Key::Character("c" | "C") => Some(UiAction::CopyField),
|
||||
keyboard::Key::Character("v" | "V") => Some(UiAction::Paste),
|
||||
keyboard::Key::Character("f" | "F") => Some(UiAction::Find),
|
||||
keyboard::Key::Character("r" | "R") => Some(UiAction::Refresh),
|
||||
keyboard::Key::Character("l" | "L") => Some(UiAction::Lock),
|
||||
keyboard::Key::Character("m" | "M") => Some(UiAction::Minimize),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn context() -> ActionContext {
|
||||
ActionContext {
|
||||
storage_ready: true,
|
||||
tree_loading: false,
|
||||
unlocked: true,
|
||||
document_open: true,
|
||||
editing: true,
|
||||
dirty: true,
|
||||
saving: false,
|
||||
focused_field: true,
|
||||
focused_sensitive: true,
|
||||
entry_path: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_is_complete_unique_and_grouped_for_every_platform_menu() {
|
||||
let actions = ACTIONS
|
||||
.iter()
|
||||
.map(|spec| spec.action)
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(actions.len(), ACTIONS.len());
|
||||
for group in MenuGroup::ALL {
|
||||
assert!(actions_in(group).next().is_some(), "missing {group:?} menu");
|
||||
}
|
||||
for action in actions {
|
||||
assert_eq!(spec_for(action).action, action);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enablement_closes_authentication_dirty_and_loading_bypasses() {
|
||||
let ready = context();
|
||||
assert!(!enabled(UiAction::NewEntry, ready));
|
||||
assert!(enabled(UiAction::Save, ready));
|
||||
assert!(enabled(UiAction::CopyEditedField, ready));
|
||||
assert!(!enabled(UiAction::CopyField, ready));
|
||||
assert!(enabled(UiAction::ToggleReveal, ready));
|
||||
|
||||
let viewing = ActionContext {
|
||||
editing: false,
|
||||
dirty: false,
|
||||
..ready
|
||||
};
|
||||
assert!(enabled(UiAction::CopyField, viewing));
|
||||
assert!(!enabled(UiAction::CopyEditedField, viewing));
|
||||
|
||||
let locked = ActionContext {
|
||||
unlocked: false,
|
||||
..ready
|
||||
};
|
||||
for action in [
|
||||
UiAction::Save,
|
||||
UiAction::CopyField,
|
||||
UiAction::CopyEditedField,
|
||||
UiAction::ToggleReveal,
|
||||
UiAction::Lock,
|
||||
] {
|
||||
assert!(!enabled(action, locked));
|
||||
}
|
||||
assert!(!enabled(
|
||||
UiAction::Refresh,
|
||||
ActionContext {
|
||||
tree_loading: true,
|
||||
..ready
|
||||
}
|
||||
));
|
||||
assert!(!enabled(
|
||||
UiAction::Save,
|
||||
ActionContext {
|
||||
dirty: false,
|
||||
..ready
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conventional_shortcuts_resolve_to_the_registered_actions() {
|
||||
let primary = keyboard::Modifiers::COMMAND;
|
||||
for (key, expected) in [
|
||||
("o", UiAction::OpenEntry),
|
||||
("s", UiAction::Save),
|
||||
("f", UiAction::Find),
|
||||
("n", UiAction::NewEntry),
|
||||
("w", UiAction::CloseWindow),
|
||||
("q", UiAction::Quit),
|
||||
("x", UiAction::Cut),
|
||||
("c", UiAction::CopyField),
|
||||
("v", UiAction::Paste),
|
||||
(",", UiAction::Settings),
|
||||
] {
|
||||
assert_eq!(
|
||||
shortcut_action(&keyboard::Key::Character(key.into()), primary),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::disallowed_types)]
|
||||
|
||||
mod action;
|
||||
mod editor;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
mod navigation;
|
||||
|
||||
use std::{
|
||||
@@ -40,18 +43,28 @@ use ironstorage::{
|
||||
use navigation::{NavigationIntent, NavigationKey, NavigationTree};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use action::{ActionContext, MenuGroup, UiAction};
|
||||
#[cfg(target_os = "macos")]
|
||||
use native_menu::NativeMenu;
|
||||
|
||||
type OpenCompletion = Arc<Mutex<Option<Result<EntryDocument, String>>>>;
|
||||
type SaveCompletion = Arc<Mutex<Option<(EntryEditor, Result<WriteOutcome, DesktopError>)>>>;
|
||||
type TreeCompletion = Arc<Mutex<Option<Result<TreeModel, String>>>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Message {
|
||||
Action(UiAction),
|
||||
FieldAction(EntryFieldId, UiAction),
|
||||
ToggleMenu(MenuGroup),
|
||||
DismissUtility,
|
||||
WindowResolved(UiAction, Option<window::Id>),
|
||||
#[cfg(target_os = "macos")]
|
||||
PollNativeMenu,
|
||||
StartupLoaded(Box<Result<(DesktopStorage, NativeAuthenticationSession, KeyInfo), String>>),
|
||||
TreeLoaded {
|
||||
generation: u64,
|
||||
completion: TreeCompletion,
|
||||
},
|
||||
RefreshTree,
|
||||
SidebarActivate(TreeNodeId),
|
||||
SidebarNavigate(NavigationKey),
|
||||
TogglePaneFocus,
|
||||
@@ -83,7 +96,6 @@ enum Message {
|
||||
generation: u64,
|
||||
result: Result<String, String>,
|
||||
},
|
||||
Save,
|
||||
SaveFinished {
|
||||
generation: u64,
|
||||
completion: SaveCompletion,
|
||||
@@ -163,6 +175,10 @@ struct App {
|
||||
generate_confirmation: Option<EntryFieldId>,
|
||||
conflict: bool,
|
||||
status: String,
|
||||
open_menu: Option<MenuGroup>,
|
||||
utility: Option<UtilityView>,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu: Option<NativeMenu>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -183,6 +199,13 @@ enum ContentMode {
|
||||
Editor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum UtilityView {
|
||||
About,
|
||||
Settings,
|
||||
Help,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
enum TreeState {
|
||||
Loading,
|
||||
@@ -226,8 +249,7 @@ fn main() -> iced::Result {
|
||||
|
||||
impl App {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
(
|
||||
Self {
|
||||
let mut app = Self {
|
||||
authentication: AuthenticationView::Loading,
|
||||
storage: None,
|
||||
session: None,
|
||||
@@ -256,7 +278,18 @@ impl App {
|
||||
generate_confirmation: None,
|
||||
conflict: false,
|
||||
status: "Loading shared configuration…".to_owned(),
|
||||
},
|
||||
open_menu: None,
|
||||
utility: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu: None,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
match NativeMenu::install(app.action_context()) {
|
||||
Ok(menu) => app.native_menu = Some(menu),
|
||||
Err(error) => app.status = format!("Native menu unavailable: {error}"),
|
||||
}
|
||||
(
|
||||
app,
|
||||
Task::perform(load_authentication(), |result| {
|
||||
Message::StartupLoaded(Box::new(result))
|
||||
}),
|
||||
@@ -265,6 +298,35 @@ impl App {
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Action(action) => return self.invoke_action(action),
|
||||
Message::FieldAction(id, action) => {
|
||||
if let Some(editor) = self.editor.as_mut() {
|
||||
editor.select(id);
|
||||
}
|
||||
return self.invoke_action(action);
|
||||
}
|
||||
Message::ToggleMenu(group) => {
|
||||
self.open_menu = (self.open_menu != Some(group)).then_some(group);
|
||||
}
|
||||
Message::DismissUtility => self.utility = None,
|
||||
Message::WindowResolved(action, id) => {
|
||||
let Some(id) = id else {
|
||||
return Task::none();
|
||||
};
|
||||
return match action {
|
||||
UiAction::CloseWindow | UiAction::Quit => {
|
||||
self.request_action(PendingAction::CloseWindow(id))
|
||||
}
|
||||
UiAction::Minimize => window::minimize(id, true),
|
||||
_ => Task::none(),
|
||||
};
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
Message::PollNativeMenu => {
|
||||
if let Some(action) = self.native_menu.as_ref().and_then(NativeMenu::poll) {
|
||||
return self.invoke_action(action);
|
||||
}
|
||||
}
|
||||
Message::StartupLoaded(result) => match *result {
|
||||
Ok((storage, session, key)) => {
|
||||
self.storage = Some(storage);
|
||||
@@ -307,7 +369,6 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::RefreshTree => return self.begin_tree_refresh(),
|
||||
Message::SidebarActivate(id) => {
|
||||
self.touch_user_activity();
|
||||
self.pane_focus = PaneFocus::Sidebar;
|
||||
@@ -484,7 +545,6 @@ impl App {
|
||||
self.status = result.unwrap_or_else(|error| format!("Clipboard: {error}"));
|
||||
}
|
||||
}
|
||||
Message::Save => return self.begin_save(),
|
||||
Message::SaveFinished {
|
||||
generation,
|
||||
completion,
|
||||
@@ -575,6 +635,81 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn action_context(&self) -> ActionContext {
|
||||
let focused = self
|
||||
.editor
|
||||
.as_ref()
|
||||
.and_then(|editor| editor.focused().and_then(|id| editor.document().field(id)));
|
||||
ActionContext {
|
||||
storage_ready: self.storage.is_some(),
|
||||
tree_loading: self.tree_state == TreeState::Loading,
|
||||
unlocked: authentication_allows_content(&self.authentication),
|
||||
document_open: self.editor.is_some(),
|
||||
editing: self.content_mode == ContentMode::Editor,
|
||||
dirty: self.editor.as_ref().is_some_and(EntryEditor::is_dirty),
|
||||
saving: self.saving,
|
||||
focused_field: focused.is_some(),
|
||||
focused_sensitive: focused
|
||||
.is_some_and(|field| field.metadata().sensitivity() == EntrySensitivity::Sensitive),
|
||||
entry_path: !self.entry_path.trim().is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn invoke_action(&mut self, action: UiAction) -> Task<Message> {
|
||||
self.open_menu = None;
|
||||
if !action::enabled(action, self.action_context()) {
|
||||
if self.content_mode == ContentMode::Editor
|
||||
&& matches!(
|
||||
action,
|
||||
UiAction::Undo
|
||||
| UiAction::Redo
|
||||
| UiAction::Cut
|
||||
| UiAction::CopyField
|
||||
| UiAction::Paste
|
||||
)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
self.status = format!(
|
||||
"{} is unavailable in the current state.",
|
||||
action::spec_for(action).label
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
match action {
|
||||
UiAction::About => self.utility = Some(UtilityView::About),
|
||||
UiAction::Settings => self.utility = Some(UtilityView::Settings),
|
||||
UiAction::Help => self.utility = Some(UtilityView::Help),
|
||||
UiAction::OpenEntry => return self.update(Message::OpenEntry),
|
||||
UiAction::Save => return self.begin_save(),
|
||||
UiAction::CloseWindow | UiAction::Quit | UiAction::Minimize => {
|
||||
return window::oldest().map(move |id| Message::WindowResolved(action, id));
|
||||
}
|
||||
UiAction::CopyField | UiAction::CopyEditedField => {
|
||||
if let Some(id) = self.editor.as_ref().and_then(EntryEditor::focused) {
|
||||
return self.update(Message::Copy(id));
|
||||
}
|
||||
}
|
||||
UiAction::TogglePaneFocus => return self.update(Message::TogglePaneFocus),
|
||||
UiAction::Refresh => return self.begin_tree_refresh(),
|
||||
UiAction::ReloadEntry => return self.update(Message::RequestReload),
|
||||
UiAction::EditEntry => return self.update(Message::BeginEdit),
|
||||
UiAction::ToggleReveal => {
|
||||
if let Some(id) = self.editor.as_ref().and_then(EntryEditor::focused) {
|
||||
return self.update(Message::ToggleReveal(id));
|
||||
}
|
||||
}
|
||||
UiAction::Lock => return self.update(Message::Lock),
|
||||
UiAction::NewEntry
|
||||
| UiAction::Undo
|
||||
| UiAction::Redo
|
||||
| UiAction::Cut
|
||||
| UiAction::Paste
|
||||
| UiAction::Find => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn begin_tree_refresh(&mut self) -> Task<Message> {
|
||||
let Some(storage) = self.storage.clone() else {
|
||||
return Task::none();
|
||||
@@ -810,20 +945,30 @@ impl App {
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch([
|
||||
let mut subscriptions = vec![
|
||||
time::every(Duration::from_secs(1)).map(|_| Message::Tick),
|
||||
event::listen_with(|event, _status, _window| event_message(&event)),
|
||||
window::close_requests().map(Message::RequestClose),
|
||||
])
|
||||
];
|
||||
#[cfg(target_os = "macos")]
|
||||
subscriptions.push(time::every(Duration::from_millis(50)).map(|_| Message::PollNativeMenu));
|
||||
Subscription::batch(subscriptions)
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message> {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(menu) = &self.native_menu {
|
||||
menu.sync(self.action_context());
|
||||
}
|
||||
if let Some(action) = &self.confirmation {
|
||||
return confirmation_view(action);
|
||||
}
|
||||
if let Some(id) = self.generate_confirmation {
|
||||
return generate_confirmation_view(id);
|
||||
}
|
||||
if let Some(utility) = self.utility {
|
||||
return utility_view(self, utility);
|
||||
}
|
||||
|
||||
let authentication = match &self.authentication {
|
||||
AuthenticationView::Loading => "Loading".to_owned(),
|
||||
@@ -857,6 +1002,7 @@ impl App {
|
||||
]
|
||||
.spacing(16)
|
||||
.padding(10),
|
||||
platform_menu_bar(self),
|
||||
panes,
|
||||
]
|
||||
.height(Length::Fill),
|
||||
@@ -875,6 +1021,95 @@ fn viewer_scroll_id() -> iced::widget::Id {
|
||||
iced::widget::Id::new("desktop-entry-viewer")
|
||||
}
|
||||
|
||||
fn platform_menu_bar(app: &App) -> Element<'_, Message> {
|
||||
if cfg!(target_os = "macos") {
|
||||
return container(row![]).height(Length::Fixed(0.0)).into();
|
||||
}
|
||||
let mut headers = row![].spacing(2).padding([0, 8]);
|
||||
for group in MenuGroup::ALL {
|
||||
headers = headers.push(
|
||||
button(group.label())
|
||||
.on_press(Message::ToggleMenu(group))
|
||||
.style(if app.open_menu == Some(group) {
|
||||
button::primary
|
||||
} else {
|
||||
button::text
|
||||
}),
|
||||
);
|
||||
}
|
||||
let mut menu = column![headers].spacing(4);
|
||||
if let Some(group) = app.open_menu {
|
||||
let context = app.action_context();
|
||||
let mut actions = row![].spacing(4).padding([6, 8]);
|
||||
for spec in action::actions_in(group) {
|
||||
let label = action::shortcut_label(spec.action).map_or_else(
|
||||
|| spec.label.to_owned(),
|
||||
|key| format!("{} {key}", spec.label),
|
||||
);
|
||||
let item = button(text(label));
|
||||
actions = actions.push(if action::enabled(spec.action, context) {
|
||||
item.on_press(Message::Action(spec.action))
|
||||
} else {
|
||||
item
|
||||
});
|
||||
}
|
||||
menu = menu.push(
|
||||
scrollable(actions).direction(scrollable::Direction::Horizontal(
|
||||
scrollable::Scrollbar::default(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
container(menu).width(Length::Fill).into()
|
||||
}
|
||||
|
||||
fn utility_view(app: &App, utility: UtilityView) -> Element<'_, Message> {
|
||||
let mut content = column![].spacing(10);
|
||||
match utility {
|
||||
UtilityView::About => {
|
||||
content = content
|
||||
.push(text(ironstorage::PRODUCT_NAME).size(28))
|
||||
.push(text(format!("Version {}", env!("CARGO_PKG_VERSION"))))
|
||||
.push(text("A native, pass-compatible password-store client."));
|
||||
}
|
||||
UtilityView::Settings => {
|
||||
content = content.push(text("Settings").size(28));
|
||||
if let Some(storage) = &app.storage {
|
||||
content = content
|
||||
.push(text(format!(
|
||||
"Authentication inactivity timeout: {} seconds",
|
||||
storage.authentication_timeout().duration().as_secs()
|
||||
)))
|
||||
.push(text(format!(
|
||||
"Clipboard cleanup timeout: {} seconds",
|
||||
storage.clipboard_timeout().duration().as_secs()
|
||||
)))
|
||||
.push(text("Values come from the shared validated configuration."));
|
||||
} else {
|
||||
content = content.push(text("Shared configuration is still loading."));
|
||||
}
|
||||
}
|
||||
UtilityView::Help => {
|
||||
content = content.push(text("Keyboard shortcuts").size(28));
|
||||
for spec in action::ACTIONS {
|
||||
if let Some(shortcut) = action::shortcut_label(spec.action) {
|
||||
content = content.push(text(format!("{shortcut} {}", spec.label)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
container(
|
||||
column![
|
||||
scrollable(content).height(Length::Fill),
|
||||
button("Done").on_press(Message::DismissUtility),
|
||||
]
|
||||
.spacing(12)
|
||||
.padding(20),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn sidebar_view<'a>(
|
||||
navigation: &'a NavigationTree,
|
||||
state: &'a TreeState,
|
||||
@@ -888,7 +1123,7 @@ fn sidebar_view<'a>(
|
||||
"Password Store"
|
||||
})
|
||||
.size(20),
|
||||
button("Refresh").on_press(Message::RefreshTree),
|
||||
button("Refresh").on_press(Message::Action(UiAction::Refresh)),
|
||||
]
|
||||
.spacing(8)
|
||||
]
|
||||
@@ -965,10 +1200,10 @@ fn content_view(app: &App) -> Element<'_, Message> {
|
||||
.size(20),
|
||||
text_input("Entry path", &app.entry_path)
|
||||
.on_input(Message::EntryPathChanged)
|
||||
.on_submit(Message::OpenEntry),
|
||||
button("Open").on_press(Message::OpenEntry),
|
||||
button("Reload").on_press(Message::RequestReload),
|
||||
button("Lock").on_press(Message::Lock),
|
||||
.on_submit(Message::Action(UiAction::OpenEntry)),
|
||||
button("Open").on_press(Message::Action(UiAction::OpenEntry)),
|
||||
button("Reload").on_press(Message::Action(UiAction::ReloadEntry)),
|
||||
button("Lock").on_press(Message::Action(UiAction::Lock)),
|
||||
]
|
||||
.spacing(8);
|
||||
|
||||
@@ -1054,7 +1289,7 @@ fn viewer_view(editor: &EntryEditor) -> Element<'_, Message> {
|
||||
let mut rows = column![
|
||||
row![
|
||||
text(editor.entry()).size(22),
|
||||
button("Edit entry").on_press(Message::BeginEdit),
|
||||
button("Edit entry").on_press(Message::Action(UiAction::EditEntry)),
|
||||
]
|
||||
.spacing(8),
|
||||
text("Use Up/Down/Home/End to select fields; Enter reveals or hides a selected sensitive value.")
|
||||
@@ -1073,15 +1308,18 @@ fn viewer_view(editor: &EntryEditor) -> Element<'_, Message> {
|
||||
ViewerValue::Text(value) => value,
|
||||
ViewerValue::Unavailable => "(binary value)",
|
||||
};
|
||||
let mut actions =
|
||||
row![button(text(format!("Copy {label}"))).on_press(Message::Copy(id)),].spacing(6);
|
||||
let mut actions = row![
|
||||
button(text(format!("Copy {label}")))
|
||||
.on_press(Message::FieldAction(id, UiAction::CopyField)),
|
||||
]
|
||||
.spacing(6);
|
||||
if field.metadata().sensitivity() == EntrySensitivity::Sensitive {
|
||||
actions = actions.push(
|
||||
button(text(format!(
|
||||
"{} {label}",
|
||||
if revealed { "Hide" } else { "Reveal" }
|
||||
)))
|
||||
.on_press(Message::ToggleReveal(id)),
|
||||
.on_press(Message::FieldAction(id, UiAction::ToggleReveal)),
|
||||
);
|
||||
}
|
||||
let mut field_view = column![
|
||||
@@ -1150,7 +1388,7 @@ fn editor_view(editor: &EntryEditor, conflict: bool) -> Element<'_, Message> {
|
||||
if editor.is_dirty() { " •" } else { "" }
|
||||
))
|
||||
.size(22),
|
||||
button("Save").on_press(Message::Save),
|
||||
button("Save").on_press(Message::Action(UiAction::Save)),
|
||||
button("Add line").on_press(Message::AddAfter(None)),
|
||||
]
|
||||
.spacing(8)
|
||||
@@ -1197,7 +1435,7 @@ fn editor_view(editor: &EntryEditor, conflict: bool) -> Element<'_, Message> {
|
||||
button("↓").on_press(Message::MoveDown(id)),
|
||||
button("Add below").on_press(Message::AddAfter(Some(id))),
|
||||
button("Remove").on_press(Message::Remove(id)),
|
||||
button("Copy value").on_press(Message::Copy(id)),
|
||||
button("Copy value").on_press(Message::FieldAction(id, UiAction::CopyEditedField)),
|
||||
]
|
||||
.spacing(6);
|
||||
if sensitive {
|
||||
@@ -1207,7 +1445,7 @@ fn editor_view(editor: &EntryEditor, conflict: bool) -> Element<'_, Message> {
|
||||
} else {
|
||||
"Reveal"
|
||||
})
|
||||
.on_press(Message::ToggleReveal(id)),
|
||||
.on_press(Message::FieldAction(id, UiAction::ToggleReveal)),
|
||||
);
|
||||
}
|
||||
if sensitive && field.metadata().kind() != EntryFieldKind::OtpUri {
|
||||
@@ -1331,13 +1569,10 @@ fn poll_lease<B: SecretStoreBackend, C: AuthenticationClock>(
|
||||
|
||||
fn event_message(event: &Event) -> Option<Message> {
|
||||
if let Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event {
|
||||
if modifiers.command() && matches!(key.as_ref(), keyboard::Key::Character("s" | "S")) {
|
||||
return Some(Message::Save);
|
||||
if let Some(action) = action::shortcut_action(key, *modifiers) {
|
||||
return Some(Message::Action(action));
|
||||
}
|
||||
let navigation = match key.as_ref() {
|
||||
keyboard::Key::Named(keyboard::key::Named::Tab) => {
|
||||
return Some(Message::TogglePaneFocus);
|
||||
}
|
||||
keyboard::Key::Named(keyboard::key::Named::ArrowUp) => NavigationKey::Previous,
|
||||
keyboard::Key::Named(keyboard::key::Named::ArrowDown) => NavigationKey::Next,
|
||||
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => NavigationKey::Collapse,
|
||||
@@ -1574,6 +1809,10 @@ mod tests {
|
||||
generate_confirmation: None,
|
||||
conflict: false,
|
||||
status: String::new(),
|
||||
open_menu: None,
|
||||
utility: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1787,6 +2026,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_actions_cannot_bypass_authentication_or_dirty_confirmation() {
|
||||
let (_temporary, storage) = fixture_storage();
|
||||
let mut editor = empty_editor(&storage, "draft");
|
||||
editor.add_after(None).expect("dirty line");
|
||||
let mut app = test_app(Some(editor));
|
||||
app.content_mode = ContentMode::Editor;
|
||||
|
||||
assert!(!action::enabled(UiAction::Save, app.action_context()));
|
||||
let _task = app.invoke_action(UiAction::Save);
|
||||
assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));
|
||||
|
||||
app.authentication = AuthenticationView::Unlocked(Duration::from_secs(60));
|
||||
assert!(action::enabled(UiAction::Save, app.action_context()));
|
||||
let id = window::Id::unique();
|
||||
let _task = app.update(Message::WindowResolved(UiAction::CloseWindow, Some(id)));
|
||||
assert_eq!(app.confirmation, Some(PendingAction::CloseWindow(id)));
|
||||
assert!(app.editor.as_ref().is_some_and(EntryEditor::is_dirty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_and_expiry_drop_the_complete_editor_and_clipboard_state() {
|
||||
let (_temporary, storage) = fixture_storage();
|
||||
@@ -1840,7 +2099,10 @@ mod tests {
|
||||
text: Some("s".into()),
|
||||
repeat: false,
|
||||
});
|
||||
assert!(matches!(event_message(&save), Some(Message::Save)));
|
||||
assert!(matches!(
|
||||
event_message(&save),
|
||||
Some(Message::Action(UiAction::Save))
|
||||
));
|
||||
|
||||
let tab = Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::Tab),
|
||||
@@ -1853,7 +2115,7 @@ mod tests {
|
||||
});
|
||||
assert!(matches!(
|
||||
event_message(&tab),
|
||||
Some(Message::TogglePaneFocus)
|
||||
Some(Message::Action(UiAction::TogglePaneFocus))
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
141
apps/desktop/src/native_menu.rs
Normal file
141
apps/desktop/src/native_menu.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! Safe macOS menu adapter over the shared desktop action registry.
|
||||
|
||||
use muda::{
|
||||
Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu,
|
||||
accelerator::{Accelerator, Code, Modifiers},
|
||||
};
|
||||
|
||||
use crate::action::{self, ActionContext, MenuGroup, UiAction};
|
||||
|
||||
pub struct NativeMenu {
|
||||
_menu: Menu,
|
||||
items: Vec<(UiAction, MenuItem)>,
|
||||
}
|
||||
|
||||
impl NativeMenu {
|
||||
pub fn install(context: ActionContext) -> Result<Self, muda::Error> {
|
||||
let menu = Menu::new();
|
||||
let mut items = Vec::new();
|
||||
for group in MenuGroup::ALL {
|
||||
let submenu = Submenu::new(group.label(), true);
|
||||
match group {
|
||||
MenuGroup::App => {
|
||||
submenu.append(&PredefinedMenuItem::about(None, None))?;
|
||||
submenu.append(&PredefinedMenuItem::separator())?;
|
||||
append_action(&submenu, UiAction::Settings, context, &mut items)?;
|
||||
submenu.append(&PredefinedMenuItem::separator())?;
|
||||
submenu.append(&PredefinedMenuItem::services(None))?;
|
||||
submenu.append(&PredefinedMenuItem::separator())?;
|
||||
submenu.append(&PredefinedMenuItem::hide(None))?;
|
||||
submenu.append(&PredefinedMenuItem::hide_others(None))?;
|
||||
submenu.append(&PredefinedMenuItem::show_all(None))?;
|
||||
submenu.append(&PredefinedMenuItem::separator())?;
|
||||
append_action(&submenu, UiAction::Quit, context, &mut items)?;
|
||||
}
|
||||
MenuGroup::Window => {
|
||||
submenu.append(&PredefinedMenuItem::minimize(None))?;
|
||||
submenu.append(&PredefinedMenuItem::bring_all_to_front(None))?;
|
||||
}
|
||||
MenuGroup::Edit => {
|
||||
submenu.append(&PredefinedMenuItem::undo(None))?;
|
||||
submenu.append(&PredefinedMenuItem::redo(None))?;
|
||||
submenu.append(&PredefinedMenuItem::separator())?;
|
||||
submenu.append(&PredefinedMenuItem::cut(None))?;
|
||||
submenu.append(&PredefinedMenuItem::copy(None))?;
|
||||
submenu.append(&PredefinedMenuItem::paste(None))?;
|
||||
submenu.append(&PredefinedMenuItem::select_all(None))?;
|
||||
submenu.append(&PredefinedMenuItem::separator())?;
|
||||
append_action(&submenu, UiAction::Find, context, &mut items)?;
|
||||
}
|
||||
_ => append_actions(&submenu, group, context, &mut items)?,
|
||||
}
|
||||
menu.append(&submenu)?;
|
||||
}
|
||||
menu.init_for_nsapp();
|
||||
Ok(Self { _menu: menu, items })
|
||||
}
|
||||
|
||||
pub fn sync(&self, context: ActionContext) {
|
||||
for (action, item) in &self.items {
|
||||
item.set_enabled(action::enabled(*action, context));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&self) -> Option<UiAction> {
|
||||
let event = MenuEvent::receiver().try_recv().ok()?;
|
||||
self.items
|
||||
.iter()
|
||||
.find_map(|(action, item)| (item.id() == &event.id).then_some(*action))
|
||||
}
|
||||
}
|
||||
|
||||
fn append_actions(
|
||||
submenu: &Submenu,
|
||||
group: MenuGroup,
|
||||
context: ActionContext,
|
||||
items: &mut Vec<(UiAction, MenuItem)>,
|
||||
) -> Result<(), muda::Error> {
|
||||
for spec in action::actions_in(group).filter(|spec| {
|
||||
!matches!(
|
||||
spec.action,
|
||||
UiAction::About
|
||||
| UiAction::Undo
|
||||
| UiAction::Redo
|
||||
| UiAction::Cut
|
||||
| UiAction::CopyEditedField
|
||||
| UiAction::Paste
|
||||
| UiAction::Minimize
|
||||
| UiAction::TogglePaneFocus
|
||||
)
|
||||
}) {
|
||||
append_action(submenu, spec.action, context, items)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_action(
|
||||
submenu: &Submenu,
|
||||
action: UiAction,
|
||||
context: ActionContext,
|
||||
items: &mut Vec<(UiAction, MenuItem)>,
|
||||
) -> Result<(), muda::Error> {
|
||||
let spec = action::spec_for(action);
|
||||
let item = MenuItem::with_id(
|
||||
format!("ironstorage.{}", action.id()),
|
||||
spec.label,
|
||||
action::enabled(action, context),
|
||||
accelerator(action),
|
||||
);
|
||||
submenu.append(&item)?;
|
||||
items.push((action, item));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn accelerator(action: UiAction) -> Option<Accelerator> {
|
||||
let command = Modifiers::SUPER;
|
||||
let (modifiers, code) = match action {
|
||||
UiAction::Settings => (command, Code::Comma),
|
||||
UiAction::NewEntry => (command, Code::KeyN),
|
||||
UiAction::OpenEntry => (command, Code::KeyO),
|
||||
UiAction::Save => (command, Code::KeyS),
|
||||
UiAction::CloseWindow => (command, Code::KeyW),
|
||||
UiAction::Quit => (command, Code::KeyQ),
|
||||
UiAction::Undo => (command, Code::KeyZ),
|
||||
UiAction::Redo => (command | Modifiers::SHIFT, Code::KeyZ),
|
||||
UiAction::Cut => (command, Code::KeyX),
|
||||
UiAction::Paste => (command, Code::KeyV),
|
||||
UiAction::Find => (command, Code::KeyF),
|
||||
UiAction::Refresh => (command, Code::KeyR),
|
||||
UiAction::Lock => (command, Code::KeyL),
|
||||
UiAction::Help => (Modifiers::empty(), Code::F1),
|
||||
UiAction::About
|
||||
| UiAction::CopyField
|
||||
| UiAction::CopyEditedField
|
||||
| UiAction::TogglePaneFocus
|
||||
| UiAction::ReloadEntry
|
||||
| UiAction::EditEntry
|
||||
| UiAction::ToggleReveal
|
||||
| UiAction::Minimize => return None,
|
||||
};
|
||||
Some(Accelerator::new(Some(modifiers), code))
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
use std::{error::Error, fmt, path::Path};
|
||||
|
||||
use crate::{
|
||||
authentication::{NativeAuthenticationHandle, NativeAuthenticationSession},
|
||||
authentication::{
|
||||
AuthenticationTimeout, NativeAuthenticationHandle, NativeAuthenticationSession,
|
||||
},
|
||||
config::Config,
|
||||
crypto::{KeyInfo, KeyStore, SecretProvider},
|
||||
document::{DocumentError, EntryDocument, EntryDocumentService},
|
||||
@@ -111,6 +113,10 @@ impl DesktopStorage {
|
||||
self.config.clipboard_timeout()
|
||||
}
|
||||
|
||||
pub fn authentication_timeout(&self) -> AuthenticationTimeout {
|
||||
self.config.authentication_timeout()
|
||||
}
|
||||
|
||||
pub fn tree(&self) -> Result<TreeModel, DesktopError> {
|
||||
let repository = self.repository()?;
|
||||
let keys = self.keys()?;
|
||||
|
||||
Reference in New Issue
Block a user