From 0922c81c0adca69212542f1d3fd5803392aca2ba Mon Sep 17 00:00:00 2001 From: Dmitry Akimov Date: Fri, 31 Oct 2025 18:28:04 +0300 Subject: [PATCH] # tui_menu 0.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Переход на Cursive 2. Менеджер навигации по меню: вызывающая сторона может осуществлять навигацию по меню вне Cursive event loop, например, в потоке обработчика внешнего прерывания GPIO --- .DS_Store | Bin 6148 -> 14340 bytes Cargo.lock | 825 ++++++++++++++------------- config_lib/.DS_Store | Bin 0 -> 6148 bytes config_lib/src/device_config.rs | 24 +- menu_tui/.DS_Store | Bin 0 -> 8196 bytes menu_tui/Cargo.toml | 8 +- menu_tui/TODO.md | 9 + menu_tui/assets/style.toml | 25 + menu_tui/examples/simple.rs | 79 ++- menu_tui/examples/simple_config.toml | 28 +- menu_tui/src/.DS_Store | Bin 0 -> 6148 bytes menu_tui/src/lib.rs | 5 +- menu_tui/src/menu_navigation.rs | 165 ++++++ menu_tui/src/menu_process.rs | 146 +++++ menu_tui/src/menu_rendering.rs | 136 ----- menu_tui/src/user_parameters.rs | 89 +++ menu_tui/src/views/config_view.rs | 89 +++ menu_tui/src/views/exit_view.rs | 6 + menu_tui/src/views/main_view.rs | 45 ++ menu_tui/src/views/mod.rs | 4 + menu_tui/src/views/update_view.rs | 6 + rk_nku_menu/.DS_Store | Bin 0 -> 6148 bytes rk_nku_menu/Cargo.toml | 7 +- rk_nku_menu/assets/style.toml | 25 + rk_nku_menu/src/main.rs | 206 +++---- 25 files changed, 1247 insertions(+), 680 deletions(-) create mode 100644 config_lib/.DS_Store create mode 100644 menu_tui/.DS_Store create mode 100644 menu_tui/TODO.md create mode 100644 menu_tui/assets/style.toml create mode 100644 menu_tui/src/.DS_Store create mode 100644 menu_tui/src/menu_navigation.rs create mode 100644 menu_tui/src/menu_process.rs delete mode 100644 menu_tui/src/menu_rendering.rs create mode 100644 menu_tui/src/user_parameters.rs create mode 100644 menu_tui/src/views/config_view.rs create mode 100644 menu_tui/src/views/exit_view.rs create mode 100644 menu_tui/src/views/main_view.rs create mode 100644 menu_tui/src/views/mod.rs create mode 100644 menu_tui/src/views/update_view.rs create mode 100644 rk_nku_menu/.DS_Store create mode 100644 rk_nku_menu/assets/style.toml diff --git a/.DS_Store b/.DS_Store index ca2b7874ee44cef1857d5487e8c8184223131c55..0409a9b9906e2575a708a01b89bd5ae3a5431406 100644 GIT binary patch literal 14340 zcmeHOTWl0n7(V|{%52-Q!_{)x>5>%`Y+y^Fg0#53078Wlc1ySAvhMB-Wy0)C-I?93 zVzo%T1RuP{2V){?@JZ2_7>P>M#0SJ%VuFa5Z^A!okq>TY*s~Sz?s0U&}3Yq%9|nOFNw&v`p8FM7{@UO2yR5 zX>y>dx<hpyU!_Z$~gA6oKY~1VsFN=@>$zWs?#$m zE0ecUp3!Dmv@_Ptpk2BLJ=3w>9Y)SYNiUJfIhM7}aZTFLm+;E2eo<4R-*s|c2eXh= z)x2wLH_(7tFgnqUn=o-4)KWNl3wM0rSZ!Ts(UN6Nt6SSPY~H&4v^ssp%vs7DrQg&I z+cWo?My@aAW(-?5?SY=6seAi+3#MzPEhBEHvPNJ!Gz3nobLY)p5Y#l@;etY`Ya*XE z@5!6qy=qvNnT>^7SlOlHptd+VwW7=F+jt$czE}%uJ84Tt&cIan6_#oZO5DY<%W8^u zT&^@K@%^T)2UM5WtW=t{9xIhLtbnR>J8P7+T5o}SGN5+x%5};dEs=4uS)8I9;C1a< zr?!Why({H<-Ghcrhjfy+ZN#>IcV8-JbQf(S=Z~lhYuWw9daATuH$8_nqBpw3n$|}z z*QV=i4)f>EQ?Rzx;fDx zlX|k0w2)4cz^C;=@)&uZ93{ueN8}Xwlzc~iB)^k$0A!xCtO+o1(o zVI6FQIP^d-Bp?M@u)%>L7={O61P;Pscmke;r{NiR0bYhz;3&KbZ@@cn9NvZZ;A8k4 zzJM>`8#oI;!7uPD{0aX`L;|TwS|9}_O}bfXlvYShQj63o-62J#m=u=`$(D-J{n9X> zfJ9dt4STpKrP3o4KR2}Pk4CO8jYCM`0pS7R0pA1k{Sd-PWz?4=T(VaOCKN)@b_yX_ zz&!E*Ru}SR)R!Y%GF8-JswlZC_!I-UI@X71ca%|Ij&RA<32=1+zGd(k3NY)a7h>HB zM7b2H@PP2ZSPxWU)upiiAjkgFiE*%h@Xk#&o4b@P*An*sww9s&*>i5JTe$ev$f`B# z+Rvk64x>xRxg3u(5PlJKXCRz`(@_BWG@}JdeW=6(E-(Ym6nYT78$&WB5$ZI*_f3mr zMDOZUzqn*67Q56+UJNc~2E5EkRsR2B_YdSO`HlPqfUtWe)Iks$U=@Pxjj#n`3~nc&A2OhW0Rvz{4qWh{ zfWZ48JOqc}VR#f__frVGpM~e(MK}U4A^3g`UWa4wCcK5v`+Yb8AHYfY1Wv(c41}M7 zpWzSq8_x0P`8=%Ndhzqk>c>3Xg;>tE9D4x!(!Hdbe7=7!LU=E!hE=)js4k%rL9rsm<{fIK_AeC_t+ek*G` zhxu&5-x1koxN-+PKK|?kxfkUAN|Afx40}i?6S*JvEraV&Y_z2hUz2B}y{sj0wQscJ zY^0;M+wu)c2ZRTN2dc@PcydJh*daOo#XfMNc z<>y-j|1Wcl$O;b#4+swk4@|TNrgDQ3dj4PfqHmmbIU>0PChzW25X;&at-7LuQoq009i6SH8WK$F6$^JYXJU}x+ O7J=+$*c{I@hZz7m4-=#S diff --git a/Cargo.lock b/Cargo.lock index 287871f..257b035 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,19 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -80,18 +93,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] -name = "byteorder" -version = "1.5.0" +name = "castaway" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.2.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" dependencies = [ - "shlex", + "rustversion", ] [[package]] @@ -147,12 +154,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "colored" -version = "3.0.0" +name = "compact_str" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" dependencies = [ - "windows-sys 0.59.0", + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", ] [[package]] @@ -162,7 +174,7 @@ dependencies = [ "env_logger", "log", "rand", - "toml_edit", + "toml_edit 0.23.2", ] [[package]] @@ -181,30 +193,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.9.1", - "core-foundation", - "libc", -] - [[package]] name = "crc" version = "3.3.0" @@ -220,6 +208,21 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crossterm" version = "0.25.0" @@ -229,13 +232,29 @@ dependencies = [ "bitflags 1.3.2", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", "winapi", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "mio 1.0.4", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + [[package]] name = "crossterm_winapi" version = "0.9.1" @@ -246,40 +265,145 @@ dependencies = [ ] [[package]] -name = "dispatch2" -version = "0.3.0" +name = "cursive" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "386d5a36020bb856e9a34ecb8a4e6c9bd6b0262d1857bae4db7bc7e2fdaa532e" dependencies = [ - "bitflags 2.9.1", - "objc2", + "ahash", + "cfg-if", + "crossbeam-channel", + "crossterm 0.28.1", + "cursive_core", + "lazy_static", + "libc", + "log", + "signal-hook", + "unicode-segmentation", + "unicode-width", ] [[package]] -name = "dyn-clone" -version = "1.0.19" +name = "cursive-macros" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "ac7ac0eb0cede3dfdfebf4d5f22354e05a730b79c25fd03481fc69fcfba0a73e" +dependencies = [ + "proc-macro2", +] [[package]] -name = "enigo" -version = "0.5.0" +name = "cursive_core" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71744ff36f35a4276e8827add8102d0e792378c574fd93cb4e1c8e0505f96b7c" +checksum = "321ec774d27fafc66e812034d0025f8858bd7d9095304ff8fc200e0b9f9cc257" dependencies = [ - "core-foundation", - "core-graphics", - "foreign-types-shared", - "libc", + "ahash", + "compact_str", + "crossbeam-channel", + "cursive-macros", + "enum-map", + "enumset", + "lazy_static", "log", - "nom", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "windows", - "x11rb", - "xkbcommon", - "xkeysym", + "num", + "parking_lot", + "serde_json", + "time", + "toml", + "unicode-segmentation", + "unicode-width", + "xi-unicode", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -322,59 +446,10 @@ dependencies = [ ] [[package]] -name = "foreign-types" -version = "0.5.0" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gethostname" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" -dependencies = [ - "libc", - "windows-targets 0.48.5", -] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" @@ -388,6 +463,39 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "gpiocdev" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ae5319702c7dc34334b9f5e2b73d4ffedb0544675b5931af61b3b13a13f928e" +dependencies = [ + "gpiocdev-uapi", + "thiserror", +] + +[[package]] +name = "gpiocdev-embedded-hal" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af983262d31a1acaec17c949a0bb2a7df7fbdd7ab89c9a6bf6f98a38b25bd690" +dependencies = [ + "embedded-hal", + "gpiocdev", + "thiserror", +] + +[[package]] +name = "gpiocdev-uapi" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb160ed63d4235176759e49b7e26aa063603ec4547e458ab1ea98483040028c" +dependencies = [ + "bitflags 2.9.1", + "ioctl-sys", + "libc", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.15.4" @@ -400,6 +508,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.10.0" @@ -410,23 +524,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "inquire" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" -dependencies = [ - "bitflags 2.9.1", - "crossterm", - "dyn-clone", - "fuzzy-matcher", - "fxhash", - "newline-converter", - "once_cell", - "unicode-segmentation", - "unicode-width", -] - [[package]] name = "io-kit-sys" version = "0.4.1" @@ -437,12 +534,24 @@ dependencies = [ "mach2", ] +[[package]] +name = "ioctl-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd11f3a29434026f5ff98c730b668ba74b1033637b8817940b54d040696133c" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jiff" version = "0.2.15" @@ -516,37 +625,16 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "menu_tui" -version = "0.1.0" +version = "0.2.0" dependencies = [ "clap", - "colored", "config_lib", - "crossterm", - "enigo", + "crossterm 0.25.0", + "cursive", "env_logger", - "inquire", "log", - "terminal-menu", ] [[package]] @@ -562,25 +650,15 @@ dependencies = [ ] [[package]] -name = "newline-converter" -version = "0.3.0" +name = "mio" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", "libc", - "memoffset", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -595,60 +673,79 @@ dependencies = [ ] [[package]] -name = "nom" -version = "8.0.0" +name = "num" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "memchr", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", ] [[package]] -name = "objc2" -version = "0.6.1" +name = "num-complex" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ - "objc2-encode", + "num-traits", ] [[package]] -name = "objc2-app-kit" -version = "0.3.1" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "bitflags 2.9.1", - "objc2", - "objc2-foundation", + "num-traits", ] [[package]] -name = "objc2-core-foundation" -version = "0.3.1" +name = "num-iter" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ - "bitflags 2.9.1", - "dispatch2", - "objc2", + "autocfg", + "num-integer", + "num-traits", ] [[package]] -name = "objc2-encode" -version = "4.1.0" +name = "num-rational" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] [[package]] -name = "objc2-foundation" -version = "0.3.1" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "bitflags 2.9.1", - "objc2", - "objc2-core-foundation", + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", ] [[package]] @@ -701,6 +798,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -810,18 +913,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "rk_nku_menu" -version = "0.1.0" -dependencies = [ - "config_lib", - "enigo", - "env_logger", - "log", - "menu_tui", - "sysfs_gpio", -] - [[package]] name = "rk_nku_sync" version = "0.1.0" @@ -833,6 +924,19 @@ dependencies = [ "protocol_lib", ] +[[package]] +name = "rk_smart_menu" +version = "0.1.0" +dependencies = [ + "config_lib", + "env_logger", + "gpiocdev", + "gpiocdev-embedded-hal", + "log", + "menu_tui", + "thiserror", +] + [[package]] name = "rustix" version = "0.38.44" @@ -846,6 +950,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "scopeguard" version = "1.2.0" @@ -872,6 +988,27 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serialport" version = "4.7.2" @@ -884,18 +1021,12 @@ dependencies = [ "core-foundation-sys", "io-kit-sys", "mach2", - "nix 0.26.4", + "nix", "scopeguard", "unescaper", "winapi", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signal-hook" version = "0.3.18" @@ -913,7 +1044,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", + "mio 1.0.4", "signal-hook", ] @@ -932,6 +1064,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -949,25 +1087,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sysfs_gpio" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8808c55bc926565c62ef7838bcaa8add51585236803e2bdfa1472e3a3ab5e17" -dependencies = [ - "nix 0.23.2", -] - -[[package]] -name = "terminal-menu" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66e7bfe79ce3db25763bc7cb3af8be872a5e74ed2844c5cfcbbacd95c4a4bc8a" -dependencies = [ - "crossterm", - "lazy_static", -] - [[package]] name = "thiserror" version = "2.0.12" @@ -989,12 +1108,57 @@ dependencies = [ ] [[package]] -name = "thread_local" -version = "1.1.9" +name = "time" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ - "cfg-if", + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[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 0.6.11", + "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]] @@ -1006,6 +1170,20 @@ dependencies = [ "serde", ] +[[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 0.6.11", + "toml_write", + "winnow", +] + [[package]] name = "toml_edit" version = "0.23.2" @@ -1013,7 +1191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1dee9dc43ac2aaf7d3b774e2fba5148212bf2bd9374f4e50152ebe9afd03d42" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.0", "toml_parser", "toml_writer", "winnow", @@ -1028,6 +1206,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "toml_writer" version = "1.0.2" @@ -1067,6 +1251,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1104,108 +1294,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -1280,15 +1368,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1446,38 +1525,10 @@ dependencies = [ ] [[package]] -name = "x11rb" -version = "0.13.1" +name = "xi-unicode" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" -dependencies = [ - "gethostname", - "rustix", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" - -[[package]] -name = "xkbcommon" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d66ca9352cbd4eecbbc40871d8a11b4ac8107cfc528a6e14d7c19c69d0e1ac9" -dependencies = [ - "libc", - "memmap2", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" [[package]] name = "zerocopy" diff --git a/config_lib/.DS_Store b/config_lib/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0, } impl DeviceConfig { - /// ## Заполнение струтуры DeviceConfig + /// ## Заполнение струтуры `DeviceConfig` /// * `path_to_scheme` - путь к файлу-cхеме pub fn create_parameter_list(path_to_scheme: &str) -> Result { let toml_config = TomlScheme::new(path_to_scheme).map_err(|e| e.to_string())?; @@ -43,7 +43,7 @@ impl DeviceConfig { Ok(device_config) } - /// # Сохранение текущих значений параметров в TOML-файл self.schema_path + /// ## Сохранение текущих значений параметров в TOML-файл `self.schema_path` pub fn save_parameters_values(&self) -> Result<(), String> { let mut toml_config = TomlScheme::new(&self.scheme.get_path_to_scheme_file()).map_err(|e| e.to_string())?; @@ -57,7 +57,7 @@ impl DeviceConfig { Ok(()) } - /// # Получение списка ключей - имен параметров + /// ## Получение списка ключей - имен параметров pub fn get_parameters_names(&self) -> Result, String> { if self.parameters.is_empty() { return Err("Parameters list is empty".to_string()); @@ -66,7 +66,7 @@ impl DeviceConfig { return Ok(self.parameters.keys().map(|key| key.clone()).collect()); } - /// # Получение описания параметра соответствующего key + /// ## Получение описания параметра соответствующего `key` pub fn get_parameter_description(&self, key: &str) -> Result { match self.parameters.get(key) { Some(parameter) => Ok(parameter.get_description()), @@ -74,7 +74,7 @@ impl DeviceConfig { } } - /// # Получение списка возможных значений параметра соответствующего key + /// ## Получение списка возможных значений параметра соответствующего `key` pub fn get_parameter_possible_values(&self, key: &str) -> Result, String> { match self.parameters.get(key) { Some(parameter) => Ok(parameter.get_possible_values()), @@ -82,7 +82,7 @@ impl DeviceConfig { } } - /// # Получение значения параметра соответствующего key + /// ## Получение значения параметра соответствующего `key` pub fn get_parameter_value(&self, key: &str) -> Result { match self.parameters.get(key) { Some(parameter) => Ok(parameter.get_value()), @@ -90,7 +90,7 @@ impl DeviceConfig { } } - /// # Установка значения параметра соответствующего key + /// ## Установка значения параметра соответствующего `key` pub fn set_parameter_value(&mut self, key: &str, value: String) -> Result<(), String> { match self.parameters.get_mut(key) { Some(parameter) => parameter.set_value(value), @@ -99,7 +99,7 @@ impl DeviceConfig { } /// ## Получение числового индекса соответствующего текущему значению параметра - /// Индекс соответствует положению текущего значения в списке возможных значений possible_values + /// Индекс соответствует положению текущего значения в списке возможных значений `possible_values` pub fn get_parameter_index_using_value(&self, key: &str) -> Result { let parameters_list = self.get_parameters_names()?; if parameters_list.contains(&key.to_owned()) { @@ -116,8 +116,8 @@ impl DeviceConfig { Err("Unable to get parameter value using index".to_string()) } - /// ## Присваивание параметру key значения, соответствующего index - /// Индекс соответствует положению значения в списке возможных значений possible_values + /// ## Присваивание параметру `key` значения, соответствующего `index` + /// Индекс соответствует положению значения в списке возможных значений `possible_values` pub fn set_parameter_value_using_index(&mut self, key: &str, index: u8) -> Result<(), String> { let parameters_list = self.get_parameters_names()?; if parameters_list.contains(&key.to_owned()) { diff --git a/menu_tui/.DS_Store b/menu_tui/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..da1f096180e5c37867fa7650064e0e72285e2d5e GIT binary patch literal 8196 zcmeHMPfrs;6n_Hb9w02i62P+C4s~U_Q+Kx%BqTlS z2k-;<0la$D51>b{n)n60c-A+6Dx`%IF(S?+Gr!k)^P8FX+sW>}EdY>QtuziW0sslD z9F1OVjwsyEYg_41J>7@|`2bb$fkNcgqiWk|-JuoG3TOqi0$KsBz-dqbpV_=5j(P4& zuWq#hT7myk0e*ilv2wH&HYAE$2R0%FK246UG`|bV5DYP5IbH# zLqYIL6o;%m_JR_t=U6Q$MS^QfM-ofH7}?vKm>kcVQxoI+S#$61t+A{*IWe`rpG>4P zH}5SLw){%KUdm92x*b4YOK7i-ipwi*S7lt}V>7NVhM^)$B+vI==)2f&3=9qpqz2OI z^zfyT%Y#?07K|a+DQ=ZRzT+9@voPn^Xu;@@Fz&e`3l>7Ujq&DVx)a;p3v>;(5Eg0a z$wAlBZWYI*4m!l74?{|W)pF>S<~KJZ8s`t$D}1$L$b5f+MPBT&U}a6L9@ENPu*q;m zzJvKHcSXxIj1b#6`4uM&ouEu}yX(H=Qhz?ku{dUxyoXWnor@*ys#ElWa_0VbDQYrf z-H?_6nWoN(RMS#OGVlGb!d?oT3PsX_^kl3@YDfY`VH#{$gXhqI*YF13!Uy;S-{1%Q zBzOKy`p#3IvVk*typ;*e*O-_g9-*VKFpNQ6e)A>Ovbz-K`jeVo?`5rk0W zpQg^h(N@vu1ATT6w7SGC$5>`  для state management + 5.Unit-тесты: Покрыть тестами критичные модули (`DeviceParameters`, `NavigationManager`) + 6.Реализовать `update_view`: Добавить функционал обновления прошивки через HTTP/UART + 7.Документация: Дополнить `rustdoc` комментариями и примерами использования diff --git a/menu_tui/assets/style.toml b/menu_tui/assets/style.toml new file mode 100644 index 0000000..89086e9 --- /dev/null +++ b/menu_tui/assets/style.toml @@ -0,0 +1,25 @@ +# First come some various options +shadow = false # Don't draw shadows around stacked views +borders = "simple" # Alternatives are "none" and "outset" + +# Here we define the color palette. +[colors] + background = "#000000" + # If the value is an array, the first valid color will be used. + # If the terminal doesn't support custom color, + # non-base colors will be skipped. + shadow = ["#000000", "black"] + view = "#d3d7cf" + + # Array and simple values have the same effect. + primary = ["#111111"] + secondary = "#EEEEEE" + tertiary = "#444444" + + # Hex values can use lower or uppercase. + title_primary = "#0000FF" + title_secondary = "#ffff55" + + # Lower precision values can use only 3 digits. + highlight = "#00F" + highlight_inactive = "#5555FF" \ No newline at end of file diff --git a/menu_tui/examples/simple.rs b/menu_tui/examples/simple.rs index 08edc41..c5702cc 100644 --- a/menu_tui/examples/simple.rs +++ b/menu_tui/examples/simple.rs @@ -1,27 +1,66 @@ -use config_lib::device_config::DeviceConfig; -use menu_tui::menu_rendering::{MainMenuStates, show_main_dialog}; +use menu_tui::menu_navigation::NavigationManager; +use menu_tui::menu_process::DeviceMenu; +use std::thread; +use std::time::Duration; + +/// Запуск меню в режиме имитации нажатия кнопок fn main() -> Result<(), Box> { - let mut device_config = - DeviceConfig::create_parameter_list("menu_tui/examples/simple_config.toml")?; + // Создаём меню + let mut menu = DeviceMenu::new( + "menu_tui/examples/simple_config.toml", + "menu_tui/assets/style.toml", + ); - loop { - match show_main_dialog(&mut device_config) { - Ok(MainMenuStates::ConfigurationState) => { - println!("Configuration state"); - continue; - } - Ok(MainMenuStates::ExitState) => { - println!("The user has chosen to exit..."); - break; - } - Err(e) => { - println!("Menu error: {}", e); - break; - } - } - + // Получаем менеджер навигации для управления от GPIO + let nav_manager = menu.get_navigation_manager(); + + menu.show_main_menu(); + + menu.launch_idling_watchdog(5); + + // Симуляция GPIO обработчиков в отдельных потоках + simulate_gpio_handlers(nav_manager); + + // Запускаем главный цикл обработки событий + menu.run(); + + let device_config = menu.get_schema_config()?; + + println!("Итоговая конфигурация:"); + for parameter in device_config.get_parameters_names()? { + let value = device_config.get_parameter_value(¶meter).unwrap(); + println!("{}: {}", parameter, value); } return Ok(()); } + +// Симуляция обработчиков GPIO кнопок +fn simulate_gpio_handlers(nav_manager: NavigationManager) { + // Кнопка 1 (GPIO): Навигация вниз + let nav_down = nav_manager.clone(); + thread::spawn(move || { + thread::sleep(Duration::from_secs(2)); + + loop { + thread::sleep(Duration::from_millis(1000)); + + // Симуляция нажатия кнопки GPIO + nav_down.navigate_down(); + } + }); + + // Кнопка 2 (GPIO): Выбор/подтверждение + let nav_select = nav_manager.clone(); + thread::spawn(move || { + thread::sleep(Duration::from_secs(3)); + + loop { + thread::sleep(Duration::from_millis(3000)); + + // Симуляция нажатия кнопки GPIO + nav_select.select_item(); + } + }); +} diff --git a/menu_tui/examples/simple_config.toml b/menu_tui/examples/simple_config.toml index d497542..c6ed0a7 100644 --- a/menu_tui/examples/simple_config.toml +++ b/menu_tui/examples/simple_config.toml @@ -26,7 +26,29 @@ possible_values = [ "15", ] default = "0" -current = "6" +current = "4" + +[placement] +name = "Размещение" +possible_values = [ + "Кабина", + "Этаж", + "Универсальное", +] +default = "Кабина" +current = "Универсальное" + + +[station] +name = "Станция управления" +possible_values = [ + "Smart", + "ШК6000", + "НКУ-CAN", + "НКУ-SD7", +] +default = "Smart" +current = "Smart" [soundvolume] name = "Громкость звука" @@ -38,7 +60,7 @@ current = "50%" name = "Громкость музыки" possible_values = ["0%", "25%", "50%", "75%", "100%"] default = "0%" -current = "100%" +current = "50%" [loadcapacity] name = "Грузоподъемность" @@ -61,4 +83,4 @@ possible_values = [ "1600кг 21чел.", ] default = "СКРЫТО" -current = "240кг 3чел." +current = "525кг 7чел." diff --git a/menu_tui/src/.DS_Store b/menu_tui/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b1a4b84acbd5abbe9ef804cb4635aa1c8931d35b GIT binary patch literal 6148 zcmeHKPfrs;6n_InS`b;FMU0wk?8Ss8f*{6ta9J>hc!013O90DmJJgl!PTk$MA|dHn zKY$;=58&0KegHjs)x;ag#=DyU{j(+F&g3I=bs^`m|LtvP&#XTtB3dNboBZLv|kc zVTHA9)(9)H$aSP6gHkf4j*jN$XA9=S-0X3|Ji2v#reMy`EgTQ!uZ8Y zC1b*K%LmnnuXTw9EV6?JEg7RRR=iSX;aWtSSZOh#PZP&~hOY6S64q(u;fZPEu#Pd= z6N4D^UPNiQU5)(8>i&LAlVX>?#b+x=EC|+E>?b}8w|2zr39YS!`wVB~D_E^_SG-B1 z7_ld(xaCHX8&;`(xEr`04OT;&B?+q)eXN47TukZI-LfB6^LM^V9y59Cs?-d~cc@Kg zJDNh0e;a%j`bOy1C?>5*Pu{w(Leek|i{QWxJb^a6gjety-oXd>0^i^}86lJ83YjJa za)aC?7Fi_gWSi^}mpqpFeVx~QS&gTF#He(==B5*c0Sl{W?PQ!z&`&)KD5BECJsxe) z3}^=acLwheL<%tYN#h?pJ6H$^xQOQP;I1Db);4Df${6oa4tcYlBX7eTty3}^=a zO$JD6*;%%+Hg&f4RLRd;6Wbm(Eg Lbf+2kqYV52p%uz@ literal 0 HcmV?d00001 diff --git a/menu_tui/src/lib.rs b/menu_tui/src/lib.rs index 18d3e22..e36429d 100644 --- a/menu_tui/src/lib.rs +++ b/menu_tui/src/lib.rs @@ -1 +1,4 @@ -pub mod menu_rendering; +pub mod menu_navigation; +pub mod menu_process; +mod user_parameters; +mod views; diff --git a/menu_tui/src/menu_navigation.rs b/menu_tui/src/menu_navigation.rs new file mode 100644 index 0000000..cfaf075 --- /dev/null +++ b/menu_tui/src/menu_navigation.rs @@ -0,0 +1,165 @@ +//! # Модуль навигации по меню +//! +//! > Позволяет имитировать нажатия определенных клавиш в процессе работы с меню. +//! Используется если в системе отсутствует клавиатура. +//! +//! + +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use cursive::event::{Event, EventResult, Key}; +use cursive::views::{CircularFocus, ListView, SelectView}; +use cursive::{CbSink, Cursive, View}; + +// TODO: Перенести в отдельный модуль +enum ActiveMenuView { + MainView, + ConfigView, + ExitView, + UpdateView, +} + +impl From<&str> for ActiveMenuView { + fn from(value: &str) -> Self { + match value { + "main_menu" => ActiveMenuView::MainView, + "config_menu" => ActiveMenuView::ConfigView, + "exit_menu" => ActiveMenuView::ExitView, + "update_menu" => ActiveMenuView::UpdateView, + _ => ActiveMenuView::MainView, + } + } +} + +/// Менеджер навигации для управления меню c вызывающей стороны +pub struct NavigationManager { + // CbSink  — это thread-safe канал, позволяющий отправлять callback-функции из любого потока. + // Cursive автоматически обрабатывает эти callback’и в своем event loop, обеспечивая потокобезопасность + cb_sink: CbSink, + current_view: Arc>, + last_activity: Arc>, +} + +impl NavigationManager { + /// Создать новый менеджер навигации + pub fn new(cb_sink: CbSink) -> Self { + NavigationManager { + cb_sink, + current_view: Arc::new(Mutex::new("main_menu".to_string())), + last_activity: Arc::new(Mutex::new(Instant::now())), + } + } + + /// Получить время последнего нажатия клавиши + pub fn get_last_activity(&self) -> Instant { + self.last_activity.lock().unwrap().clone() + } + + /// Установить время последнего нажатия клавиши + pub fn set_last_activity(&self, instant: Instant) { + let mut last_time = self.last_activity.lock().unwrap(); + *last_time = instant; + } + + /// Установить текущий активный View (Cursive) + pub(crate) fn set_current_view(&self, view_name: &str) { + let mut current = self.current_view.lock().unwrap(); + *current = view_name.to_string(); + } + + /// Получить имя текущего View (Cursive) + pub(crate) fn get_current_view(&self) -> String { + self.current_view.lock().unwrap().clone() + } + + /// Имитация нажатия клавиши вниз + pub fn navigate_down(&self) { + self.set_last_activity(Instant::now()); + let view_name = self.get_current_view(); + self.send_event(&view_name, Event::Key(Key::Down)); + } + + /// Имитация нажатия клавиши вверх + pub fn navigate_up(&self) { + self.set_last_activity(Instant::now()); + let view_name = self.get_current_view(); + self.send_event(&view_name, Event::Key(Key::Up)); + } + + /// Имитация нажатия клавиши Enter + pub fn select_item(&self) { + let mut last_time = self.last_activity.lock().unwrap(); + *last_time = Instant::now(); + let view_name = self.get_current_view(); + self.send_event(&view_name, Event::Key(Key::Enter)); + } + + /// ## Отправка события `event` текущему рантайму `Cursive` для активного `View` + /// Отправка по каналу с помощью `cb_sink: Sender` + fn send_event(&self, view_name: &str, event: Event) { + let view_name = view_name.to_string(); + let event_clone = event.clone(); + + self.cb_sink + .send(Box::new(move |s: &mut Cursive| { + Self::dispatch_event_globally(s, &view_name, event_clone); + })) + .ok(); // Используем .ok() чтобы не паниковать при закрытии + } + + // Отправка события `event` в разные типы view + fn dispatch_event_globally(siv: &mut cursive::Cursive, view_name: &str, event: Event) { + // Сначала пытаемся отправить событие в активный слой (верхний) + // Это позволит обработать popup окна + let result = siv.screen_mut().on_event(event.clone()); + + match result { + EventResult::Consumed(maybe_callback) => { + // Событие обработано, выполняем callback если есть + if let Some(callback) = maybe_callback { + callback(siv); + } + return; + } + EventResult::Ignored => { + + // Событие проигнорировано верхним слоем + // Пытаемся отправить в именованный view + } + } + + // Fallback: отправка в конкретный именованный view + // Для CircularFocus> - главное меню + if let Some(result) = siv + .call_on_name(view_name, |view: &mut CircularFocus>| { + view.on_event(event.clone()) + }) + { + if let EventResult::Consumed(Some(callback)) = result { + callback(siv); + return; + } + } + + // Для CircularFocus - меню параметров устройства + if let Some(result) = siv.call_on_name(view_name, |view: &mut CircularFocus| { + view.on_event(event.clone()) + }) { + if let EventResult::Consumed(Some(callback)) = result { + callback(siv); + } + } + } +} + +/// Реализация клонирования для использования менеджера в разных потоках +impl Clone for NavigationManager { + fn clone(&self) -> Self { + NavigationManager { + cb_sink: self.cb_sink.clone(), + current_view: Arc::clone(&self.current_view), + last_activity: Arc::clone(&self.last_activity), + } + } +} diff --git a/menu_tui/src/menu_process.rs b/menu_tui/src/menu_process.rs new file mode 100644 index 0000000..1a2dbef --- /dev/null +++ b/menu_tui/src/menu_process.rs @@ -0,0 +1,146 @@ +//! # Модуль отображения меню +//! +//! ## Навигация +//! > Для навигации по меню используется клавиши стрелка вверх/вниз +//! Выбор элемента в меню происходит клавишей Enter +//! `В случае отсутствия данных клавиш в системе, можно эмулировать их с помощью модуля menu_navigation +//! +//! ## Параметры меню +//! > Параметры меню подтягиваются из файла-схемы TOML с помощью крейта config_lib + +use std::time::{Duration, Instant}; + +use config_lib::device_config::DeviceConfig; +use cursive::{ + Cursive, CursiveExt, + event::{Event, EventResult, EventTrigger}, +}; + +use crate::{ + menu_navigation::NavigationManager, user_parameters::DeviceParameters, + views::main_view::show_main_view, +}; + +pub struct DeviceMenu { + siv: Cursive, + nav_manager: NavigationManager, + scheme_config: DeviceConfig, +} + +pub(crate) struct MenuAppState { + pub navigation_manager: NavigationManager, + pub inner_config: DeviceParameters, +} + +impl DeviceMenu { + pub fn new(path_to_scheme: &str, theme_path: &str) -> Self { + // Получение конфигурации устройства + let device_config = DeviceConfig::create_parameter_list(path_to_scheme) + .expect("Ошибка загрузки config_scheme"); + + let mut siv = Cursive::default(); + + siv.set_global_callback('q', |siv: &mut Cursive| { + siv.quit(); + }); + // TODO: обработка ошибок + let mut device_parameters: DeviceParameters = DeviceParameters::new(); + + // Заполнение списка пользовательских параметров данными из `DeviceConfig` + device_parameters + .load_user_config(&device_config) + .expect("Ошибка загрузки пользовательских параметров!"); + + let nav_manager = NavigationManager::new(siv.cb_sink().clone()); + + // TODO: Cursive logs example + if let Err(_) = siv.load_theme_file(theme_path) { + println!("{}", format!("Не удалось загрузить тему: {theme_path}")); + } + + // Сохраняем и NavigationManager и конфигурацию + let app_state = MenuAppState { + navigation_manager: nav_manager.clone(), + inner_config: device_parameters, + }; + + // При работе в меню с исподьзованием обычной клавиатуры, обновляем last_activity (активность пользователя) + siv.set_on_pre_event_inner(EventTrigger::from_fn(|e| matches!(e, Event::Key(_))), { + let nav_manager_clone = nav_manager.clone(); + move |_event: &Event| -> Option { + nav_manager_clone.set_last_activity(Instant::now()); + None + } + }); + + siv.set_user_data(app_state); + + DeviceMenu { + siv, + nav_manager, + scheme_config: device_config, + } + } + + /// Показать главное меню + pub fn show_main_menu(&mut self) { + self.nav_manager.set_current_view("main_menu"); + show_main_view(&mut self.siv); + } + + /// Получить менеджер навигации для управления извне + pub fn get_navigation_manager(&self) -> NavigationManager { + self.nav_manager.clone() + } + + /// Запустить главный цикл обработки событий + pub fn run(&mut self) { + self.siv.run(); + } + + /// ## Запуск таймера, который проверяет активность пользователя + /// > Если пользователь не активен в течение `timeout_seconds`, то приложение закрывается. + /// > Каждый раз, когда пользователь нажимает одну из кнопок навигации, сбрасывается таймер. + pub fn launch_idling_watchdog(&mut self, timeout_seconds: u64) { + let nav_manager = self.get_navigation_manager(); + let cb_sink = self.siv.cb_sink().clone(); + + std::thread::spawn(move || { + loop { + std::thread::sleep(Duration::from_secs(1)); + + // Проверяем активность + let is_active = { + let last = nav_manager.get_last_activity(); + last.elapsed() < Duration::from_secs(timeout_seconds) + }; + + if !is_active { + cb_sink + .send(Box::new(|s: &mut Cursive| { + s.quit(); + })) + .ok(); + break; + } + } + }); + } + + /// Закрытие приложения + pub fn quit(&mut self) { + self.siv.quit(); + } + + /// Получить текущую конфигурацию + pub fn get_schema_config(&mut self) -> Result { + // TODO: обработка ошибок + if let Some(app_state) = self.siv.take_user_data::() { + app_state + .inner_config + .update_user_config(&mut self.scheme_config)?; + } + + Ok(self.scheme_config.clone()) + } +} diff --git a/menu_tui/src/menu_rendering.rs b/menu_tui/src/menu_rendering.rs deleted file mode 100644 index 0f006f3..0000000 --- a/menu_tui/src/menu_rendering.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! # Модуль отображения меню -//! -//! ## Навигация -//! > Для навигации по меню используется клавиши стрелка вверх/вниз -//! Выбор эелемента в меню происходит клавишей Enter -//! `В случае отсутствия данных клавиш в системе, можно эмулировать их с помощью библиотеки enigo` -//! -//! ## Параметры меню -//! > Параметры меню подтягиваются из файла-схемы TOML с помощью крейта config_lib -use config_lib::device_config::DeviceConfig; -use crossterm::style::Color; -use inquire::Select; -use terminal_menu::{TerminalMenuItem, button, label, menu, mut_menu, run}; - -pub enum MainMenuStates { - ConfigurationState, - ExitState, -} - -/// Отображение главного меню -pub fn show_main_dialog(config: &mut DeviceConfig) -> Result { - // Header - let mut menu_structure = generate_menu_header("Меню настроек индикатора"); - - // Текущие значения - menu_structure.extend(generate_current_values(config)?); - - // Элементы меню - menu_structure.extend(generate_menu_items(config)?); - - let main_menu = menu(menu_structure); - - // Отрисовка и навигация по меню - run(&main_menu); - - // Получение названия выбранного пункта меню - let mut_menu_instance = mut_menu(&main_menu); - let selected_menu_item = mut_menu_instance.selected_item_name(); - - // Получение ключа параметра в HashMap, соответствующего названию пункта меню - let selected_parameter_name = config - .get_parameters_names()? - .into_iter() - .find_map(|par_name| { - // Получение ключа параметра в HashMap, в случае если описание параметра совпадает с названием пункта меню - if let Ok(par_desc) = config.get_parameter_description(&par_name) { - return if par_desc == selected_menu_item { - Some(par_name) - } else { - None - }; - } - None - }); - - if let Some(key) = selected_parameter_name { - show_configuration_dialog(config, &key)?; - return Ok(MainMenuStates::ConfigurationState); - } - - // Сохранение настроек в TOML-файл схему - config.save_parameters_values()?; - - return Ok(MainMenuStates::ExitState); -} - -/// Отображение диалога для выбора значения параметра -fn show_configuration_dialog(config: &mut DeviceConfig, par_name: &str) -> Result<(), String> { - let possible_values = config.get_parameter_possible_values(par_name)?; - let answer = Select::new("Выберите значение параметра", possible_values.clone()).prompt(); - - match answer { - Ok(selection) => { - config.set_parameter_value(par_name, selection)?; - Ok(()) - } - Err(e) => return Err(e.to_string()), - } -} - -/// Генерация структуры меню: Титул -fn generate_menu_header(title: &str) -> Vec { - vec![ - label("----------------------").colorize(Color::DarkGreen), - label(title).colorize(Color::DarkGreen), - label(format!( - "{} v{}", - env!("CARGO_CRATE_NAME"), - env!("CARGO_PKG_VERSION") - )) - .colorize(Color::DarkGreen), - label(format!("{}", env!("CARGO_PKG_AUTHORS"))).colorize(Color::DarkGreen), - label("-----------------------").colorize(Color::DarkGreen), - ] -} - -/// Генерация структуры меню: Текущие значения настраиваемых параметров -fn generate_current_values(config: &DeviceConfig) -> Result, String> { - let parameter_names = config.get_parameters_names()?; - - let mut menu_structure = vec![]; - - for parameter_name in ¶meter_names { - let parameter_value = config.get_parameter_value(¶meter_name)?; - let parameter_description = config.get_parameter_description(¶meter_name)?; - menu_structure.push( - label(format!("{}: {}", parameter_description, parameter_value)) - .colorize(Color::DarkYellow), - ); - } - - menu_structure.push(label("-----------------------").colorize(Color::DarkYellow)); - - Ok(menu_structure) -} - -/// Генерация структуры меню: Элементы меню -fn generate_menu_items(config: &DeviceConfig) -> Result, String> { - let parameter_names = config.get_parameters_names()?; - - let mut menu_structure = vec![]; - - menu_structure.push(label("-----------------------").colorize(Color::DarkGrey)); - - // Элементы меню; имя каждого параметра соответствует полю description - let mut menu_structure = menu_structure; - for parameter_name in ¶meter_names { - let parameter_description = config.get_parameter_description(¶meter_name)?; - menu_structure.push(button(parameter_description).colorize(Color::White)); - } - menu_structure.push(button("Выход").colorize(Color::White)); - - menu_structure.push(label("-----------------------").colorize(Color::DarkGrey)); - - Ok(menu_structure) -} diff --git a/menu_tui/src/user_parameters.rs b/menu_tui/src/user_parameters.rs new file mode 100644 index 0000000..9195016 --- /dev/null +++ b/menu_tui/src/user_parameters.rs @@ -0,0 +1,89 @@ +use config_lib::device_config::DeviceConfig; + +/// ### Структура для хранения пользовательских параметров +/// Дублирует данные из экземпляра `DeviceConfig` (пара ключ-значение из `HashMap`) +#[derive(Clone, Debug)] +pub struct Parameter { + /// Имя параметра (совпадает с ключом в `HashMap`) + pub key: String, + /// Описание параметра (используется для отображения в интерфейсе) + pub description: String, + /// Возможные значения параметра + pub options: Vec, + /// Текущее значение параметра + pub selected_value: String, +} +impl Parameter { + fn new(key: String, description: String, options: Vec, selected: String) -> Self { + Parameter { + key, + description, + options, + selected_value: selected, + } + } +} + +/// Конфигурация устройства +/// Представляет собой набор `Parameter`, дублирует данные из экземпляра `DeviceConfig` +#[derive(Clone, Debug)] +pub struct DeviceParameters { + pub parameters: Vec, +} + +impl Default for DeviceParameters { + fn default() -> Self { + DeviceParameters::new() + } +} + +impl DeviceParameters { + pub fn new() -> Self { + DeviceParameters { + parameters: Vec::new(), + } + } + + /// Заполнение списка пользовательских параметров данными из `DeviceConfig` + pub fn load_user_config(&mut self, parameters_schema: &DeviceConfig) -> Result<(), String> { + for parameter_key in parameters_schema.get_parameters_names()? { + let parameter_values = parameters_schema + .get_parameter_possible_values(¶meter_key)? + .clone(); + let parameter_desc = parameters_schema.get_parameter_description(¶meter_key)?; + let parameter_value = parameters_schema + .get_parameter_value(¶meter_key)? + .clone(); + + self.add_parameter(Parameter::new( + parameter_key, + parameter_desc, + parameter_values, + parameter_value, + )); + } + Ok(()) + } + + /// Обновление конфигурации `DeviceConfig` по данным из `DeviceParameters` + pub fn update_user_config(&self, parameters_schema: &mut DeviceConfig) -> Result<(), String> { + for parameter in self.parameters.iter() { + let value = parameter.selected_value.clone(); + parameters_schema.set_parameter_value(¶meter.key, value)?; + } + + Ok(()) + } + + /// Добавление нового параметра + fn add_parameter(&mut self, param: Parameter) { + self.parameters.push(param); + } + + /// Обновление значения параметра + pub fn update_parameter(&mut self, key: &str, value: String) { + if let Some(param) = self.parameters.iter_mut().find(|p| p.key == key) { + param.selected_value = value; + } + } +} diff --git a/menu_tui/src/views/config_view.rs b/menu_tui/src/views/config_view.rs new file mode 100644 index 0000000..b5475a5 --- /dev/null +++ b/menu_tui/src/views/config_view.rs @@ -0,0 +1,89 @@ +use crate::menu_process::MenuAppState; +use cursive::{ + Cursive, + align::HAlign, + view::{Nameable, Resizable}, + views::{Button, CircularFocus, Dialog, ListView, SelectView}, +}; + +/// Отображение диалога выбора значения параметров устройства +pub(crate) fn show_config_view(siv: &mut Cursive) { + // Получение текущего состояния приложения + let app_state: MenuAppState = siv + .take_user_data() + .expect("Не удалось выполнить take_user_data"); + + // Обновляем current_view + app_state.navigation_manager.set_current_view("params_menu"); + + let mut parameter_list = ListView::new(); + + // Разделитель + parameter_list.add_delimiter(); + + // Создание списка параметров c выпадающими списками возможных значений + for parameter in app_state.inner_config.parameters.iter() { + let key = parameter.key.clone(); + let current_value = parameter.selected_value.clone(); + + let mut select_view = SelectView::new().popup().h_align(HAlign::Left); + + let mut selected_index = 0; + for (index, option) in parameter.options.iter().enumerate() { + select_view.add_item(option, option.clone()); + if option == ¤t_value { + selected_index = index; + } + } + // Установка курсора на текущем значении параметра + select_view.set_selection(selected_index); + + // Добавление коллбэка для обновления выбранного значения + let key_for_callback = key.clone(); + + select_view.set_on_submit(move |s, selected_value: &String| { + let mut state: MenuAppState = s + .take_user_data() + .expect("Не удалось выполнить take_user_data"); + + state + .inner_config + .update_parameter(&key_for_callback, selected_value.clone()); + s.set_user_data(state); + }); + + let parameter_view = select_view.with_name(key); + + parameter_list.add_child(¶meter.description, parameter_view); + } + + // Разделитель + parameter_list.add_delimiter(); + + // Кнопка возврата в главное меню + parameter_list.add_child( + "", + Button::new("Назад в главное меню", |s| { + let state: MenuAppState = s + .take_user_data() + .expect("Не удалось выполнить take_user_data"); + state.navigation_manager.set_current_view("main_menu"); + s.set_user_data(state); + s.pop_layer(); + }), + ); + + // Обёртка для циклической навигации с помощью одной кнопки + let circular_list = CircularFocus::new(parameter_list) + .with_wrap_arrows(true) + .with_name("params_menu"); + + let parameter_dialog = Dialog::around(circular_list) + .title("⚙ Параметры устройства") + .fixed_width(50); + + // Обновление текущих значений параметров + siv.set_user_data(app_state); + + siv.add_layer(parameter_dialog); +} diff --git a/menu_tui/src/views/exit_view.rs b/menu_tui/src/views/exit_view.rs new file mode 100644 index 0000000..6e9f774 --- /dev/null +++ b/menu_tui/src/views/exit_view.rs @@ -0,0 +1,6 @@ +use cursive::Cursive; + +/// Закрытие приложения с сохранением параметров +pub(crate) fn show_exit_view(siv: &mut Cursive) { + siv.quit(); +} diff --git a/menu_tui/src/views/main_view.rs b/menu_tui/src/views/main_view.rs new file mode 100644 index 0000000..38fa8b7 --- /dev/null +++ b/menu_tui/src/views/main_view.rs @@ -0,0 +1,45 @@ +use crate::views::config_view::show_config_view; +use crate::views::exit_view::show_exit_view; +use crate::views::update_view::show_update_view; +use cursive::{ + Cursive, + view::{Nameable, Resizable}, + views::{CircularFocus, Dialog, LinearLayout, SelectView}, +}; + +/// Отображение главного меню +pub(crate) fn show_main_view(siv: &mut Cursive) { + siv.pop_layer(); + + let main_menu = SelectView::new() + .item("Параметры устройства", 1) + .item("Обновление прошивки", 2) + .item("Сохранение и выход", 3) + .h_align(cursive::align::HAlign::Center) + .on_submit(|s: &mut Cursive, menu_item_index| menu_item_selected(s, *menu_item_index)); + + // Обёртка для циклической навигации с помощью одной кнопки + let circular_menu = CircularFocus::new(main_menu).with_wrap_arrows(true); + + let layout = LinearLayout::vertical().child( + Dialog::around(circular_menu.with_name("main_menu")) + .title(format!( + "{} {}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .fixed_size((50, 5)), + ); + + siv.add_layer(layout); +} + +/// Обработка выбора определенного пункта в главном меню +fn menu_item_selected(siv: &mut Cursive, menu_item_index: i32) { + match menu_item_index { + 1 => show_config_view(siv), + 2 => show_update_view(siv), + 3 => show_exit_view(siv), + _ => {} + } +} diff --git a/menu_tui/src/views/mod.rs b/menu_tui/src/views/mod.rs new file mode 100644 index 0000000..83ba7f0 --- /dev/null +++ b/menu_tui/src/views/mod.rs @@ -0,0 +1,4 @@ +pub mod config_view; +pub mod exit_view; +pub mod main_view; +pub mod update_view; diff --git a/menu_tui/src/views/update_view.rs b/menu_tui/src/views/update_view.rs new file mode 100644 index 0000000..a2a8869 --- /dev/null +++ b/menu_tui/src/views/update_view.rs @@ -0,0 +1,6 @@ +use cursive::{Cursive, views::Dialog}; + +/// Отображение диалога обновления прошивки +pub(crate) fn show_update_view(siv: &mut Cursive) { + siv.add_layer(Dialog::info("Обновление прошивки пока не реализовано")); +} diff --git a/rk_nku_menu/.DS_Store b/rk_nku_menu/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Result<(), Box> { + env_logger::init(); + + // Создаём меню + let mut menu = DeviceMenu::new(NKU_DEVICE_CONFIG_PATH, TUI_APP_CONFIG_PATH); + + let nav_manager = menu.get_navigation_manager(); + + menu.show_main_menu(); + + // Навигация по меню с помощью кнопок, подключенных к GPIO + gpio_navigation_handlers(nav_manager); + + menu.launch_idling_watchdog(IDLE_TIMEOUT_SEC); + + menu.run(); + + menu.quit(); + + let device_config = menu.get_schema_config()?; + + debug!("Итоговая конфигурация:"); + for parameter in device_config.get_parameters_names()? { + let value = device_config.get_parameter_value(¶meter).unwrap(); + debug!("{}: {}", parameter, value); + } + + device_config.save_parameters_values()?; + + return Ok(()); +} + +/// ligpiod номер порта кнопки ввода +const S1_BTN: (&str, u8) = ("/dev/gpiochip3", 16); +/// ligpiod номер порта кнопки выбора +const S2_BTN: (&str, u8) = ("/dev/gpiochip2", 1); +/// ## Симуляция обработчиков GPIO кнопок +/// ### Расчёт номера пина в порядке libgpiod /// ``` /// bank = 0; // GPIO0_B5 => 0, bank ∈ [0,4] /// group = 1; // GPIO0_B5 => 1, group ∈ {(A=0), (B=1), (C=2), (D=3)} /// X = 5; // GPIO0_B5 => 5, X ∈ [0,7] /// number = group * 8 + X = 1 * 8 + 5 = 13; /// pin = bank * 32 + number = 0 * 32 + 13 = 13; +/// +/// gpiochip_number = pin_number / 32; // Номер банка +/// line_offset = pin_number % 32; // Номер пина в банке /// ``` - -/// SYSFS номер порта кнопки ввода -const S1_BTN: u8 = 124; //124 -/// SYSFS номер порта кнопки выбора -const S2_BTN: u8 = 125; //125 -/// Максимальное время бездействия [c], после достижения которого происходит выход из меню -const IDLE_TIMEOUT_SEC: u32 = 30; - -fn main() -> Result<(), Box> { - env_logger::init(); - let mut device_config = DeviceConfig::create_parameter_list(NKU_DEVICE_CONFIG_PATH)?; - - static BTN_PRESSED: AtomicBool = AtomicBool::new(false); - static STOP_FLAG: AtomicBool = AtomicBool::new(false); - - // Открытие потоков для обслуживания EXT прерываний - let down_irq_thread = std::thread::spawn(move || { - let input = Pin::new(S2_BTN as u64); - debug!("down_irq_thread launched!"); - input.with_exported(|| { - input.set_direction(Direction::In)?; - input.set_edge(Edge::RisingEdge)?; - let mut poller = input.get_poller()?; - - while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) { - if let Some(pin_value) = poller.poll(1000)? { - debug!("{}", format!("Pin value: {}", pin_value)); - if pin_value == 1 { - let mut enigo = Enigo::new(&Settings::default()).unwrap(); - enigo.key(Key::DownArrow, Click).unwrap(); - BTN_PRESSED.store(true, std::sync::atomic::Ordering::SeqCst); - std::thread::sleep(Duration::from_millis(500)); - debug!("DownArrow pressed"); - } - } - } - Ok(()) - }) - }); - - let return_irq_thread = std::thread::spawn(move || { - let input = Pin::new(S1_BTN as u64); - debug!("return_irq_thread launched!"); - input.with_exported(|| { - input.set_direction(Direction::In)?; - input.set_edge(Edge::RisingEdge)?; - let mut poller = input.get_poller()?; - - while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) { - if let Some(pin_value) = poller.poll(1000)? { - debug!("{}", format!("Pin value: {}", pin_value)); - if pin_value == 1 { - let mut enigo = Enigo::new(&Settings::default()).unwrap(); - enigo.key(Key::Return, Click).unwrap(); - BTN_PRESSED.store(true, std::sync::atomic::Ordering::SeqCst); - std::thread::sleep(Duration::from_millis(500)); - debug!("Return pressed"); - } - } - } - - Ok(()) - }) - }); - - // Поток TUI меню +/// +fn gpio_navigation_handlers(nav_manager: NavigationManager) { + // Кнопка 1 (GPIO): Навигация вниз + let nav_down = nav_manager.clone(); std::thread::spawn(move || { + let down_btn_req = Request::builder() + .on_chip(S1_BTN.0) + .with_line(S1_BTN.1 as u32) + .as_input() + .with_edge_detection(gpiocdev::line::EdgeDetection::RisingEdge) + .request() + .unwrap(); + debug!("down_irq_thread launched!"); + loop { - match show_main_dialog(&mut device_config) { - Ok(MainMenuStates::ConfigurationState) => { - debug!("Configuration state"); - continue; - } - Ok(MainMenuStates::ExitState) => { - debug!("The user has chosen to exit..."); - break; - } - Err(e) => { - error!("Menu error: {}", e); - break; + if down_btn_req.has_edge_event().unwrap() { + if let Ok(edge) = down_btn_req.read_edge_event() { + if edge.kind == gpiocdev::line::EdgeKind::Rising { + debug!("DownArrow pressed"); + // Симуляция нажатия кнопки DOWN + nav_down.navigate_down(); + std::thread::sleep(Duration::from_millis(400)); + } } } } - - STOP_FLAG.store(true, std::sync::atomic::Ordering::SeqCst); - debug!("The user has chosen to exit..."); }); - // Главный поток - проверка бездействия - let mut idle_counter: u32 = 0; - while !STOP_FLAG.load(std::sync::atomic::Ordering::SeqCst) { - if BTN_PRESSED.load(std::sync::atomic::Ordering::SeqCst) { - idle_counter = 0; - BTN_PRESSED.store(false, std::sync::atomic::Ordering::SeqCst); - } else { - idle_counter += 1; - if idle_counter > IDLE_TIMEOUT_SEC * 10 { - STOP_FLAG.store(true, std::sync::atomic::Ordering::SeqCst); - debug!("Idle timeout has been reached, exiting..."); + // Кнопка 2 (GPIO): Выбор/подтверждение + let nav_select = nav_manager.clone(); + std::thread::spawn(move || { + let return_btn_req = Request::builder() + .on_chip(S2_BTN.0) + .with_line(S2_BTN.1 as u32) + .as_input() + .with_edge_detection(gpiocdev::line::EdgeDetection::RisingEdge) + .request() + .unwrap(); + debug!("return_irq_thread launched!"); + loop { + if return_btn_req.has_edge_event().unwrap() { + if let Ok(edge) = return_btn_req.read_edge_event() { + if edge.kind == gpiocdev::line::EdgeKind::Rising { + debug!("Return pressed"); + // Симуляция нажатия кнопки ENTER + nav_select.select_item(); + std::thread::sleep(Duration::from_millis(400)); + } + } } } - std::thread::sleep(Duration::from_millis(100)); - } - - if down_irq_thread.join().is_err() { - debug!("Unable to stop down_irq_thread"); - } - - if return_irq_thread.join().is_err() { - debug!("Unable to stop return_irq_thread"); - } - - return Ok(()); + }); }