commit 5cecc46d7adab105bced8e83141316613c9884c9 Author: Yakumo Hokori Date: Wed Feb 4 22:09:54 2026 +0800 first commit diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a2ddc47 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,907 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cairo-rs" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib 0.16.9", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "system-deps", +] + +[[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 = "diskmanager" +version = "0.1.0" +dependencies = [ + "glib 0.15.12", + "gtk4", + "regex", + "serde", + "serde_json", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[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 = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05" +dependencies = [ + "bitflags", + "gdk-pixbuf-sys", + "gio", + "glib 0.16.9", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" +dependencies = [ + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2181330ebf9d091f8ea7fed6877f7adc92114128592e1fdaeb1da28e0d01e9" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib 0.16.9", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de55cb49432901fe2b3534177fa06844665b9b0911d85d8601a8d8b88b7791db" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gio" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib 0.16.9", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +dependencies = [ + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros 0.15.13", + "glib-sys 0.15.10", + "gobject-sys 0.15.10", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16aa2475c9debed5a32832cb5ff2af5a3f9e1ab9e69df58eaadc1ab2004d6eba" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros 0.16.8", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-macros" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1a9325847aa46f1e96ffea37611b9d51fc4827e67f79e7de502a297560a67b" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glib-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys 0.15.10", + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ecb4d347e6d09820df3bdfd89a74a8eec07753a06bb92a3aac3ad31d04447b" +dependencies = [ + "glib 0.16.9", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9aa82337d3972b4eafdea71e607c23f47be6f27f749aab613f1ad8ddbe6dcd6" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591239f5c52ca803b222124ac9c47f230cd180cee9b114c4d672e4a94b74f491" +dependencies = [ + "bitflags", + "cairo-rs", + "gdk4", + "glib 0.16.9", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "195a63f0be42529f98c3eb3bae0decfd0428ba2cc683b3e20ced88f340904ec5" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd89dba65def483a233dc4fdd3f3dab01576e3d83f80f6c9303ebe421661855e" +dependencies = [ + "bitflags", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib 0.16.9", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "once_cell", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42829d621396a69b352d80b952dfcb4ecb4272506b2e10a65457013af1b395a4" +dependencies = [ + "anyhow", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "gtk4-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e370564e3fdacff7cffc99f7366b6a4689feb44e819d3ccee598a9a215b71605" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pango" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94" +dependencies = [ + "bitflags", + "gio", + "glib 0.16.9", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" +dependencies = [ + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[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-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]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[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", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.7.14", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..50695ab --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "diskmanager" +version = "0.1.0" +edition = "2021" + +[dependencies] +gtk4 = "0.5" +glib = "0.15" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +regex = "1.5" diff --git a/src/application.rs b/src/application.rs new file mode 100644 index 0000000..f6f9b8b --- /dev/null +++ b/src/application.rs @@ -0,0 +1,203 @@ +use gtk4::prelude::*; +use gtk4::{Application, ApplicationWindow, Button, TextView, TreeView, Notebook, Box, Frame, + ScrolledWindow, Statusbar, Label, TreeViewColumn, CellRendererText, ListStore}; +use std::rc::Rc; +use std::cell::RefCell; + +// Data structures +#[derive(Debug, Clone)] +pub struct BlockDevice { + pub name: String, pub path: String, pub device_type: String, + pub size: String, pub fstype: String, pub mountpoint: String, + pub ro: bool, pub uuid: String, pub partuuid: String, + pub vendor: String, pub model: String, pub serial: String, + pub maj_min: String, pub pkname: String, pub children: Vec, +} + +#[derive(Debug, Clone)] +pub struct RaidArray { + pub device: String, pub level: String, pub state: String, + pub array_size: String, pub active_devices: String, pub failed_devices: String, + pub spare_devices: String, pub total_devices: String, pub uuid: String, + pub name: String, pub chunk_size: String, pub mountpoint: String, + pub member_devices: Vec, +} + +#[derive(Debug, Clone)] +pub struct RaidMember { pub device_path: String, pub state: String, pub raid_device: String } + +#[derive(Debug, Clone)] +pub struct LVMInfo { pub pvs: Vec, pub vgs: Vec, pub lvs: Vec } +#[derive(Debug, Clone)] +pub struct PV { pub pv_name: String, pub vg_name: String, pub pv_uuid: String, pub pv_size: String, pub pv_free: String, pub pv_attr: String, pub pv_fmt: String } +#[derive(Debug, Clone)] +pub struct VG { pub vg_name: String, pub vg_uuid: String, pub vg_size: String, pub vg_free: String, pub vg_attr: String, pub pv_count: String, pub lv_count: String, pub vg_alloc_percent: String, pub vg_fmt: String } +#[derive(Debug, Clone)] +pub struct LV { pub lv_name: String, pub vg_name: String, pub lv_uuid: String, pub lv_size: String, pub lv_attr: String, pub origin: String, pub snap_percent: String, pub lv_path: String, pub mountpoint: String } + +pub struct DiskManagerApp { + pub window: ApplicationWindow, + pub refresh_btn: Button, + pub tabs: Notebook, + pub tree_block: TreeView, + pub tree_raid: TreeView, + pub tree_lvm: TreeView, + pub log_out: TextView, + pub status: Statusbar, + pub block_data: Rc>>, + pub raid_data: Rc>>, + pub lvm_data: Rc>, + pub block_store: Rc>, + pub raid_store: Rc>, + pub lvm_store: Rc>, +} + +impl DiskManagerApp { + pub fn new(app: &Application) -> Self { + let window = ApplicationWindow::new(app); + window.set_title(Some("Linux 存储管理工具")); + window.set_default_size(1000, 700); + + // Main vertical box layout + let main_box = Box::new(gtk4::Orientation::Vertical, 0); + window.set_child(Some(&main_box)); + + // Refresh button + let refresh_btn = Button::with_label("刷新数据"); + main_box.append(&refresh_btn); + + // Tab widget + let tabs = Notebook::new(); + main_box.append(&tabs); + + // Tab 1: 块设备概览 + let (tree_block, store_block, scroll_block) = Self::create_block_tab(); + tabs.append_page(&scroll_block, Some(&Label::new(Some("块设备概览")))); + + // Tab 2: RAID 管理 + let (tree_raid, store_raid, scroll_raid) = Self::create_raid_tab(); + tabs.append_page(&scroll_raid, Some(&Label::new(Some("RAID 管理")))); + + // Tab 3: LVM 管理 + let (tree_lvm, store_lvm, scroll_lvm) = Self::create_lvm_tab(); + tabs.append_page(&scroll_lvm, Some(&Label::new(Some("LVM 管理")))); + + // Set tab index + tabs.set_current_page(Some(0)); + + // Log output frame + let log_frame = Frame::new(Some("日志输出")); + let log_scroll = ScrolledWindow::new(); + let log_out = TextView::new(); + log_out.set_editable(false); + log_scroll.set_child(Some(&log_out)); + log_frame.set_child(Some(&log_scroll)); + main_box.append(&log_frame); + + // Status bar + let status = Statusbar::new(); + main_box.append(&status); + + let app_state = Self { + window, + refresh_btn: refresh_btn.clone(), + tabs, + tree_block, + tree_raid, + tree_lvm, + log_out, + status, + block_data: Rc::new(RefCell::new(Vec::new())), + raid_data: Rc::new(RefCell::new(Vec::new())), + lvm_data: Rc::new(RefCell::new(LVMInfo { pvs: Vec::new(), vgs: Vec::new(), lvs: Vec::new() })), + block_store: Rc::new(RefCell::new(store_block)), + raid_store: Rc::new(RefCell::new(store_raid)), + lvm_store: Rc::new(RefCell::new(store_lvm)), + }; + + app_state.window.show(); + app_state + } + + fn create_block_tab() -> (TreeView, ListStore, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + // 13 columns for block devices + let headers = ["设备名", "类型", "大小", "挂载点", "文件系统", "只读", + "UUID", "PARTUUID", "厂商", "型号", "序列号", "主次号", "父设备名"]; + + let store = ListStore::new(&[String::static_type(); 13]); + tree.set_model(Some(&store)); + + for (i, header) in headers.iter().enumerate() { + let col = TreeViewColumn::new(); + let cell = CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(header); + col.set_resizable(true); + col.add_attribute(&cell, "text", i as i32); + tree.append_column(&col); + } + + (tree, store, scrolled) + } + + fn create_raid_tab() -> (TreeView, ListStore, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + // 12 columns for RAID arrays + let headers = ["阵列设备", "级别", "状态", "大小", "活动设备", "失败设备", + "备用设备", "总设备数", "UUID", "名称", "Chunk Size", "挂载点"]; + + let store = ListStore::new(&[String::static_type(); 12]); + tree.set_model(Some(&store)); + + for (i, header) in headers.iter().enumerate() { + let col = TreeViewColumn::new(); + let cell = CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(header); + col.set_resizable(true); + col.add_attribute(&cell, "text", i as i32); + tree.append_column(&col); + } + + (tree, store, scrolled) + } + + fn create_lvm_tab() -> (TreeView, ListStore, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + // 8 columns for LVM + let headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"]; + + let store = ListStore::new(&[String::static_type(); 8]); + tree.set_model(Some(&store)); + + for (i, header) in headers.iter().enumerate() { + let col = TreeViewColumn::new(); + let cell = CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(header); + col.set_resizable(true); + col.add_attribute(&cell, "text", i as i32); + tree.append_column(&col); + } + + (tree, store, scrolled) + } + + pub fn log(&self, msg: &str) { + let buffer = self.log_out.buffer(); + let mut end = buffer.end_iter(); + buffer.insert(&mut end, msg); + buffer.insert(&mut end, "\n"); + self.status.push(0, msg); + } +} diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..2a0e0ee --- /dev/null +++ b/src/command.rs @@ -0,0 +1,89 @@ +use std::process::{Command, Output}; +use std::io::{self, Write}; +use regex::Regex; +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct CommandResult { + pub success: bool, + pub stdout: String, + pub stderr: String, +} + +pub struct CommandExecutor; + +impl CommandExecutor { + /// Execute a shell command with optional sudo + pub fn run_command(args: &[&str], use_sudo: bool) -> CommandResult { + let mut cmd_args = if use_sudo { + vec!["sudo"] + } else { + vec![] + }; + cmd_args.extend_from_slice(args); + + let output = Command::new(&cmd_args[0]) + .args(&cmd_args[1..]) + .output(); + + match output { + Ok(o) => CommandResult { + success: o.status.success(), + stdout: String::from_utf8_lossy(&o.stdout).into_owned(), + stderr: String::from_utf8_lossy(&o.stderr).into_owned(), + }, + Err(e) => CommandResult { + success: false, + stdout: String::new(), + stderr: e.to_string(), + }, + } + } + + /// Run command and parse JSON output + pub fn run_command_json(args: &[&str], use_sudo: bool) -> Option { + let result = Self::run_command(args, use_sudo); + if result.success { + serde_json::from_str(&result.stdout).ok() + } else { + None + } + } + + /// Run command and extract value using regex + pub fn extract_value(output: &str, pattern: &str) -> Option { + let re = Regex::new(pattern).ok()?; + re.captures(output)?.get(1).map(|m| m.as_str().to_string()) + } + + /// Check if device path exists + pub fn path_exists(path: &str) -> bool { + std::path::Path::new(path).exists() + } + + /// Get real path (resolve symlinks) + pub fn realpath(path: &str) -> Option { + let result = Self::run_command(&["realpath", path], false); + if result.success && !result.stdout.trim().is_empty() { + Some(result.stdout.trim().to_string()) + } else { + None + } + } + + /// Get device major:minor in hex + pub fn stat_maj_min(path: &str) -> Option { + let result = Self::run_command(&["stat", "-c", "%t:%T", path], false); + if result.success { + let raw = result.stdout.trim(); + if raw.contains(':') { + let parts: Vec<&str> = raw.split(':').collect(); + if parts.len() == 2 { + let major_dec = i64::from_str_radix(parts[0], 16).ok()?; + return Some(format!("{}:{}", major_dec, parts[1])); + } + } + } + None + } +} diff --git a/src/dialogs.rs b/src/dialogs.rs new file mode 100644 index 0000000..5fc3b2e --- /dev/null +++ b/src/dialogs.rs @@ -0,0 +1,213 @@ +use crate::command::CommandExecutor; + +/// Simple dialog utilities using zenity or osascript +pub struct SimpleDialogs; + +impl SimpleDialogs { + pub fn question(title: &str, message: &str) -> bool { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"No\", \"Yes\"}} default button \"No\" with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let result = CommandExecutor::run_command(&["osascript", "-e", &script], false); + result.success && result.stdout.contains("Yes") + } + #[cfg(not(target_os = "macos"))] + { + let result = CommandExecutor::run_command( + &["zenity", "--question", "--title", title, "--text", message], + false + ); + result.success + } + } + + pub fn info(title: &str, message: &str) { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false); + } + #[cfg(not(target_os = "macos"))] + { + let _ = CommandExecutor::run_command( + &["zenity", "--info", "--title", title, "--text", message], + false + ); + } + } + + pub fn warning(title: &str, message: &str) { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false); + } + #[cfg(not(target_os = "macos"))] + { + let _ = CommandExecutor::run_command( + &["zenity", "--warning", "--title", title, "--text", message], + false + ); + } + } + + pub fn error(title: &str, message: &str) { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false); + } + #[cfg(not(target_os = "macos"))] + { + let _ = CommandExecutor::run_command( + &["zenity", "--error", "--title", title, "--text", message], + false + ); + } + } + + pub fn input(title: &str, message: &str, default: &str) -> Option { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" default answer \"{}\" with title \"{}\"", + message.replace("\"", "\\\""), default.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let result = CommandExecutor::run_command(&["osascript", "-e", &script], false); + if result.success && result.stdout.contains("text returned:") { + if let Some(start) = result.stdout.find("text returned:") { + let text = &result.stdout[start + "text returned:".len()..]; + return Some(text.trim().to_string()); + } + } + Some(default.to_string()) + } + #[cfg(not(target_os = "macos"))] + { + let result = CommandExecutor::run_command( + &["zenity", "--entry", "--title", title, "--text", message, "--entry-text", default], + false + ); + if result.success && !result.stdout.trim().is_empty() { + Some(result.stdout.trim().to_string()) + } else { + None + } + } + } + + pub fn select(title: &str, message: &str, items: &[&str]) -> Option<(String, usize)> { + #[cfg(target_os = "macos")] + { + let items_str = items.join("\", \""); + let script = format!( + "choose from list {{\"{}\"}} with title \"{}\" with prompt \"{}\"", + items_str, title.replace("\"", "\\\""), message.replace("\"", "\\\"") + ); + let result = CommandExecutor::run_command(&["osascript", "-e", &script], false); + if result.success && !result.stdout.trim().is_empty() && !result.stdout.trim().is_empty() { + let selected = result.stdout.trim().to_string(); + for (i, item) in items.iter().enumerate() { + if *item == selected { + return Some((selected, i)); + } + } + } + None + } + #[cfg(not(target_os = "macos"))] + { + let mut cmd = vec!["zenity", "--list", "--title", title, "--text", message, "--column", "选择"]; + for item in items { + cmd.push(item); + } + let result = CommandExecutor::run_command(&cmd, false); + if result.success && !result.stdout.trim().is_empty() { + let selected = result.stdout.trim().to_string(); + for (i, item) in items.iter().enumerate() { + if *item == selected { + return Some((selected, i)); + } + } + } + None + } + } + + pub fn scale(title: &str, message: &str, min: i32, max: i32, default: i32) -> Option { + #[cfg(target_os = "macos")] + { + // macOS doesn't have native scale dialog, use input with validation + if let Some(value) = Self::input(title, &format!("{} (范围: {}-{})", message, min, max), &default.to_string()) { + if let Ok(v) = value.parse::() { + if v >= min && v <= max { + return Some(v); + } + } + } + Some(default) + } + #[cfg(not(target_os = "macos"))] + { + let result = CommandExecutor::run_command( + &["zenity", "--scale", "--title", title, "--text", message, + "--min-value", &min.to_string(), "--max-value", &max.to_string(), + "--value", &default.to_string()], + false + ); + if result.success { + if let Ok(v) = result.stdout.trim().parse::() { + return Some(v); + } + } + None + } + } + + pub fn file_selection(title: &str, multiple: bool, directory: bool) -> Vec { + #[cfg(target_os = "macos")] + { + let script = if directory { + format!("choose folder with title \"{}\"", title.replace("\"", "\\\"")) + } else { + format!("choose file {}with title \"{}\"", + if multiple { "multiple " } else { "" }, + title.replace("\"", "\\\"")) + }; + let result = CommandExecutor::run_command(&["osascript", "-e", &script], false); + if result.success && !result.stdout.trim().is_empty() { + result.stdout.trim().split(", ").map(|s| s.to_string()).collect() + } else { + Vec::new() + } + } + #[cfg(not(target_os = "macos"))] + { + let mut cmd = vec!["zenity", "--file-selection", "--title", title]; + if multiple { + cmd.push("--multiple"); + } + if directory { + cmd.push("--directory"); + } + let result = CommandExecutor::run_command(&cmd, false); + if result.success && !result.stdout.trim().is_empty() { + result.stdout.trim().split("|").map(|s| s.to_string()).collect() + } else { + Vec::new() + } + } + } +} diff --git a/src/disk_ops.rs b/src/disk_ops.rs new file mode 100644 index 0000000..b8f349d --- /dev/null +++ b/src/disk_ops.rs @@ -0,0 +1,259 @@ +use crate::command::CommandExecutor; +use crate::system_info::SystemInfoManager; + +pub struct DiskOperations; + +impl DiskOperations { + /// Mount partition to mountpoint + pub fn mount_partition(device_path: &str, mountpoint: &str) -> bool { + std::fs::create_dir_all(mountpoint).ok(); + let result = CommandExecutor::run_command(&["mount", device_path, mountpoint], true); + result.success + } + + /// Unmount partition + pub fn unmount_partition(device_path: &str) -> bool { + let result = CommandExecutor::run_command(&["umount", device_path], true); + result.success || result.stderr.contains("not mounted") || result.stderr.contains("未挂载") + } + + /// Get disk free space info in MiB + pub fn get_disk_free_space_info_mib(disk_path: &str) -> Option<(f64, f64)> { + let result = CommandExecutor::run_command(&[ + "parted", "-s", disk_path, "unit", "MiB", "print", "free" + ], true); + + if !result.success { + return None; + } + + let mut free_spaces = Vec::new(); + for line in result.stdout.lines() { + if let Some(caps) = regex::Regex::new( + r"\s*(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(\d+\.?\d*)MiB\s+(?:Free Space|空闲空间|可用空间)" + ).ok()?.captures(line) { + if let (Some(start_str), Some(size_str)) = ( + caps.get(1).and_then(|m| m.as_str().parse::().ok()), + caps.get(3).and_then(|m| m.as_str().parse::().ok()) + ) { + free_spaces.push((start_str, size_str)); + } + } + } + + free_spaces.iter() + .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) + .map(|(s, sz)| (*s, *sz)) + } + + /// Create partition on disk + pub fn create_partition( + disk_path: &str, + partition_table_type: &str, + size_gb: f64, + total_disk_mib: f64, + use_max_space: bool + ) -> bool { + let check = CommandExecutor::run_command(&["parted", "-s", disk_path, "print"], true); + let has_partition_table = check.success && + !check.stdout.contains("Partition Table: unknown") && + !check.stdout.contains("分区表:unknown"); + + let start_mib = if !has_partition_table { + if !Dialogs::question( + "确认创建分区表", + &format!("磁盘 {} 没有分区表。您确定要创建 {} 分区表吗?此操作将擦除磁盘上的所有数据。", disk_path, partition_table_type) + ) { + return false; + } + + if !CommandExecutor::run_command(&["parted", "-s", disk_path, "mklabel", partition_table_type], true).success { + return false; + } + 1.0 + } else { + if let Some((start, _)) = Self::get_disk_free_space_info_mib(disk_path) { + if start < 1.0 { 1.0 } else { start } + } else { + return false; + } + }; + + let end_pos = if use_max_space { + "100%".to_string() + } else { + let end_mib = start_mib + size_gb * 1024.0; + if end_mib > total_disk_mib { + Dialogs::warning( + "警告", + &format!("请求的分区大小 ({:.1}GB) 超出了可用空间。将调整为最大可用空间。", size_gb) + ); + "100%".to_string() + } else { + format!("{}MiB", end_mib) + } + }; + + let cmd = &["parted", "-s", disk_path, "mkpart", "primary", &format!("{}MiB", start_mib), &end_pos]; + CommandExecutor::run_command(cmd, true).success + } + + /// Delete partition + pub fn delete_partition(device_path: &str) -> bool { + Self::unmount_partition(device_path); + + if let Ok(re) = regex::Regex::new(r"(/dev/[a-z]+)(\d+)") { + if let Some(caps) = re.captures(device_path) { + let disk_path = caps.get(1).map(|m| m.as_str()).unwrap_or(""); + let part_num = caps.get(2).map(|m| m.as_str()).unwrap_or(""); + + if !disk_path.is_empty() && !part_num.is_empty() { + return CommandExecutor::run_command( + &["parted", "-s", disk_path, "rm", part_num], + true + ).success; + } + } + } + false + } + + /// Format partition + pub fn format_partition(device_path: &str, fstype: &str) -> bool { + let cmd = match fstype { + "ext4" => vec!["mkfs.ext4", "-F", device_path], + "xfs" => vec!["mkfs.xfs", "-f", device_path], + "ntfs" => vec!["mkfs.ntfs", "-f", device_path], + "fat32" => vec!["mkfs.vfat", "-F", "32", device_path], + _ => return false, + }; + + Self::unmount_partition(device_path); + CommandExecutor::run_command(&cmd, true).success + } + + /// Get device UUID and fstype + pub fn get_device_details(device_path: &str) -> Option<(String, String)> { + let devices = SystemInfoManager::get_block_devices(); + + if let Some(dev) = SystemInfoManager::find_device_by_path(&devices, device_path) { + if !dev.fstype.is_empty() { + return Some((dev.uuid, dev.fstype)); + } + } + + if let Some(maj_min) = CommandExecutor::stat_maj_min(device_path) { + if let Some(dev) = SystemInfoManager::find_device_by_maj_min(&devices, &maj_min) { + if !dev.fstype.is_empty() { + return Some((dev.uuid, dev.fstype)); + } + } + } + + None + } + + /// Add entry to /etc/fstab + pub fn add_to_fstab(_device_path: &str, mountpoint: &str, fstype: &str, uuid: &str) -> bool { + if uuid.is_empty() || mountpoint.is_empty() || fstype.is_empty() { + return false; + } + + let entry = format!("UUID={} {} {} defaults 0 2", uuid, mountpoint, fstype); + CommandExecutor::run_command( + &["sh", "-c", &format!("echo '{}' >> /etc/fstab", entry)], + true + ).success + } + + /// Remove entry from /etc/fstab + pub fn remove_from_fstab(uuid: &str) -> bool { + CommandExecutor::run_command( + &["sed", "-i", &format!("/UUID={}/d", uuid), "/etc/fstab"], + true + ).success + } + + /// Wipe partition table + pub fn wipe_partition_table(device_path: &str) -> bool { + CommandExecutor::run_command(&["parted", "-s", device_path, "mklabel", "gpt"], true).success + } +} + +pub struct Dialogs; + +impl Dialogs { + pub fn question(title: &str, message: &str) -> bool { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"No\", \"Yes\"}} default button \"No\" with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let result = CommandExecutor::run_command(&["osascript", "-e", &script], false); + result.success && result.stdout.contains("Yes") + } + #[cfg(not(target_os = "macos"))] + { + let result = CommandExecutor::run_command( + &["zenity", "--question", "--title", title, "--text", message], + false + ); + result.success + } + } + + pub fn info(title: &str, message: &str) { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false); + } + #[cfg(not(target_os = "macos"))] + { + let _ = CommandExecutor::run_command( + &["zenity", "--info", "--title", title, "--text", message], + false + ); + } + } + + pub fn warning(title: &str, message: &str) { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false); + } + #[cfg(not(target_os = "macos"))] + { + let _ = CommandExecutor::run_command( + &["zenity", "--warning", "--title", title, "--text", message], + false + ); + } + } + + pub fn error(title: &str, message: &str) { + #[cfg(target_os = "macos")] + { + let script = format!( + "display dialog \"{}\" buttons {{\"OK\"}} with title \"{}\"", + message.replace("\"", "\\\""), title.replace("\"", "\\\"") + ); + let _ = CommandExecutor::run_command(&["osascript", "-e", &script], false); + } + #[cfg(not(target_os = "macos"))] + { + let _ = CommandExecutor::run_command( + &["zenity", "--error", "--title", title, "--text", message], + false + ); + } + } +} diff --git a/src/lvm_ops.rs b/src/lvm_ops.rs new file mode 100644 index 0000000..7c52011 --- /dev/null +++ b/src/lvm_ops.rs @@ -0,0 +1,289 @@ +use crate::command::CommandExecutor; +use crate::disk_ops::Dialogs; + +pub struct LvmOperations; + +impl LvmOperations { + fn execute_command( + args: &[&str], + error_msg: &str, + suppress_errors: Option<&[&str]> + ) -> (bool, String, String) { + let result = CommandExecutor::run_command(args, true); + let should_suppress = if let Some(patterns) = suppress_errors { + patterns.iter().any(|p| result.stderr.contains(p)) + } else { + false + }; + + if !result.success && !should_suppress { + Dialogs::error(error_msg, &result.stderr); + } + + (result.success, result.stdout, result.stderr) + } + + pub fn create_pv(device_path: &str) -> bool { + if !Dialogs::question( + "确认创建物理卷", + &format!("您确定要在设备 {} 上创建物理卷吗?\n此操作将覆盖设备上的数据。", device_path) + ) { + return false; + } + + let (success, _, stderr) = Self::execute_command( + &["pvcreate", "-y", device_path], + &format!("在 {} 上创建物理卷失败", device_path), + Some(&["device is partitioned"]) + ); + + if success { + Dialogs::info("成功", &format!("物理卷已在 {} 上创建", device_path)); + return true; + } + + if stderr.contains("device is partitioned") { + if Dialogs::question( + "设备已分区", + &format!("设备 {} 已分区。您是否要擦除分区表并将其用于物理卷?", device_path) + ) { + let (mklabel_success, _, _) = Self::execute_command( + &["parted", "-s", device_path, "mklabel", "gpt"], + &format!("擦除 {} 上的分区表失败", device_path), + None + ); + + if mklabel_success { + let (retry_success, _, _) = Self::execute_command( + &["pvcreate", "-y", device_path], + &format!("在 {} 上创建物理卷失败(重试)", device_path), + None + ); + + if retry_success { + Dialogs::info("成功", &format!("物理卷已在 {} 上创建", device_path)); + return true; + } + } + } else { + Dialogs::info("信息", "未在已分区设备上创建物理卷"); + } + } + + false + } + + pub fn delete_pv(device_path: &str) -> bool { + if !Dialogs::question( + "确认删除物理卷", + &format!("您确定要删除物理卷 {} 吗?\n如果该物理卷属于某个卷组,请先将其从卷组中移除。", device_path) + ) { + return false; + } + + let (success, _, _) = Self::execute_command( + &["pvremove", "-y", device_path], + &format!("删除物理卷 {} 失败", device_path), + None + ); + + if success { + Dialogs::info("成功", &format!("物理卷 {} 已删除", device_path)); + } + + success + } + + pub fn create_vg(vg_name: &str, pv_paths: &[String]) -> bool { + if !Dialogs::question( + "确认创建卷组", + &format!("您确定要使用物理卷 {} 创建卷组 {} 吗?", pv_paths.join(", "), vg_name) + ) { + return false; + } + + let mut cmd: Vec<&str> = vec!["vgcreate", vg_name]; + for pv in pv_paths { + cmd.push(pv); + } + + let (success, _, _) = Self::execute_command( + &cmd, + &format!("创建卷组 {} 失败", vg_name), + None + ); + + if success { + Dialogs::info("成功", &format!("卷组 {} 已创建", vg_name)); + } + + success + } + + pub fn delete_vg(vg_name: &str) -> bool { + if !Dialogs::question( + "确认删除卷组", + &format!("您确定要删除卷组 {} 吗?\n此操作将删除所有属于该卷组的逻辑卷!", vg_name) + ) { + return false; + } + + let (success, _, _) = Self::execute_command( + &["vgremove", "-y", "-f", vg_name], + &format!("删除卷组 {} 失败", vg_name), + None + ); + + if success { + Dialogs::info("成功", &format!("卷组 {} 已删除", vg_name)); + } + + success + } + + pub fn create_lv(lv_name: &str, vg_name: &str, size_gb: f64, use_max_space: bool) -> bool { + let message = if use_max_space { + format!("您确定要在卷组 {} 中创建逻辑卷 {} 吗?\n使用卷组所有可用空间。", vg_name, lv_name) + } else { + format!("您确定要在卷组 {} 中创建 {}GB 的逻辑卷 {} 吗?", vg_name, size_gb, lv_name) + }; + + if !Dialogs::question("确认创建逻辑卷", &message) { + return false; + } + + let size_str = format!("{}G", size_gb); + let cmd: Vec<&str> = if use_max_space { + vec!["lvcreate", "-y", "-l", "100%FREE", "-n", lv_name, vg_name] + } else { + vec!["lvcreate", "-y", "-L", &size_str, "-n", lv_name, vg_name] + }; + + let (success, _, _) = Self::execute_command( + &cmd, + &format!("创建逻辑卷 {}/{} 失败", vg_name, lv_name), + None + ); + + if success { + Dialogs::info("成功", &format!("逻辑卷 {}/{} 已创建", vg_name, lv_name)); + } + + success + } + + pub fn delete_lv(lv_name: &str, vg_name: &str) -> bool { + if !Dialogs::question( + "确认删除逻辑卷", + &format!("您确定要删除逻辑卷 {}/{} 吗?\n此操作将擦除所有数据!", vg_name, lv_name) + ) { + return false; + } + + let path = format!("{}/{}", vg_name, lv_name); + let (success, _, _) = Self::execute_command( + &["lvremove", "-y", &path], + &format!("删除逻辑卷 {} 失败", path), + None + ); + + if success { + Dialogs::info("成功", &format!("逻辑卷 {} 已删除", path)); + } + + success + } + + pub fn activate_lv(lv_name: &str, vg_name: &str) -> bool { + let path = format!("{}/{}", vg_name, lv_name); + let (success, _, _) = Self::execute_command( + &["lvchange", "-ay", &path], + &format!("激活逻辑卷 {} 失败", path), + None + ); + + if success { + Dialogs::info("成功", &format!("逻辑卷 {} 已激活", path)); + } + + success + } + + pub fn deactivate_lv(lv_name: &str, vg_name: &str) -> bool { + let path = format!("{}/{}", vg_name, lv_name); + let (success, _, _) = Self::execute_command( + &["lvchange", "-an", &path], + &format!("停用逻辑卷 {} 失败", path), + None + ); + + if success { + Dialogs::info("成功", &format!("逻辑卷 {} 已停用", path)); + } + + success + } + + pub fn extend_vg(vg_name: &str, pv_path: &str) -> bool { + if !Dialogs::question( + "确认扩展卷组", + &format!("您确定要将物理卷 {} 添加到卷组 {} 吗?", pv_path, vg_name) + ) { + return false; + } + + let (success, _, _) = Self::execute_command( + &["vgextend", vg_name, pv_path], + &format!("扩展卷组 {} 失败", vg_name), + None + ); + + if success { + Dialogs::info("成功", &format!("卷组 {} 已扩展", vg_name)); + } + + success + } + + pub fn reduce_vg(vg_name: &str, pv_path: &str) -> bool { + if !Dialogs::question( + "确认缩减卷组", + &format!("您确定要从卷组 {} 中移除物理卷 {} 吗?\n确保该物理卷未被使用。", vg_name, pv_path) + ) { + return false; + } + + let (success, _, _) = Self::execute_command( + &["vgreduce", vg_name, pv_path], + &format!("缩减卷组 {} 失败", vg_name), + None + ); + + if success { + Dialogs::info("成功", &format!("卷组 {} 已缩减", vg_name)); + } + + success + } + + pub fn pvmove(from_pv: &str) -> bool { + if !Dialogs::question( + "确认移动数据", + &format!("您确定要将物理卷 {} 上的数据移动到其他物理卷吗?\n此操作可能需要较长时间。", from_pv) + ) { + return false; + } + + let (success, _, _) = Self::execute_command( + &["pvmove", from_pv], + &format!("移动物理卷 {} 上的数据失败", from_pv), + None + ); + + if success { + Dialogs::info("成功", &format!("已完成移动物理卷 {} 上的数据", from_pv)); + } + + success + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2b84f9e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,354 @@ +mod command; +mod system_info; +mod disk_ops; +mod raid_ops; +mod lvm_ops; + +use gtk4::prelude::*; +use gtk4::{Application, ApplicationWindow, Button, TextView, TreeView, Notebook, Box, Frame, + ScrolledWindow, Statusbar, Label, TreeViewColumn, CellRendererText, ListStore}; +use std::rc::Rc; +use std::cell::RefCell; + +use crate::system_info::{SystemInfoManager, BlockDevice, RaidArray, LVMInfo}; + +pub struct DiskManagerApp { + window: ApplicationWindow, + refresh_btn: Button, + tabs: Notebook, + tree_block: TreeView, + tree_raid: TreeView, + tree_lvm: TreeView, + log_out: TextView, + status: Statusbar, + block_data: Rc>>, + raid_data: Rc>>, + lvm_data: Rc>, + block_store: Rc>, + raid_store: Rc>, + lvm_store: Rc>, +} + +impl DiskManagerApp { + pub fn new(app: &Application) -> Self { + let window = ApplicationWindow::new(app); + window.set_title(Some("Linux 存储管理工具")); + window.set_default_size(1000, 700); + + let main_box = Box::new(gtk4::Orientation::Vertical, 0); + window.set_child(Some(&main_box)); + + let refresh_btn = Button::with_label("刷新数据"); + main_box.append(&refresh_btn); + + let tabs = Notebook::new(); + main_box.append(&tabs); + + let (tree_block, store_block, scroll_block) = Self::create_block_tab(); + tabs.append_page(&scroll_block, Some(&Label::new(Some("块设备概览")))); + + let (tree_raid, store_raid, scroll_raid) = Self::create_raid_tab(); + tabs.append_page(&scroll_raid, Some(&Label::new(Some("RAID 管理")))); + + let (tree_lvm, store_lvm, scroll_lvm) = Self::create_lvm_tab(); + tabs.append_page(&scroll_lvm, Some(&Label::new(Some("LVM 管理")))); + + tabs.set_current_page(Some(0)); + + let log_frame = Frame::new(Some("日志输出")); + let log_scroll = ScrolledWindow::new(); + let log_out = TextView::new(); + log_out.set_editable(false); + log_scroll.set_child(Some(&log_out)); + log_frame.set_child(Some(&log_scroll)); + main_box.append(&log_frame); + + let status = Statusbar::new(); + main_box.append(&status); + + let app_state = Self { + window, + refresh_btn: refresh_btn.clone(), + tabs, + tree_block, + tree_raid, + tree_lvm, + log_out, + status, + block_data: Rc::new(RefCell::new(Vec::new())), + raid_data: Rc::new(RefCell::new(Vec::new())), + lvm_data: Rc::new(RefCell::new(LVMInfo { pvs: Vec::new(), vgs: Vec::new(), lvs: Vec::new() })), + block_store: Rc::new(RefCell::new(store_block)), + raid_store: Rc::new(RefCell::new(store_raid)), + lvm_store: Rc::new(RefCell::new(store_lvm)), + }; + + { + let app = app_state.clone(); + refresh_btn.connect_clicked(move |_| { + app.log("开始刷新所有设备信息..."); + app.refresh_block_devices(); + app.refresh_raid(); + app.refresh_lvm(); + app.log("所有设备信息刷新完成。"); + }); + } + + app_state.window.show(); + app_state + } + + fn create_block_tab() -> (TreeView, ListStore, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + let headers = ["设备名", "类型", "大小", "挂载点", "文件系统", "只读", + "UUID", "PARTUUID", "厂商", "型号", "序列号", "主次号", "父设备名"]; + + let store = ListStore::new(&[String::static_type(); 13]); + tree.set_model(Some(&store)); + + for (i, header) in headers.iter().enumerate() { + let col = TreeViewColumn::new(); + let cell = CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(header); + col.set_resizable(true); + col.add_attribute(&cell, "text", i as i32); + tree.append_column(&col); + } + + (tree, store, scrolled) + } + + fn create_raid_tab() -> (TreeView, ListStore, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + let headers = ["阵列设备", "级别", "状态", "大小", "活动设备", "失败设备", + "备用设备", "总设备数", "UUID", "名称", "Chunk Size", "挂载点"]; + + let store = ListStore::new(&[String::static_type(); 12]); + tree.set_model(Some(&store)); + + for (i, header) in headers.iter().enumerate() { + let col = TreeViewColumn::new(); + let cell = CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(header); + col.set_resizable(true); + col.add_attribute(&cell, "text", i as i32); + tree.append_column(&col); + } + + (tree, store, scrolled) + } + + fn create_lvm_tab() -> (TreeView, ListStore, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + let headers = ["名称", "大小", "属性", "UUID", "关联", "空闲/已用", "路径/格式", "挂载点"]; + + let store = ListStore::new(&[String::static_type(); 8]); + tree.set_model(Some(&store)); + + for (i, header) in headers.iter().enumerate() { + let col = TreeViewColumn::new(); + let cell = CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(header); + col.set_resizable(true); + col.add_attribute(&cell, "text", i as i32); + tree.append_column(&col); + } + + (tree, store, scrolled) + } + + pub fn log(&self, msg: &str) { + let buffer = self.log_out.buffer(); + let mut end = buffer.end_iter(); + buffer.insert(&mut end, msg); + buffer.insert(&mut end, "\n"); + self.status.push(0, msg); + } + + pub fn refresh_block_devices(&self) { + self.log("刷新块设备信息..."); + let devices = SystemInfoManager::get_block_devices(); + *self.block_data.borrow_mut() = devices; + + let store = self.block_store.borrow_mut(); + store.clear(); + + for dev in self.block_data.borrow().iter() { + Self::add_block_device(&store, dev, None); + } + + self.log("块设备信息刷新成功。"); + } + + fn add_block_device(store: &ListStore, dev: &BlockDevice, _parent: Option<>k4::TreeIter>) { + let iter = store.append(); + let ro_text = if dev.ro { "是" } else { "否" }.to_string(); + store.set(&iter, &[ + (0, &dev.name), + (1, &dev.device_type), + (2, &dev.size), + (3, &dev.mountpoint), + (4, &dev.fstype), + (5, &ro_text), + (6, &dev.uuid), + (7, &dev.partuuid), + (8, &dev.vendor), + (9, &dev.model), + (10, &dev.serial), + (11, &dev.maj_min), + (12, &dev.pkname), + ]); + + for child in &dev.children { + Self::add_block_device(store, child, Some(&iter)); + } + } + + pub fn refresh_raid(&self) { + self.log("刷新 RAID 信息..."); + let arrays = SystemInfoManager::get_mdadm_arrays(); + *self.raid_data.borrow_mut() = arrays; + + let store = self.raid_store.borrow_mut(); + store.clear(); + + for array in self.raid_data.borrow().iter() { + let iter = store.append(); + store.set(&iter, &[ + (0, &array.device), + (1, &array.level), + (2, &array.state), + (3, &array.array_size), + (4, &array.active_devices), + (5, &array.failed_devices), + (6, &array.spare_devices), + (7, &array.total_devices), + (8, &array.uuid), + (9, &array.name), + (10, &array.chunk_size), + (11, &array.mountpoint), + ]); + } + + self.log("RAID 信息刷新成功。"); + } + + pub fn refresh_lvm(&self) { + self.log("刷新 LVM 信息..."); + let info = SystemInfoManager::get_lvm_info(); + *self.lvm_data.borrow_mut() = info; + + let store = self.lvm_store.borrow_mut(); + store.clear(); + + let info = self.lvm_data.borrow(); + + // PVs section + let pv_iter = store.append(); + store.set(&pv_iter, &[ + (0, &"物理卷 (PVs)"), (1, &""), (2, &""), (3, &""), (4, &""), (5, &""), (6, &""), (7, &"") + ]); + + for pv in &info.pvs { + let iter = store.append(); + store.set(&iter, &[ + (0, &pv.pv_name), + (1, &pv.pv_size), + (2, &pv.pv_attr), + (3, &pv.pv_uuid), + (4, &format!("VG: {}", pv.vg_name)), + (5, &format!("空闲: {}", pv.pv_free)), + (6, &pv.pv_fmt), + (7, &""), + ]); + } + + // VGs section + let vg_iter = store.append(); + store.set(&vg_iter, &[ + (0, &"卷组 (VGs)"), (1, &""), (2, &""), (3, &""), (4, &""), (5, &""), (6, &""), (7, &"") + ]); + + for vg in &info.vgs { + let iter = store.append(); + store.set(&iter, &[ + (0, &vg.vg_name), + (1, &vg.vg_size), + (2, &vg.vg_attr), + (3, &vg.vg_uuid), + (4, &format!("PVs: {}, LVs: {}", vg.pv_count, vg.lv_count)), + (5, &format!("空闲: {}, 已分配: {}%", vg.vg_free, vg.vg_alloc_percent)), + (6, &vg.vg_fmt), + (7, &""), + ]); + } + + // LVs section + let lv_iter = store.append(); + store.set(&lv_iter, &[ + (0, &"逻辑卷 (LVs)"), (1, &""), (2, &""), (3, &""), (4, &""), (5, &""), (6, &""), (7, &"") + ]); + + for lv in &info.lvs { + let iter = store.append(); + store.set(&iter, &[ + (0, &lv.lv_name), + (1, &lv.lv_size), + (2, &lv.lv_attr), + (3, &lv.lv_uuid), + (4, &format!("VG: {}, Origin: {}", lv.vg_name, lv.origin)), + (5, &format!("快照: {}%", lv.snap_percent)), + (6, &lv.lv_path), + (7, &lv.mountpoint), + ]); + } + + self.log("LVM 信息刷新成功。"); + } +} + +impl Clone for DiskManagerApp { + fn clone(&self) -> Self { + Self { + window: self.window.clone(), + refresh_btn: self.refresh_btn.clone(), + tabs: self.tabs.clone(), + tree_block: self.tree_block.clone(), + tree_raid: self.tree_raid.clone(), + tree_lvm: self.tree_lvm.clone(), + log_out: self.log_out.clone(), + status: self.status.clone(), + block_data: self.block_data.clone(), + raid_data: self.raid_data.clone(), + lvm_data: self.lvm_data.clone(), + block_store: self.block_store.clone(), + raid_store: self.raid_store.clone(), + lvm_store: self.lvm_store.clone(), + } + } +} + +fn main() { + let app = Application::new( + Some("com.diskmanager.app"), + Default::default(), + ); + + app.connect_activate(|app| { + let _window = DiskManagerApp::new(app); + }); + + app.run(); +} diff --git a/src/raid_ops.rs b/src/raid_ops.rs new file mode 100644 index 0000000..31d91e6 --- /dev/null +++ b/src/raid_ops.rs @@ -0,0 +1,246 @@ +use crate::command::CommandExecutor; +use crate::system_info::{SystemInfoManager, RaidArray}; +use crate::disk_ops::Dialogs; + +pub struct RaidOperations; + +impl RaidOperations { + fn execute_command(args: &[&str], error_msg: &str, suppress_errors: Option<&[&str]>)-> (bool, String, String) { + let result = CommandExecutor::run_command(args, true); + let should_suppress = if let Some(patterns) = suppress_errors { + patterns.iter().any(|p| result.stderr.contains(p)) + } else { + false + }; + + if !result.success && !should_suppress { + Dialogs::error(error_msg, &result.stderr); + } + + (result.success, result.stdout, result.stderr) + } + + fn get_next_md_name() -> String { + let mut existing = std::collections::HashSet::new(); + let arrays = SystemInfoManager::get_mdadm_arrays(); + + for array in arrays { + if let Some(name) = array.device.strip_prefix("/dev/md") { + if let Ok(num) = name.parse::() { + existing.insert(num); + } + } + } + + let mut num = 0u32; + while existing.contains(&num) { + num += 1; + } + format!("/dev/md{}", num) + } + + pub fn create_raid_array(devices: &[String], level: &str, chunk_size: &str) -> bool { + let device_count = devices.len(); + match level { + "raid0" | "0" => if device_count < 1 { + Dialogs::error("错误", "RAID0 至少需要1个设备"); + return false; + }, + "raid1" | "1" => if device_count < 2 { + Dialogs::error("错误", "RAID1 至少需要2个设备"); + return false; + }, + "raid5" | "5" => if device_count < 3 { + Dialogs::error("错误", "RAID5 至少需要3个设备"); + return false; + }, + "raid6" | "6" => if device_count < 4 { + Dialogs::error("错误", "RAID6 至少需要4个设备"); + return false; + }, + "raid10" | "10" => if device_count < 2 { + Dialogs::error("错误", "RAID10 至少需要2个设备"); + return false; + }, + _ => {}, + } + + if !Dialogs::question( + "确认创建 RAID 阵列", + &format!("您确定要使用设备 {} 创建 RAID {} 阵列吗?\n此操作将销毁设备上的所有数据!", + devices.join(", "), level) + ) { + return false; + } + + for dev in devices { + let (success, _, stderr) = Self::execute_command( + &["mdadm", "--zero-superblock", "--force", dev], + &format!("清除设备 {} 上的 RAID 超级块失败", dev), + Some(&["Unrecognised md component device"]) + ); + + if !success && !stderr.contains("Unrecognised md component device") { + return false; + } + } + + let array_name = Self::get_next_md_name(); + let level_num = level.trim_start_matches("raid"); + + let mut cmd: Vec = vec!["mdadm".to_string(), "--create".to_string(), array_name.clone(), + format!("--level={}", level_num), format!("--raid-devices={}", device_count)]; + + if ["raid0", "raid5", "raid6", "raid10", "0", "5", "6", "10"].contains(&level) { + cmd.push(format!("--chunk={}K", chunk_size)); + } + + cmd.push("--force".to_string()); + for dev in devices { + cmd.push(dev.clone()); + } + + let cmd_args: Vec<&str> = cmd.iter().map(|s| s.as_str()).collect(); + let (success, stdout, _) = Self::execute_command(&cmd_args, "创建 RAID 阵列失败", None); + + if success { + Dialogs::info("成功", &format!("成功创建 RAID {} 阵列 {}", level, array_name)); + + let (scan_success, scan_stdout, _) = Self::execute_command( + &["mdadm", "--examine", "--scan"], + "扫描 mdadm 配置失败", + None + ); + + if scan_success { + let _ = CommandExecutor::run_command(&["mkdir", "-p", "/etc"], false); + let _ = CommandExecutor::run_command( + &["sh", "-c", &format!("echo '{}' | tee -a /etc/mdadm.conf", scan_stdout.trim())], + true + ); + } + + true + } else { + if stdout.contains("Array name") && stdout.contains("is in use already") { + Dialogs::error("错误", "阵列名称已被占用,请尝试停止或删除现有阵列"); + } + false + } + } + + pub fn stop_raid_array(array_path: &str) -> bool { + if !Dialogs::question( + "确认停止 RAID 阵列", + &format!("您确定要停止 RAID 阵列 {} 吗?\n停止阵列将使其无法访问。", array_path) + ) { + return false; + } + + let _ = CommandExecutor::run_command(&["umount", array_path], true); + + let (success, _, _) = Self::execute_command( + &["mdadm", "--stop", array_path], + &format!("停止 RAID 阵列 {} 失败", array_path), + Some(&["not mounted", "未挂载"]) + ); + + if success { + Dialogs::info("成功", &format!("成功停止 RAID 阵列 {}", array_path)); + } + + success + } + + pub fn delete_active_raid_array(array: &RaidArray) -> bool { + if !Dialogs::question( + "确认删除 RAID 阵列", + &format!("您确定要删除 RAID 阵列 {} 吗?\n此操作将停止阵列并清除成员设备上的数据!", array.device) + ) { + return false; + } + + if !Self::stop_raid_array(&array.device) { + Dialogs::error("错误", "无法停止阵列"); + return false; + } + + let mut all_cleared = true; + for member in &array.member_devices { + let (success, _, stderr) = Self::execute_command( + &["mdadm", "--zero-superblock", "--force", &member.device_path], + &format!("清除设备 {} 上的 RAID 超级块失败", member.device_path), + Some(&["Unrecognised md component device"]) + ); + + if !success && !stderr.contains("Unrecognised md component device") { + all_cleared = false; + } + } + + let _ = Self::delete_config_from_mdadm_conf(&array.uuid); + + if all_cleared { + Dialogs::info("成功", &format!("成功删除 RAID 阵列 {}", array.device)); + } else { + Dialogs::error("错误", "删除完成,但部分设备超级块未能完全清除"); + } + + true + } + + pub fn delete_configured_raid_array(uuid: &str) -> bool { + if !Dialogs::question( + "确认删除 RAID 阵列配置", + &format!("您确定要删除 UUID 为 {} 的 RAID 阵列配置吗?\n此操作只删除配置文件中的条目。", uuid) + ) { + return false; + } + + let result = Self::delete_config_from_mdadm_conf(uuid); + + if result { + Dialogs::info("成功", "成功删除 RAID 阵列配置"); + } + + result + } + + fn delete_config_from_mdadm_conf(uuid: &str) -> bool { + for conf_path in ["/etc/mdadm/mdadm.conf", "/etc/mdadm.conf"] { + if CommandExecutor::path_exists(conf_path) { + let content = CommandExecutor::run_command(&["cat", conf_path], false); + if content.success { + let lines: Vec<&str> = content.stdout.lines() + .filter(|line| !line.trim_start().starts_with("ARRAY") || + !line.contains(uuid)) + .collect(); + + if lines.len() != content.stdout.lines().count() { + let new_content = lines.join("\n"); + let _ = CommandExecutor::run_command( + &["sh", "-c", &format!("echo '{}' > {}", new_content, conf_path)], + true + ); + return true; + } + } + } + } + false + } + + pub fn activate_raid_array(array_path: &str, uuid: &str) -> bool { + let (success, _, _) = Self::execute_command( + &["mdadm", "--assemble", array_path, "--uuid", uuid], + &format!("激活 RAID 阵列 {} 失败", array_path), + Some(&["already active"]) + ); + + if success { + Dialogs::info("成功", &format!("成功激活 RAID 阵列 {}", array_path)); + } + + success + } +} diff --git a/src/system_info.rs b/src/system_info.rs new file mode 100644 index 0000000..fe49d52 --- /dev/null +++ b/src/system_info.rs @@ -0,0 +1,421 @@ +use crate::command::CommandExecutor; +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct BlockDevice { + pub name: String, + pub path: String, + pub device_type: String, + pub size: String, + pub fstype: String, + pub mountpoint: String, + pub ro: bool, + pub uuid: String, + pub partuuid: String, + pub vendor: String, + pub model: String, + pub serial: String, + pub maj_min: String, + pub pkname: String, + pub children: Vec, +} + +#[derive(Debug, Clone)] +pub struct RaidMember { + pub number: String, + pub major: String, + pub minor: String, + pub raid_device: String, + pub state: String, + pub device_path: String, +} + +#[derive(Debug, Clone)] +pub struct RaidArray { + pub device: String, + pub level: String, + pub state: String, + pub array_size: String, + pub active_devices: String, + pub failed_devices: String, + pub spare_devices: String, + pub total_devices: String, + pub uuid: String, + pub name: String, + pub chunk_size: String, + pub mountpoint: String, + pub member_devices: Vec, +} + +#[derive(Debug, Clone)] +pub struct PV { + pub pv_name: String, + pub vg_name: String, + pub pv_uuid: String, + pub pv_size: String, + pub pv_free: String, + pub pv_attr: String, + pub pv_fmt: String, +} + +#[derive(Debug, Clone)] +pub struct VG { + pub vg_name: String, + pub vg_uuid: String, + pub vg_size: String, + pub vg_free: String, + pub vg_attr: String, + pub pv_count: String, + pub lv_count: String, + pub vg_alloc_percent: String, + pub vg_fmt: String, +} + +#[derive(Debug, Clone)] +pub struct LV { + pub lv_name: String, + pub vg_name: String, + pub lv_uuid: String, + pub lv_size: String, + pub lv_attr: String, + pub origin: String, + pub snap_percent: String, + pub lv_path: String, + pub mountpoint: String, +} + +#[derive(Debug, Clone)] +pub struct LVMInfo { + pub pvs: Vec, + pub vgs: Vec, + pub lvs: Vec, +} + +pub struct SystemInfoManager; + +impl SystemInfoManager { + /// Get all block devices using lsblk + pub fn get_block_devices() -> Vec { + let result = CommandExecutor::run_command(&[ + "lsblk", "-J", "-o", + "NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME,PATH" + ], false); + + if let Some(data) = CommandExecutor::run_command_json(&[ + "lsblk", "-J", "-o", + "NAME,FSTYPE,SIZE,MOUNTPOINT,RO,TYPE,UUID,PARTUUID,VENDOR,MODEL,SERIAL,MAJ:MIN,PKNAME,PATH" + ], false) { + if let Some(devs) = data.get("blockdevices").and_then(|v| v.as_array()) { + return devs.iter().filter_map(|d| Self::parse_device(d)).collect(); + } + } + Vec::new() + } + + fn parse_device(d: &Value) -> Option { + let name = d.get("NAME")?.as_str()?.to_string(); + Some(BlockDevice { + name: name.clone(), + path: d.get("PATH").and_then(|v| v.as_str()).unwrap_or(&format!("/dev/{}", name)).to_string(), + device_type: d.get("TYPE")?.as_str()?.to_string(), + size: d.get("SIZE")?.as_str()?.to_string(), + fstype: d.get("FSTYPE")?.as_str()?.to_string(), + mountpoint: d.get("MOUNTPOINT")?.as_str()?.to_string(), + ro: d.get("RO")?.as_u64()? == 1, + uuid: d.get("UUID")?.as_str()?.to_string(), + partuuid: d.get("PARTUUID")?.as_str()?.to_string(), + vendor: d.get("VENDOR")?.as_str()?.to_string(), + model: d.get("MODEL")?.as_str()?.to_string(), + serial: d.get("SERIAL")?.as_str()?.to_string(), + maj_min: d.get("MAJ:MIN")?.as_str()?.to_string(), + pkname: d.get("PKNAME")?.as_str()?.to_string(), + children: d.get("children").and_then(|v| v.as_array()) + .map(|c| c.iter().filter_map(|d| Self::parse_device(d)).collect()) + .unwrap_or_default(), + }) + } + + /// Find device by path recursively + pub fn find_device_by_path(devices: &[BlockDevice], target_path: &str) -> Option { + for dev in devices { + if dev.path == target_path { + return Some(dev.clone()); + } + if let Some(found) = Self::find_in_children(&dev.children, target_path) { + return Some(found); + } + } + None + } + + fn find_in_children(children: &[BlockDevice], target_path: &str) -> Option { + for child in children { + if child.path == target_path { + return Some(child.clone()); + } + if let Some(found) = Self::find_in_children(&child.children, target_path) { + return Some(found); + } + } + None + } + + /// Find device by maj:min + pub fn find_device_by_maj_min(devices: &[BlockDevice], target_maj_min: &str) -> Option { + for dev in devices { + if dev.maj_min == target_maj_min { + return Some(dev.clone()); + } + if let Some(found) = Self::find_maj_min_in_children(&dev.children, target_maj_min) { + return Some(found); + } + } + None + } + + fn find_maj_min_in_children(children: &[BlockDevice], target_maj_min: &str) -> Option { + for child in children { + if child.maj_min == target_maj_min { + return Some(child.clone()); + } + if let Some(found) = Self::find_maj_min_in_children(&child.children, target_maj_min) { + return Some(found); + } + } + None + } + + /// Get mountpoint for device + pub fn get_mountpoint_for_device(device_path: &str) -> Option { + let devices = Self::get_block_devices(); + + // Try direct path lookup + if let Some(dev) = Self::find_device_by_path(&devices, device_path) { + if !dev.mountpoint.is_empty() && dev.mountpoint != "[SWAP]" { + return Some(dev.mountpoint); + } + } + + // Try resolving symlink + if let Some(resolved) = CommandExecutor::realpath(device_path) { + if let Some(dev) = Self::find_device_by_path(&devices, &resolved) { + if !dev.mountpoint.is_empty() && dev.mountpoint != "[SWAP]" { + return Some(dev.mountpoint); + } + } + } + + // Try by maj:min + if let Some(maj_min) = CommandExecutor::stat_maj_min(device_path) { + if let Some(dev) = Self::find_device_by_maj_min(&devices, &maj_min) { + if !dev.mountpoint.is_empty() && dev.mountpoint != "[SWAP]" { + return Some(dev.mountpoint); + } + } + } + + None + } + + /// Get all RAID arrays + pub fn get_mdadm_arrays() -> Vec { + let mut arrays = Vec::new(); + + // Get active arrays from mdadm --detail --scan + let scan_result = CommandExecutor::run_command(&["mdadm", "--detail", "--scan"], true); + if scan_result.success { + for line in scan_result.stdout.lines() { + if line.starts_with("ARRAY") { + if let Some((device, uuid)) = Self::parse_array_line(line) { + let detail = CommandExecutor::run_command(&["mdadm", "--detail", &device], true); + if detail.success { + if let Some(array) = Self::parse_mdadm_detail(&device, &uuid, &detail.stdout) { + arrays.push(array); + } + } + } + } + } + } + + arrays + } + + fn parse_array_line(line: &str) -> Option<(String, String)> { + let re = regex::Regex::new(r"ARRAY\s+(\S+)(?:\s+\S+=\S+)*\s+UUID=([0-9a-f:]+)").ok()?; + let caps = re.captures(line)?; + Some(( + caps.get(1)?.as_str().to_string(), + caps.get(2)?.as_str().to_string(), + )) + } + + fn parse_mdadm_detail(device: &str, uuid: &str, output: &str) -> Option { + let name = CommandExecutor::extract_value(output, r"Name\s*:\s*(.+)") + .unwrap_or_else(|| std::path::Path::new(device).file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| device.to_string())); + + let state = CommandExecutor::extract_value(output, r"State\s*:\s*(.+)") + .unwrap_or_else(|| { + if output.to_lowercase().contains("clean") && output.to_lowercase().contains("active") { + "Active, Clean".to_string() + } else if output.to_lowercase().contains("degraded") { + "Degraded".to_string() + } else { + "N/A".to_string() + } + }); + + let mut members = Vec::new(); + let re = regex::Regex::new(r"\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+([^\s]+(?:\s+[^\s]+)*)\s+(/dev/\S+)").ok()?; + for line in output.lines() { + if let Some(caps) = re.captures(line) { + members.push(RaidMember { + number: caps.get(1)?.as_str().to_string(), + major: caps.get(2)?.as_str().to_string(), + minor: caps.get(3)?.as_str().to_string(), + raid_device: caps.get(4)?.as_str().to_string(), + state: caps.get(5)?.as_str().to_string().trim().to_string(), + device_path: caps.get(6)?.as_str().to_string(), + }); + } + } + + let mountpoint = Self::get_mountpoint_for_device(device); + + Some(RaidArray { + device: device.to_string(), + level: CommandExecutor::extract_value(output, r"Raid Level\s*:\s*(.+)") + .unwrap_or_else(|| "N/A".to_string()), + state, + array_size: CommandExecutor::extract_value(output, r"Array Size\s*:\s*(.+)") + .unwrap_or_else(|| "N/A".to_string()), + active_devices: CommandExecutor::extract_value(output, r"Active Devices\s*:\s*(.+)") + .unwrap_or_else(|| "N/A".to_string()), + failed_devices: CommandExecutor::extract_value(output, r"Failed Devices\s*:\s*(.+)") + .unwrap_or_else(|| "N/A".to_string()), + spare_devices: CommandExecutor::extract_value(output, r"Spare Devices\s*:\s*(.+)") + .unwrap_or_else(|| "N/A".to_string()), + total_devices: CommandExecutor::extract_value(output, r"Raid Devices\s*:\s*(.+)") + .unwrap_or_else(|| "N/A".to_string()), + uuid: uuid.to_string(), + name, + chunk_size: CommandExecutor::extract_value(output, r"Chunk Size\s*:\s*(.+)") + .unwrap_or_else(|| "N/A".to_string()), + mountpoint: mountpoint.unwrap_or_default(), + member_devices: members, + }) + } + + /// Get LVM info + pub fn get_lvm_info() -> LVMInfo { + let mut info = LVMInfo { pvs: Vec::new(), vgs: Vec::new(), lvs: Vec::new() }; + + // Get PVs + if let Some(data) = CommandExecutor::run_command_json(&["pvs", "--reportformat", "json"], false) { + if let Some(report) = data.get("report").and_then(|v| v.as_array()).and_then(|v| v.first()) { + if let Some(pvs) = report.get("pv").and_then(|v| v.as_array()) { + for p in pvs { + info.pvs.push(PV { + pv_name: p.get("pv_name").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_name: p.get("vg_name").and_then(|v| v.as_str()).unwrap_or("").to_string(), + pv_uuid: p.get("pv_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(), + pv_size: p.get("pv_size").and_then(|v| v.as_str()).unwrap_or("").to_string(), + pv_free: p.get("pv_free").and_then(|v| v.as_str()).unwrap_or("").to_string(), + pv_attr: p.get("pv_attr").and_then(|v| v.as_str()).unwrap_or("").to_string(), + pv_fmt: p.get("pv_fmt").and_then(|v| v.as_str()).unwrap_or("").to_string(), + }); + } + } + } + } + + // Get VGs + if let Some(data) = CommandExecutor::run_command_json(&["vgs", "--reportformat", "json"], false) { + if let Some(report) = data.get("report").and_then(|v| v.as_array()).and_then(|v| v.first()) { + if let Some(vgs) = report.get("vg").and_then(|v| v.as_array()) { + for v in vgs { + info.vgs.push(VG { + vg_name: v.get("vg_name").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_uuid: v.get("vg_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_size: v.get("vg_size").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_free: v.get("vg_free").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_attr: v.get("vg_attr").and_then(|v| v.as_str()).unwrap_or("").to_string(), + pv_count: v.get("pv_count").and_then(|v| v.as_str()).unwrap_or("").to_string(), + lv_count: v.get("lv_count").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_alloc_percent: v.get("vg_alloc_percent").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_fmt: v.get("vg_fmt").and_then(|v| v.as_str()).unwrap_or("").to_string(), + }); + } + } + } + } + + // Get LVs + if let Some(data) = CommandExecutor::run_command_json(&[ + "lvs", "-o", "lv_name,vg_name,lv_uuid,lv_size,lv_attr,origin,snap_percent,lv_path", + "--reportformat", "json" + ], false) { + if let Some(report) = data.get("report").and_then(|v| v.as_array()).and_then(|v| v.first()) { + if let Some(lvs) = report.get("lv").and_then(|v| v.as_array()) { + for l in lvs { + let lv_path = l.get("lv_path").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let mountpoint = Self::get_mountpoint_for_device(&lv_path); + + info.lvs.push(LV { + lv_name: l.get("lv_name").and_then(|v| v.as_str()).unwrap_or("").to_string(), + vg_name: l.get("vg_name").and_then(|v| v.as_str()).unwrap_or("").to_string(), + lv_uuid: l.get("lv_uuid").and_then(|v| v.as_str()).unwrap_or("").to_string(), + lv_size: l.get("lv_size").and_then(|v| v.as_str()).unwrap_or("").to_string(), + lv_attr: l.get("lv_attr").and_then(|v| v.as_str()).unwrap_or("").to_string(), + origin: l.get("origin").and_then(|v| v.as_str()).unwrap_or("").to_string(), + snap_percent: l.get("snap_percent").and_then(|v| v.as_str()).unwrap_or("").to_string(), + lv_path, + mountpoint: mountpoint.unwrap_or_default(), + }); + } + } + } + } + + info + } + + /// Get unallocated partitions + pub fn get_unallocated_partitions() -> Vec { + let mut candidates = Vec::new(); + let devices = Self::get_block_devices(); + + fn process_dev(dev: &BlockDevice, candidates: &mut Vec) { + let mountpoint = &dev.mountpoint; + let fstype = &dev.fstype; + + if !mountpoint.is_empty() && mountpoint != "[SWAP]" { + for child in &dev.children { process_dev(child, candidates); } + return; + } + + if fstype == "LVM2_member" || fstype == "linux_raid_member" { + for child in &dev.children { process_dev(child, candidates); } + return; + } + + if dev.device_type == "disk" && dev.children.is_empty() { + candidates.push(dev.path.clone()); + } else if dev.device_type == "part" { + candidates.push(dev.path.clone()); + } + + if dev.device_type == "disk" && !dev.children.is_empty() { + for child in &dev.children { process_dev(child, candidates); } + } + } + + for dev in &devices { process_dev(dev, &mut candidates); } + + candidates.sort(); + candidates.dedup(); + candidates + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..f9df9e0 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,150 @@ +use gtk4::{prelude::*, Box, Button, TextView, TreeView, Notebook, Statusbar, + ScrolledWindow, Frame, Label, ApplicationWindow}; +use std::rc::Rc; + +pub struct DiskManagerUI { + pub main_container: Box, + pub refresh_button: Button, + pub tab_widget: Notebook, + pub tree_block_devices: TreeView, + pub tree_raid: TreeView, + pub tree_lvm: TreeView, + pub log_output: TextView, + pub statusbar: Statusbar, +} + +impl DiskManagerUI { + pub fn new() -> Rc { + // Main vertical box + let main_container = Box::new(gtk4::Orientation::Vertical, 0); + + // Refresh button + let refresh_button = Button::with_label("刷新数据"); + main_container.append(&refresh_button); + + // Tab widget + let tab_widget = Notebook::new(); + main_container.append(&tab_widget); + + // Tab 1: 块设备概览 + let (tree_block_devices, scroll_block) = Self::create_block_device_tab(); + tab_widget.append_page(&scroll_block, Some(&Label::new(Some("块设备概览")))); + + // Tab 2: RAID 管理 + let (tree_raid, scroll_raid) = Self::create_raid_tab(); + tab_widget.append_page(&scroll_raid, Some(&Label::new(Some("RAID 管理")))); + + // Tab 3: LVM 管理 + let (tree_lvm, scroll_lvm) = Self::create_lvm_tab(); + tab_widget.append_page(&scroll_lvm, Some(&Label::new(Some("LVM 管理")))); + + tab_widget.set_current_page(Some(0)); + + // Log output frame + let log_frame = Frame::new(Some("日志输出")); + let log_scroll = ScrolledWindow::new(); + let log_output = TextView::new(); + log_output.set_editable(false); + log_scroll.set_child(Some(&log_output)); + log_frame.set_child(Some(&log_scroll)); + main_container.append(&log_frame); + + // Status bar + let statusbar = Statusbar::new(); + main_container.append(&statusbar); + + Rc::new(Self { + main_container, + refresh_button, + tab_widget, + tree_block_devices, + tree_raid, + tree_lvm, + log_output, + statusbar, + }) + } + + fn create_block_device_tab() -> (TreeView, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + // 13 columns + let columns = [ + ("设备名", 0), ("类型", 1), ("大小", 2), ("挂载点", 3), + ("文件系统", 4), ("只读", 5), ("UUID", 6), ("PARTUUID", 7), + ("厂商", 8), ("型号", 9), ("序列号", 10), ("主次号", 11), ("父设备名", 12) + ]; + + for (title, idx) in columns.iter() { + let col = gtk4::TreeViewColumn::new(); + let cell = gtk4::CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(title); + col.set_resizable(true); + col.add_attribute(&cell, "text", *idx); + tree.append_column(&col); + } + + (tree, scrolled) + } + + fn create_raid_tab() -> (TreeView, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + // 12 columns + let columns = [ + ("阵列设备", 0), ("级别", 1), ("状态", 2), ("大小", 3), + ("活动设备", 4), ("失败设备", 5), ("备用设备", 6), + ("总设备数", 7), ("UUID", 8), ("名称", 9), + ("Chunk Size", 10), ("挂载点", 11) + ]; + + for (title, idx) in columns.iter() { + let col = gtk4::TreeViewColumn::new(); + let cell = gtk4::CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(title); + col.set_resizable(true); + col.add_attribute(&cell, "text", *idx); + tree.append_column(&col); + } + + (tree, scrolled) + } + + fn create_lvm_tab() -> (TreeView, ScrolledWindow) { + let scrolled = ScrolledWindow::new(); + let tree = TreeView::new(); + scrolled.set_child(Some(&tree)); + + // 8 columns + let columns = [ + ("名称", 0), ("大小", 1), ("属性", 2), ("UUID", 3), + ("关联", 4), ("空闲/已用", 5), ("路径/格式", 6), ("挂载点", 7) + ]; + + for (title, idx) in columns.iter() { + let col = gtk4::TreeViewColumn::new(); + let cell = gtk4::CellRendererText::new(); + col.pack_start(&cell, true); + col.set_title(title); + col.set_resizable(true); + col.add_attribute(&cell, "text", *idx); + tree.append_column(&col); + } + + (tree, scrolled) + } + + pub fn log(&self, message: &str) { + let buffer = self.log_output.buffer(); + let mut end = buffer.end_iter(); + buffer.insert(&mut end, message); + buffer.insert(&mut end, "\n"); + self.statusbar.push(0, message); + } +}