Compare commits

...

735 Commits

Author SHA1 Message Date
Priec
bb75b70341 Update submodule tui-canvas to latest commit 2025-10-26 18:06:09 +01:00
Priec
585891b0f8 Link komp_ac_server as submodule 2025-10-26 18:03:39 +01:00
Priec
c4ed5e71b1 Remove inlined server crate (moved to komp_ac_server repo) 2025-10-26 18:03:06 +01:00
Priec
824eadc363 Link tui-canvas as submodule 2025-10-26 17:54:22 +01:00
Priec
afab4bb521 Remove inlined canvas crate (moved to tui-canvas repo) 2025-10-26 17:53:53 +01:00
Priec
bf29c666a5 Update komp_ac_client submodule after layout fix 2025-10-26 17:49:34 +01:00
Priec
4708f98ee9 Link komp_ac_client as submodule 2025-10-26 17:45:54 +01:00
Priec
0d465e47b3 Remove inlined client crate (moved to separate repository) 2025-10-26 17:44:37 +01:00
Priec
8ec1fa1761 validation2 I dont know if its correct??? 2025-10-26 16:44:15 +01:00
Priec
11185282c4 cargo fix on server 2025-10-26 16:07:19 +01:00
Priec
492f1f1e55 with last commit, we can simplify the logic + remove old 2025_ prefix for search of the tables 2025-10-17 22:55:59 +02:00
Priec
241ab99584 get column gets converted to get column with index automatically now 2025-10-17 22:44:36 +02:00
Priec
8bd5b5c62f marked crucial todo for 2025_ deprecated prefixing 2025-09-21 21:47:32 +02:00
Priec
7e21258d2e all tests passed without any problem 2025-09-21 20:50:14 +02:00
Priec
49277cfdd4 last error remaining 2025-09-18 18:53:55 +02:00
Priec
1f6dc3cd75 failed tests all over the place now 2025-09-18 10:47:27 +02:00
Priec
7350b0985c JSONB on table scripts also now 2025-09-18 10:47:01 +02:00
Priec
73bc6dc99c fixing tests and migration to the serialized deserialized JSONB2 2025-09-17 21:19:41 +02:00
Priec
095645a209 fixing tests and migration to the serialized deserialized JSONB 2025-09-17 21:10:11 +02:00
Priec
532977056d fixing serialization and deserialization in the data insertion 2025-09-17 09:38:05 +02:00
Priec
2435f58256 table definition now has serialization deserialization properly implemented 2025-09-16 22:55:49 +02:00
Priec
ceb560c658 serialization of the gRPC JSONB now fully works for the validation 2025-09-14 11:24:27 +02:00
Priec
d88c239bf6 serde of jsonb in grpc 2025-09-14 10:56:38 +02:00
filipriec
01c4ff2e14 validation backend 2025-09-13 21:15:44 +02:00
Priec
c2890e1f3d tests via script and make test works now 2025-09-13 08:53:26 +02:00
Priec
e1ea44c68c inputing data for the client for the validation 2025-09-12 23:06:42 +02:00
Priec
cec2361b00 validation1 for the form 2025-09-12 21:25:49 +02:00
Priec
9672b9949c finally a working space 2025-09-12 19:14:21 +02:00
Priec
e4e9594a9d minor changes 2025-09-12 19:05:17 +02:00
Priec
6daa5202b1 debug is now running properly in the background without any issues 2025-09-12 18:17:52 +02:00
Priec
cae47da5f2 reused grpc connections, not a constant refreshes anymore, all fixed now, keep on fixing other bugs 2025-09-12 18:15:46 +02:00
filipriec
85c7c89c28 space2 is now debugging better 2025-09-12 15:46:14 +02:00
Priec
0d80266e9b space commands here we go again 2025-09-11 22:36:40 +02:00
Priec
a604d62d44 inputs from keyboard are now decoupled 2025-09-10 22:12:22 +02:00
Priec
2cbbfd21aa revert works on login, now do the same for other pages as well 2025-09-08 22:11:53 +02:00
Priec
1c17d07497 space and revert working properly well, also shift 2025-09-08 20:05:39 +02:00
Priec
ad15becd7a doing key sequencing via space 2025-09-08 12:56:03 +02:00
Priec
c2a6272413 buttons in add_logic and add_table works properly well now 2025-09-04 18:56:21 +02:00
Priec
c51af13fb1 intro buttons fixed 2025-09-04 17:46:32 +02:00
Priec
d9d8562539 moving the state from general to each page owning its own state of button or canvas focus 2025-09-04 17:36:13 +02:00
Priec
6891631b8d validation of exact strings 2025-09-02 13:52:36 +02:00
Priec
738d58b5f1 moving add_table to add_logic modern architecture3 2025-09-02 11:46:35 +02:00
Priec
3081125716 moving add_table to add_logic modern architecture2 2025-09-02 00:36:49 +02:00
Priec
6073c7ab43 moving add_table to add_logic modern architecture 2025-09-02 00:23:50 +02:00
filipriec
8157dc7a60 add_table working properly well 2025-09-01 16:37:43 +02:00
filipriec
3b130e9208 add_table fixing 2025-09-01 16:30:57 +02:00
Priec
ab81434c4e add table3 2025-09-01 07:41:13 +02:00
Priec
62c54dc1eb moving add table to the same way as add logic 2025-08-31 23:07:57 +02:00
Priec
347802b2a4 working suggestions in add_logic 2025-08-31 21:48:54 +02:00
Priec
a5a8d98984 add Logic fully decoupled 2025-08-31 21:37:18 +02:00
filipriec
5b42da8290 separate page 2025-08-31 21:25:43 +02:00
filipriec
4e041f36ce move out of canvas properly fixed, now working everyhing properly well 2025-08-31 10:02:48 +02:00
filipriec
22926b7266 add logic now using general movement 2025-08-30 22:38:48 +02:00
filipriec
0a7f032028 add_logic cursor when refocus is proper again 2025-08-30 21:26:20 +02:00
filipriec
4edec5e72d add logic is now using canvas library now 2025-08-30 21:10:10 +02:00
filipriec
c7d524c76a add table and add logic removal from ui.rs and event.rs 2025-08-30 19:47:26 +02:00
filipriec
9ed558562b moved admin now 2025-08-30 19:26:12 +02:00
filipriec
43f5c1a764 login and register are now havving own handlers and loaders, moving logic out of event.rs and ui.rs 2025-08-30 19:13:12 +02:00
filipriec
46149c09db event.rs and ui.rs refactor for the forms page(moved logic to the forms page dir and just calling it now) 2025-08-30 16:42:04 +02:00
filipriec
a0757efe8b moved add_table and add_logic, needs more things done tho 2025-08-30 14:46:34 +02:00
filipriec
10f4b9d8e2 moved add_table to be feature based 2025-08-30 14:25:33 +02:00
filipriec
42db496ad7 admin page being rendered properly well now 2025-08-30 13:32:45 +02:00
filipriec
d6fd672409 roles are now better 2025-08-30 13:19:45 +02:00
filipriec
60eb1c9f51 register has dropdown now 2025-08-29 22:07:04 +02:00
filipriec
a09c804595 intro empty buffer fixed 2025-08-29 20:13:25 +02:00
filipriec
a17f73fd54 buffer bug fixed, now proper names are being displayed 2025-08-29 19:53:31 +02:00
filipriec
2373ae4b8c form pages robust finish chnages 2025-08-29 19:49:27 +02:00
filipriec
16dd460469 we compiled but buffer doesnt work 2025-08-29 18:11:27 +02:00
filipriec
58f109ca91 adding to have multiple forms pages 2025-08-29 16:18:42 +02:00
filipriec
75da9c0f4b login and register are sending data to the backend successfuly 2025-08-29 14:46:43 +02:00
filipriec
833b918c5b cursor style is handled properly now 2025-08-29 12:32:33 +02:00
filipriec
72c2691a17 registration now has working form 2025-08-29 12:22:25 +02:00
filipriec
cf79bc7bd5 bottom_panel decoupled 2025-08-29 08:25:24 +02:00
filipriec
f5f2f2cdef login page using canvas library 2025-08-28 21:26:21 +02:00
filipriec
19a9bab8c2 login page using canvas for forms 2025-08-28 21:07:23 +02:00
filipriec
6e221ef8c1 HARDEST COMMIT IN THE RECENT TIMES we fixed movement in the admin page 2025-08-28 13:43:17 +02:00
Priec
e142f56706 admin page 2025-08-28 09:15:14 +02:00
Priec
a794f22366 admin page is now featured 2025-08-27 16:32:20 +02:00
Priec
cfe4903c79 admin page is now featured 2025-08-27 16:32:09 +02:00
Priec
a0a473f96c admin page 2025-08-27 12:14:09 +02:00
Priec
9e4dd3b4c7 intro movement fully fixed 2025-08-27 01:38:51 +02:00
Priec
e5db0334c0 fixed intro movement, select not working yet 2025-08-27 01:34:56 +02:00
Priec
d641ad1bbb centralized general movement 2025-08-27 01:06:54 +02:00
filipriec
18393ff661 is edit mode is gone from the codebase 2025-08-24 16:54:18 +02:00
filipriec
b2a82fba30 fixing is_edit_mode flag removal 2025-08-24 16:37:30 +02:00
filipriec
f6c2fd627f fixing is_edit_mode 2025-08-24 16:00:58 +02:00
filipriec
15d9b31cb6 removing edit mode from the codebase 2025-08-24 15:32:24 +02:00
filipriec
06cc1663b3 working general mode only with canvas, removing highlight, readonly or edit 2025-08-23 23:34:14 +02:00
filipriec
88a4b2d69c intro is now separated 2025-08-23 21:58:29 +02:00
filipriec
e6072d25c5 register is now separated also 2025-08-23 21:47:18 +02:00
filipriec
fc2b65601e login function moved 2025-08-23 21:05:02 +02:00
filipriec
597bdde7e1 login moved to the pages 2025-08-23 20:58:12 +02:00
filipriec
f56092e86c login page now in a separate dir 2025-08-23 19:48:23 +02:00
filipriec
d5cfe59f47 dialog refactor comment, dialog crate finished for now 2025-08-23 13:36:46 +02:00
filipriec
f281eaa662 dialog is a feature 2025-08-23 13:29:28 +02:00
Priec
cbb3ed7c48 small cleanup 2025-08-23 00:22:07 +02:00
Priec
41a0b85376 forms page moved more2 2025-08-23 00:16:07 +02:00
Priec
b5a31ee81c forms page 2025-08-22 23:54:22 +02:00
Priec
dceb031822 removed docs book from git history 2025-08-22 23:31:08 +02:00
Priec
78bc9fc432 router4 compiled 2025-08-22 23:27:32 +02:00
Priec
b9072e4d7c router2, needs bug fixes3 2025-08-22 22:57:28 +02:00
Priec
5d97e63f93 router2, needs bug fixes 2025-08-22 22:52:20 +02:00
Priec
957f5bf9f0 router implementation 2025-08-22 22:19:59 +02:00
Priec
6833ac5fad find palette in the bottom panel 2025-08-22 17:11:52 +02:00
Priec
3dff2ced6c bottom panel moved 2025-08-22 16:48:25 +02:00
Priec
ea7ff3796f search grpc client isolated a bit mode 2025-08-22 16:09:16 +02:00
Priec
310617d62b cargo fix 2025-08-22 15:49:33 +02:00
Priec
1d94e82f4b search 2025-08-22 15:48:30 +02:00
Priec
00dad5d673 fixed buffer logic 2025-08-22 14:26:58 +02:00
Priec
414c6957e7 sidebar as a feature 2025-08-22 14:11:36 +02:00
Priec
f127298e5a buffer as a feature 2025-08-22 13:47:34 +02:00
Priec
f49899e66d general movement now works 2025-08-22 11:23:11 +02:00
Priec
5717c88857 proper config.toml 2025-08-22 10:55:37 +02:00
Priec
ae8aa16208 working entering to the edit mode 2025-08-22 09:56:55 +02:00
Priec
4ed8e7b421 fixed form state removed, but not won, aint working yet 2025-08-22 00:27:23 +02:00
Priec
3dd6808ea2 now no need for init_form_editor everywhere 2025-08-21 21:16:59 +02:00
Priec
f2b426851b compiled still not working 2025-08-21 13:23:21 +02:00
Priec
f9e0833bcf working keymap 2025-08-21 12:32:36 +02:00
Priec
11b073c2fd removing highlightmode from the app, handled by the library now 2025-08-21 10:33:52 +02:00
Priec
1320884409 more improvements 2025-08-20 23:52:14 +02:00
filipriec
aea2c39215 reverted core actions in canvas 2025-08-20 16:33:19 +02:00
filipriec
4c2464ab30 compiled 2025-08-20 16:28:31 +02:00
filipriec
26053a5fd8 fixes 9 2025-08-20 11:36:56 +02:00
filipriec
589220a2ba fixing 8 2025-08-20 11:33:34 +02:00
filipriec
2cda54633f not working, needs more fixes 2025-08-19 22:14:43 +02:00
filipriec
3eea6b9e88 fixes 7 2025-08-19 22:02:00 +02:00
filipriec
db9bb7e168 fixes 5 2025-08-19 20:05:18 +02:00
filipriec
3ccd094a22 fixing problems more6 2025-08-19 19:21:02 +02:00
filipriec
032f21edaa more canvas implementation4 2025-08-19 14:43:10 +02:00
Priec
42eb087363 canvas fixes3 2025-08-19 13:25:10 +02:00
Priec
d0ff449e3b going into a new canvas library structure 2025-08-19 13:24:48 +02:00
Priec
858f5137d8 migrating to the new canvas library 2025-08-19 10:56:54 +02:00
Priec
80d5dd0761 forgotten cargo 2025-08-19 01:15:16 +02:00
Priec
49b31c6e92 readme updated 2025-08-19 01:12:59 +02:00
Priec
8ed2fbbe34 more warnings cleared 2025-08-19 01:04:40 +02:00
Priec
5ae8d13719 clippy improvements 2025-08-19 01:03:28 +02:00
Priec
7bf2b81229 syntax highlighting example is now fine 2025-08-19 00:58:21 +02:00
Priec
0215f2824a working syntax highlighting 2025-08-19 00:51:57 +02:00
Priec
3fdb7e4e37 syntec, but not compiling 2025-08-19 00:46:11 +02:00
Priec
a3f578ebac using ropey for textarea 2025-08-19 00:03:04 +02:00
Priec
f0bc7abaad closer to prod more than ever 2025-08-18 22:52:08 +02:00
Priec
f9d9231d50 more prod ready comments 2025-08-18 21:10:06 +02:00
Priec
465db82bd9 prod comments in the codebase 2025-08-18 21:00:14 +02:00
Priec
885a48bdd8 more clippy fixes 2025-08-18 19:49:08 +02:00
Priec
c915b3287b cargo clippy ran 2025-08-18 19:42:31 +02:00
Priec
7b2f021509 bugs fixed 2025-08-18 19:23:10 +02:00
Priec
5f1bdfefca fixing warnings 2025-08-18 18:01:22 +02:00
Priec
3273a43e20 restored grayed out suggestions 2025-08-18 17:39:25 +02:00
Priec
61e439a1d4 fixing warnings and depracated legacy things 2025-08-18 17:19:21 +02:00
Priec
03808a8b3b now finally end line working as intended 2025-08-18 16:59:38 +02:00
Priec
57aa0ed8e3 trying to fix end line bugs 2025-08-18 16:45:49 +02:00
Priec
5efee3f044 line wrapping is now working properly well 2025-08-18 09:44:53 +02:00
Priec
6588f310f2 end of the line fixed 2025-08-18 00:22:09 +02:00
Priec
25b54afff4 improved textarea normal editor mode, not just vim 2025-08-17 18:35:51 +02:00
Priec
b9a7f9a03f textarea 2025-08-17 17:52:40 +02:00
Priec
e36324af6f working textarea with example, time to prepare it for the future implementations 2025-08-17 12:17:46 +02:00
Priec
60cb45dcca first textarea implementation 2025-08-17 11:01:38 +02:00
Priec
215be3cf09 renamed capital lettered functions and fixed examples 2025-08-16 23:10:50 +02:00
Priec
b2aa966588 suggestions behind features flag only 2025-08-15 00:06:54 +02:00
Priec
67512ac151 src/editor.rs doesnt exist anymore 2025-08-15 00:06:19 +02:00
Priec
3f5dedbd6e a bit of a cleanup, updated functionality of ge now working porperly well 2025-08-14 14:23:08 +02:00
Priec
ce07105eea more vim functionality added 2025-08-14 00:08:18 +02:00
Priec
587470c48b vim like behaviour is being built 2025-08-13 22:16:28 +02:00
Priec
3227d341ed cleared codebase 2025-08-13 01:22:50 +02:00
Priec
2b16a80ef8 removed silenced variables 2025-08-12 09:53:24 +02:00
Priec
8b742bbe09 comments for reimplementation of autotrigger 2025-08-11 23:08:57 +02:00
Priec
189d3d2fc5 suggestions2 only on tab trigger and not automatic 2025-08-11 23:05:56 +02:00
Priec
082093ea17 compiled examples 2025-08-11 22:50:28 +02:00
Priec
280f314100 fixing examples 2025-08-11 12:41:42 +02:00
Priec
163a6262c8 proper example is being set 2025-08-11 12:03:56 +02:00
Priec
e8a564aed3 sugggestions are agnostic 2025-08-11 00:01:53 +02:00
filipriec
53464dfcbf switch handled by the library from now on 2025-08-10 22:07:25 +02:00
filipriec
b364a6606d fixing more code refactorization 2025-08-10 16:10:45 +02:00
Priec
f09e476bb6 working, restored 2025-08-10 12:20:43 +02:00
Priec
e2c9cc4347 WIP: staged changes before destructive reset 2025-08-10 11:03:31 +02:00
Priec
06106dc31b improvements done by gpt5 2025-08-08 23:10:23 +02:00
Priec
8e3c85991c fixed example, now working everything properly well 2025-08-07 23:30:31 +02:00
Priec
d3e5418221 fixed example of suggestions2 2025-08-07 20:05:39 +02:00
Priec
0d0e54032c better suggestions2 example, not there yet 2025-08-07 18:51:45 +02:00
Priec
a8de16f66d suggestions is getting more and more strong than ever before 2025-08-07 16:00:46 +02:00
Priec
5b2e0e976f fixing examples 2025-08-07 13:51:59 +02:00
Priec
d601134535 computed fields are working perfectly well now 2025-08-07 12:38:09 +02:00
Priec
dff320d534 autocomplete to suggestions 2025-08-07 12:08:02 +02:00
Priec
96cde3ca0d working examples 4 and 5 2025-08-07 00:23:45 +02:00
Priec
6ba0124779 feature5 implementation is full now 2025-08-07 00:03:11 +02:00
Priec
34c68858a3 feature4 implemented and working properly well 2025-08-06 23:16:04 +02:00
Priec
4c8cfd4f80 feature3 cursor bug fixed WARNING MIGHT BE BREAKING IF PROBLEMS, CHECK THIS COMMIT but it should be safe imo 2025-08-06 22:19:07 +02:00
Priec
85c5d7ccf9 feature3 with bug, needs a fix immidiately 2025-08-06 22:05:10 +02:00
Priec
46a0d2b9db better example for feature2 being implemented and integrated into the codebase 2025-08-05 21:15:25 +02:00
Priec
c9b4841f67 validation2 example now working and displaying the full potential of the feature2 being implemented 2025-08-05 21:11:31 +02:00
Priec
d62cc2add6 feature2 implemented bug needs to be addressed 2025-08-05 19:22:30 +02:00
Priec
9c36e76eaa validation of characters length is finished 2025-08-05 18:27:16 +02:00
Priec
abd8cba7a5 forgotten cargo lock 2025-08-05 00:12:25 +02:00
Priec
e6c4cb7e75 validation passed to the canvas library now compiled 2025-08-04 23:38:44 +02:00
filipriec
3d4435bac5 working colors in vim mode 2025-08-03 22:08:52 +02:00
filipriec
4146d0820b line different color changed 2025-08-03 21:09:58 +02:00
filipriec
dbaa32f589 Merge branch 'main' of gitlab.com:filipriec/komp_ac 2025-08-03 07:53:36 +02:00
Priec
2b8eae67b9 highlight is now finally working 2025-08-02 23:31:03 +02:00
Priec
225bdc2bb6 Merge branch 'main' of gitlab.com:filipriec/komp_ac 2025-08-02 22:11:16 +02:00
Priec
8605ed1547 fixing issues in the edit/normal mode 2025-08-02 22:08:43 +02:00
filipriec
91cecabaca append at the end of the line is being fully fixed now 2025-08-02 16:56:16 +02:00
filipriec
d4922233ae Merge branch 'canvas' of gitlab.com:filipriec/komp_ac 2025-08-02 15:46:51 +02:00
filipriec
c00a214a0f Merge branch 'main' of gitlab.com:filipriec/komp_ac 2025-08-02 15:42:56 +02:00
Priec
0baf152c3e automatic cursor style handled by the library 2025-08-02 15:06:29 +02:00
Priec
c92c617314 exposed api to full vim mode 2025-08-02 13:41:21 +02:00
Priec
8c8ba53668 better example 2025-08-02 10:45:21 +02:00
Priec
2b08e64db8 fixed generics 2025-08-02 00:19:45 +02:00
Priec
643db8e586 removed deprecantions 2025-08-01 23:38:24 +02:00
Priec
5c39386a3a completely redesign philosofy of this library 2025-08-01 22:54:05 +02:00
Priec
8f99aa79ec working autocomplete now, with backwards deprecation 2025-07-31 22:44:21 +02:00
Priec
c594c35b37 autocomplete now working 2025-07-31 22:25:43 +02:00
Priec
828a63c30c canvas is fixed, lets fix autocomplete also 2025-07-31 22:04:15 +02:00
Priec
36690e674a canvas library config removed compeltely 2025-07-31 21:41:54 +02:00
Priec
8788323c62 fixed canvas library 2025-07-31 20:44:23 +02:00
Priec
5b64996462 example with debug stuff 2025-07-31 19:05:57 +02:00
Priec
3f4380ff48 documented code now 2025-07-31 17:29:03 +02:00
Priec
59a29aa54b not working example to canvas crate, improving and fixing now 2025-07-31 15:07:28 +02:00
Priec
5d084bf822 fixed working canvas in client, need more fixes now 2025-07-31 14:44:47 +02:00
Priec
ebe4adaa5d bug is present, i cant type or move in canvas from client 2025-07-31 13:39:38 +02:00
Priec
c3441647e0 docs and config adjustement 2025-07-31 13:18:27 +02:00
Priec
574803988d introspection to generated config now works 2025-07-31 12:31:21 +02:00
Priec
9ff3c59961 Remove canvas .toml files from git tracking and ensure they remain ignored 2025-07-31 11:37:56 +02:00
Priec
c5f22d7da1 canvas library config is now required 2025-07-31 11:16:21 +02:00
Priec
3c62877757 removing compatibility code fully, we are now fresh without compat layer. We compiled successfuly 2025-07-30 22:54:02 +02:00
Priec
cc19c61f37 new canvas library changed client for compatibility 2025-07-30 22:42:32 +02:00
Priec
ad82bd4302 canvas robust solution to movement 2025-07-30 22:02:52 +02:00
Priec
d584a25fdb removed hardcoded values from the canvas library 2025-07-30 21:16:16 +02:00
Priec
baa4295059 removed _e files completely 2025-07-30 20:25:58 +02:00
Priec
6cbfac9d6e read only deleted completely 2025-07-30 19:39:27 +02:00
Priec
13d28f19ea removing _ro files completely 2025-07-30 19:30:55 +02:00
Priec
8fa86965b8 removing canvasstate permanently 2025-07-30 19:20:23 +02:00
Priec
72c38f613f canvasstate is now officially nonexistent as dep 2025-07-30 19:14:35 +02:00
Priec
e4982f871f add_logic is now using canvas library 2025-07-30 18:02:59 +02:00
Priec
4e0338276f autotrigger vs manual trigger 2025-07-30 17:16:20 +02:00
Priec
fe193f4f91 unimportant 2025-07-30 16:34:21 +02:00
Priec
0011ba0c04 add_table now ported to the canvas library also 2025-07-30 14:06:05 +02:00
Priec
3c2eef9596 registering canvas functions now instead of internal state 2025-07-30 13:24:49 +02:00
Priec
dac788351f autocomplete gui as original, needs logic change in the future 2025-07-30 13:00:23 +02:00
Priec
8d5bc1296e usage of the canvas is fully implemented, time to fix bugs. Working now fully 2025-07-30 12:51:18 +02:00
Priec
969ad229e4 compiled 2025-07-30 12:45:13 +02:00
Priec
0d291fcf57 auth not working with canvas crate yet 2025-07-30 12:08:35 +02:00
Priec
d711f4c491 usage of canvas lib for auth BROKEN 2025-07-30 11:14:05 +02:00
Priec
9369626e21 migration md 2025-07-29 23:56:53 +02:00
Priec
f84bb0dc9e compiled successfulywith rich suggestions now 2025-07-29 23:49:58 +02:00
Priec
20b428264e library is now conflicting with client and its breaking it, but lets change the client 2025-07-29 23:25:10 +02:00
Priec
05bb84fc98 different structure of the library 2025-07-29 23:04:48 +02:00
Priec
46a85e4b4a autocomplete separate traits, one for autocomplete one for canvas purely 2025-07-29 22:31:35 +02:00
Priec
b4d1572c79 autocomplete is now robust, but unified, time to split it up 2025-07-29 22:18:44 +02:00
Priec
b8e1b77222 compiled autocomplete 2025-07-29 21:46:55 +02:00
Priec
1a451a576f working, cleaning trash via cargo fix 2025-07-29 20:14:24 +02:00
Priec
074b2914d8 gui canvas with rounded corners 2025-07-29 20:00:16 +02:00
Priec
aec5f80879 gui of canvas is from the canvas crate now 2025-07-29 19:54:29 +02:00
Priec
a1fa42e204 config now finally works 2025-07-29 18:56:56 +02:00
Priec
306cb956a0 canvas has now its own config for keybindings, lets use that from the client 2025-07-29 17:16:03 +02:00
Priec
d837acde63 bugs fixed, canvas library now replaced internal canvas, only form is using it now 2025-07-29 16:05:45 +02:00
Priec
db938a2c8d canvas library usage instead of internal canvas on the form, others are still using canvas from state. Needed debugging because its not working yet 2025-07-29 15:20:00 +02:00
Priec
f24156775a canvas ready, lets implement now client 2025-07-29 12:47:43 +02:00
Priec
2a7f94cf17 strings to enum, eased state.rs 2025-07-29 10:12:16 +02:00
Priec
15922ed953 canvas compiled for the first time 2025-07-29 09:35:17 +02:00
Priec
7129ec97fd canvas in a separate crate 2025-07-29 08:33:49 +02:00
Priec
a921806e62 version 0.4.1 2025-07-29 08:28:33 +02:00
Priec
d1b28b4fdd init startup fail fixed to detect working profile not just default profile 2025-07-28 23:15:26 +02:00
Priec
64fd7e4af2 Add Nix flake development environment with direnv 2025-07-28 11:41:50 +02:00
Priec
7b52a739c2 basic nix flake added 2025-07-28 10:59:25 +02:00
filipriec
a4e94878e7 enter decider should be taken care of next, suggestions works in register now also 2025-07-26 22:30:45 +02:00
filipriec
c7353ac81e email is now required 2025-07-26 20:34:02 +02:00
filipriec
1fbc720620 updated 2025-07-26 19:05:08 +02:00
filipriec
263ccc3260 updated system 2025-07-26 08:49:09 +02:00
filipriec
00c0a399cd sql search2 added 2025-07-25 22:38:34 +02:00
filipriec
8127c7bb1b renamed again and fixed some minor stuff 2025-07-25 18:18:00 +02:00
filipriec
7437908baf removal of @ syntax as reference from the backend 2025-07-25 12:13:51 +02:00
filipriec
9eb46cb5d3 kompAC 2025-07-25 11:59:18 +02:00
filipriec
38a70128b0 prod code 2, time for new functionality 2025-07-25 00:08:16 +02:00
filipriec
c58ce52b33 fixing warnings and making prod code 2025-07-24 23:57:21 +02:00
filipriec
c82813185f code cleanup in post endpoint 2025-07-24 22:18:36 +02:00
filipriec
a96681e9d6 cleaned up code 2025-07-24 22:04:55 +02:00
filipriec
4df6c40034 removal of hardcoded fix, now working in general 2025-07-24 21:56:07 +02:00
filipriec
089d728cc7 passers 2025-07-24 10:55:03 +02:00
filipriec
aca3d718b5 CHECK THIS COMMIT I HAVE NO CLUE IF ITS CORRECT 2025-07-23 23:30:56 +02:00
filipriec
8a6a584cf3 compiletime error in test fixed 2025-07-23 00:43:11 +02:00
filipriec
00ed0cf796 test is now passing 2025-07-22 18:31:26 +02:00
filipriec
7e54b2fe43 we got a full passer now 2025-07-20 14:58:39 +02:00
filipriec
84871faad4 another passer, 2 more to go 2025-07-20 11:57:46 +02:00
filipriec
bcb433d7b2 fixing post table script 2025-07-20 11:46:00 +02:00
filipriec
7d1b130b68 we have a passer 2025-07-17 22:55:54 +02:00
filipriec
24c2376ea1 crucial self reference allowed 2025-07-17 22:06:53 +02:00
filipriec
810ef5fc10 post table script is now aware of a type in the database 2025-07-17 21:16:49 +02:00
filipriec
fe246b1fe6 reject boolean and text in math functions at the post table script 2025-07-16 22:31:55 +02:00
filipriec
de42bb48aa deprecated function removed, no need for backup 2025-07-13 08:51:47 +02:00
filipriec
17495c49ac steel scripts now have far better logic than before 2025-07-12 23:06:21 +02:00
filipriec
0e3a7a06a3 put needs more adjustements 2025-07-12 11:06:11 +02:00
filipriec
e0ee48eb9c put endpoint is now stronq boiii 2025-07-11 23:24:28 +02:00
filipriec
d2053b1d5a SCRIPTS only scripts reference to a linked table from this commit 2025-07-11 08:55:32 +02:00
filipriec
fbe8e53858 circular dependency fix in post script logic 2025-07-11 08:50:10 +02:00
filipriec
8fe2581b3f put request at halt until script fixes 2025-07-10 21:11:42 +02:00
filipriec
60cc0e562e adjusted tests for the put request 2025-07-09 22:24:33 +02:00
filipriec
26898d474f tests for steel in post request is fixed with all the errors found in the codebase 2025-07-08 22:04:11 +02:00
filipriec
2311fbaa3b post steel tests 2025-07-08 20:26:48 +02:00
filipriec
be99cd9423 forgotten lock 2025-07-08 18:07:28 +02:00
filipriec
a3dd6fa95b now fully functional without a steel decimal crate, we are only importing it via cargo, because steel decimal is a separate published crate now 2025-07-08 18:02:32 +02:00
filipriec
433d87c96d stuff around publishing crate successfuly done 2025-07-07 22:08:38 +02:00
filipriec
aff4383671 tests are passing well now 2025-07-07 20:29:51 +02:00
filipriec
b7c8f6b1a2 tests in the steel decimal crate with serious issue fixed 2025-07-07 19:24:08 +02:00
filipriec
3443839ba4 placing them properly 2025-07-07 18:47:50 +02:00
filipriec
6c31d48f3b steel decimal needs readme before being published to the crates io 2025-07-07 18:08:45 +02:00
filipriec
1770292fd8 all the tests are now passing perfectly well 2025-07-07 15:35:33 +02:00
filipriec
afdd5c5740 parser finally fixed 2025-07-07 15:07:08 +02:00
filipriec
11487f0833 more fixes, still not done yet 2025-07-07 13:32:06 +02:00
filipriec
4d5d22d0c2 precision to steel decimal crate implemented 2025-07-07 00:31:13 +02:00
filipriec
314a957922 fixes, now .0 from rust decimal is being discussed 2025-07-06 20:53:40 +02:00
filipriec
4c57b562e6 more fixes, not there yet tho 2025-07-05 10:00:04 +02:00
filipriec
a757acf51c we are now fully using steel decimal crate inside of the server crate 2025-07-04 13:11:47 +02:00
filipriec
f4a23be1a2 steel decimal crate with a proper setup, needs so much work to be done to be finished 2025-07-04 11:56:21 +02:00
filipriec
93c67ffa14 steel decimal crate implemented 2025-07-02 16:31:15 +02:00
filipriec
d1ebe4732f steel with decimal math, saving before separating steel to a separate crate 2025-07-02 14:44:37 +02:00
filipriec
7b7f3ca05a more tests for the frontend 2025-06-26 20:25:59 +02:00
filipriec
234613f831 more frontend tests 2025-06-26 20:03:47 +02:00
filipriec
f6d84e70cc testing frontend to connect to the backend in the form page 2025-06-26 19:19:08 +02:00
filipriec
5cd324b6ae client tests now have a proper structure 2025-06-26 11:56:18 +02:00
filipriec
a7457f5749 frontend tui tests 2025-06-25 23:00:51 +02:00
filipriec
a5afc75099 crit bug fixed 2025-06-25 17:33:37 +02:00
filipriec
625c9b3e09 adresar and uctovnictvo are now wiped out of the existence 2025-06-25 16:14:43 +02:00
filipriec
e20623ed53 removing adresar and uctovnictvo hardcoded way of doing things from the project entirely 2025-06-25 13:52:00 +02:00
filipriec
aa9adf7348 removed unused tests 2025-06-25 13:50:08 +02:00
filipriec
2e82aba0d1 full passer on the tables data now 2025-06-25 13:46:35 +02:00
filipriec
b7a3f0f8d9 count is now fixed and working properly 2025-06-25 12:40:27 +02:00
filipriec
38c82389f7 count gets a full passer in tests 2025-06-25 12:37:37 +02:00
filipriec
cb0a2bee17 get by count well tested 2025-06-25 11:47:25 +02:00
filipriec
dc99131794 ordering of the tests for tables data 2025-06-25 10:34:58 +02:00
filipriec
5c23f61a10 get method passing without any problem 2025-06-25 09:44:38 +02:00
filipriec
f87e3c03cb get test updated, working now 2025-06-25 09:16:32 +02:00
filipriec
d346670839 tests for delete endpoint are passing all the tests 2025-06-25 09:04:58 +02:00
filipriec
560d8b7234 delete tests robustness not yet fully working 2025-06-25 08:44:36 +02:00
filipriec
b297c2b311 working full passer on put request 2025-06-24 20:06:39 +02:00
filipriec
d390c567d5 more tests 2025-06-24 00:46:51 +02:00
filipriec
029e614b9c more put tests 2025-06-24 00:45:37 +02:00
filipriec
f9a78e4eec the tests for the put endpoint is now being tested and passing but its not what i would love 2025-06-23 23:25:45 +02:00
filipriec
d8758f7531 we are passing all the tests now properly with the table definition and the post tables data now 2025-06-23 13:52:29 +02:00
filipriec
4e86ecff84 its now passing all the tests 2025-06-22 23:05:38 +02:00
filipriec
070d091e07 robustness, one test still failing, will fix it 2025-06-22 23:02:41 +02:00
filipriec
7403b3c3f8 4 tests are failing 2025-06-22 22:15:08 +02:00
filipriec
1b1e7b7205 robust decimal solution to push tables data to the backend 2025-06-22 22:08:22 +02:00
filipriec
1b8f19f1ce tables data tests are now generalized, needs a bit more fixes, 6/6 are passing 2025-06-22 16:10:24 +02:00
filipriec
2a14eadf34 fixed compatibility layer to old tests git status REMOVE IN THE FUTURE 2025-06-22 14:00:49 +02:00
filipriec
fd36cd5795 tests are now passing fully 2025-06-22 13:13:20 +02:00
filipriec
f4286ac3c9 more changes and more fixes, 3 more tests to go 2025-06-22 12:48:36 +02:00
filipriec
92d5eb4844 needs last one to be fixed, otherwise its getting perfect 2025-06-21 23:57:52 +02:00
filipriec
87b9f6ab87 more fixes 2025-06-21 21:43:39 +02:00
filipriec
06d98aab5c 5 more tests to go 2025-06-21 21:01:49 +02:00
filipriec
298f56a53c tests are passing better than ever before, its looking decent actually nowc 2025-06-21 16:18:32 +02:00
filipriec
714a5f2f1c tests compiled 2025-06-21 15:11:27 +02:00
filipriec
4e29d0084f compiled with the profile to be schemas 2025-06-21 10:37:37 +02:00
filipriec
63f1b4da2e changing profile id to schema in the whole project 2025-06-21 09:57:14 +02:00
filipriec
9477f53432 big change in the schema, its profile names now and not gen 2025-06-20 22:31:49 +02:00
filipriec
ed786f087c changing test for a huge change in a project 2025-06-20 20:07:07 +02:00
filipriec
8e22ea05ff improvements and fixing of the tests 2025-06-20 19:59:42 +02:00
filipriec
8414657224 gen isolated tables 2025-06-18 23:19:19 +02:00
filipriec
e25213ed1b tests are robusts running in parallel 2025-06-18 22:38:00 +02:00
filipriec
4843b0778c robust testing of the table definitions 2025-06-18 21:37:30 +02:00
filipriec
f5fae98c69 tests now working via make file 2025-06-18 14:44:38 +02:00
filipriec
6faf0a4a31 tests for table definitions 2025-06-17 22:46:04 +02:00
filipriec
011fafc0ff now working proper types 2025-06-17 17:31:11 +02:00
filipriec
8ebe74484c now not creating tables with the year_ prefix and living in the gen schema by default 2025-06-17 11:45:55 +02:00
filipriec
3eb9523103 you are going to kill me but please dont, i just cleaned up migration file and its 100% valid, do not use any version before this version and after this version so many things needs to be changed so haha... im ashamed but i love it at the same time 2025-06-17 11:21:33 +02:00
filipriec
3dfa922b9e unimportant change 2025-06-17 10:27:22 +02:00
filipriec
248d54a30f accpeting now null in the post table data as nothing 2025-06-16 22:51:05 +02:00
filipriec
b30fef4ccd post doesnt work, but refactored code displays the autocomplete at least, needs fix 2025-06-16 16:42:25 +02:00
filipriec
a9c4527318 complete redesign oh how client is displaying data 2025-06-16 16:10:24 +02:00
filipriec
c31f08d5b8 fixing post with links 2025-06-16 14:42:49 +02:00
filipriec
9e0fa9ddb1 autocomplete now autocompleting data not just id 2025-06-16 11:54:54 +02:00
filipriec
8fcd28832d better answer parsing 2025-06-16 11:14:04 +02:00
filipriec
cccf029464 autocomplete is now perfectc 2025-06-16 10:52:28 +02:00
filipriec
512e7fb9e7 suggestions in the dropdown menu now works amazingly well 2025-06-15 23:11:27 +02:00
filipriec
0e69df8282 empty search is now allowed 2025-06-15 18:36:01 +02:00
filipriec
eb5532c200 finally works as i wanted it to 2025-06-15 14:23:19 +02:00
filipriec
49ed1dfe33 trash 2025-06-15 13:52:43 +02:00
filipriec
62d1c3f7f5 suggestion works, but not exactly, needs more stuff 2025-06-15 13:35:45 +02:00
filipriec
b49dce3334 dropdown is being triggered 2025-06-15 12:15:25 +02:00
filipriec
8ace9bc4d1 links are now in the get method of the backend 2025-06-14 18:09:30 +02:00
filipriec
ce490007ed fixing server responses, now push data links fixed 2025-06-14 17:39:59 +02:00
filipriec
eb96c64e26 links to the other tables 2025-06-14 12:47:59 +02:00
filipriec
2ac96a8486 working perfectly well with the search and debug in the status line when enabled 2025-06-13 20:46:33 +02:00
filipriec
b8e6cc22af way better debugging in the status line now 2025-06-13 16:57:58 +02:00
filipriec
634a01f618 service search changed 2025-06-13 16:53:39 +02:00
filipriec
6abea062ba ui debug in status line 2025-06-13 15:26:45 +02:00
filipriec
f50887a326 outputting to the status line 2025-06-13 13:38:40 +02:00
filipriec
3c0af05a3c the search tui is not working yet 2025-06-11 22:08:23 +02:00
filipriec
c9131d4457 working but not properly displaying search results 2025-06-11 16:46:55 +02:00
filipriec
2af79a3ef2 search added, but unable to trigger it yet 2025-06-11 16:24:42 +02:00
filipriec
afd9228efa json in the otput of the tantivy 2025-06-11 14:07:22 +02:00
filipriec
495d77fda5 4 ngram tokenizer, not doing anything elsekeeping this as is 2025-06-10 23:56:31 +02:00
filipriec
679bb3b6ab search in common module, now fixing layer mixing issue 2025-06-10 13:47:18 +02:00
filipriec
350c522d19 better search but still has some flaws. It at least works, even tho its not perfect. Needs more testing, but im pretty happy with it rn, keeping it this way 2025-06-10 00:22:31 +02:00
filipriec
4760f42589 slovak language tokenized search 2025-06-09 16:36:18 +02:00
filipriec
50d15e321f automatic indexing is working perfectly well 2025-06-08 23:26:13 +02:00
filipriec
a3e7fd8f0a forgotten changes to the lib that are needed for a single port of two crates working separately 2025-06-08 22:40:46 +02:00
filipriec
645172747a we are now running search server at the same port as the whole backend service 2025-06-08 21:53:48 +02:00
filipriec
7c4ac1eebc search via tantivy on different grpc port works perfectly well now 2025-06-08 21:28:10 +02:00
filipriec
4b4301ad49 fixed now it all compiled successfuly 2025-06-08 20:14:44 +02:00
filipriec
b60e03eb70 search crate compiled, lets get to fixing all the other errors 2025-06-08 20:10:57 +02:00
filipriec
2c7bda3ff1 search crate created 2025-06-08 16:25:56 +02:00
filipriec
eeaaa3635b crucial dialog reloading bug fixed for good(hardest bug had a single line of code fix) 2025-06-08 10:53:46 +02:00
filipriec
e61cbb3956 features ui debug is now working perfectly well, it debugs the rerender flags 2025-06-08 09:26:56 +02:00
filipriec
f9841f2ef3 centralizing logic in the formstate 2025-06-08 00:00:37 +02:00
filipriec
dc232b2523 form is now working as expected 2025-06-07 15:25:35 +02:00
filipriec
b086b3e236 hardcoded firma is being removed part2 2025-06-07 15:12:00 +02:00
filipriec
387e1a0fe0 displaying data properly, fixing hardcoded backend to firma part one 2025-06-07 14:05:35 +02:00
filipriec
08e01d41f2 now properly not displaying in the frontend form fields that should be hidden from the user 2025-06-07 09:37:12 +02:00
filipriec
f5edf52571 working find palette now properly well 2025-06-07 09:16:43 +02:00
filipriec
02c62213c3 making select from the find file to work, not yet working, needs more redesign in how select is working 2025-06-06 23:44:29 +02:00
filipriec
d0722fbbbe working well now, creation of the columns 2025-06-06 20:18:51 +02:00
filipriec
4ec569342d hidden from the user now in the form 2025-06-03 18:47:14 +02:00
filipriec
9540d9ccb9 table definitions are now forbidden for user to allocated rust autoallocated table columns 2025-06-03 18:46:57 +02:00
filipriec
6b5cbe854b now working with the gen schema in the database 2025-06-02 12:39:23 +02:00
filipriec
59ed52814e compiled, needs other fixes 2025-06-02 12:08:16 +02:00
filipriec
3488ab4f6b hardcoded adresar to general form 2025-06-02 10:32:39 +02:00
filipriec
6e2fc5349b code cleanup 2025-05-31 23:02:09 +02:00
filipriec
ea88c2686d tabbing now adds / if there is nothing to tab to 2025-05-30 23:43:49 +02:00
filipriec
3df4baec92 tabbing now works perfectly well 2025-05-30 23:36:53 +02:00
filipriec
ff74e1aaa1 it works amazingly well now, we can select the table name via command line 2025-05-30 22:46:32 +02:00
filipriec
b0c865ab76 workig suggestion menu 2025-05-29 19:46:58 +02:00
filipriec
3dbc086f10 overriding overflows by using empty spaces as letters 2025-05-29 19:32:48 +02:00
filipriec
e9b4b34fb4 fixed height of the find file 2025-05-29 19:02:02 +02:00
filipriec
668eeee197 navigation in the menu but needs refactoring 2025-05-29 16:11:41 +02:00
filipriec
799d8471c9 open menu in command mode now implemented 2025-05-28 19:09:55 +02:00
filipriec
f77c16dec9 temp fix, before implementing C-x C-f 2025-05-28 15:53:33 +02:00
filipriec
45026cac6a table schema is gen now 2025-05-28 15:40:17 +02:00
filipriec
edf6ab5bca gen schema being created 2025-05-28 13:10:08 +02:00
filipriec
462b1f14e2 generated tables are now in gen schema, breaking change, needs crucial fixes NOW 2025-05-27 22:21:40 +02:00
filipriec
7a8f18b116 cargo fix 2025-05-26 22:28:58 +02:00
filipriec
d255e4abb6 proper postiion of the cursor when using sql 2025-05-26 20:53:05 +02:00
filipriec
b770240f0d better autocomplete 2025-05-26 20:43:58 +02:00
filipriec
43b064673b autocomplete is now powerful 2025-05-26 20:22:47 +02:00
filipriec
bf2726c151 tablenames added properly well 2025-05-26 19:51:48 +02:00
filipriec
f3cd921c76 we are suggesting properly table column names now 2025-05-26 19:42:23 +02:00
filipriec
913f6b6b64 broken autocomplete in the add_logic, but its usable, we are keeping it as is, there is nothing more we can do 2025-05-26 16:37:01 +02:00
filipriec
3463a52960 working autocomplete, need more fixes soon 2025-05-26 11:54:28 +02:00
filipriec
116db3566f intro buffer can be killed now also 2025-05-25 22:37:27 +02:00
filipriec
32210a5f7c killing of the buffer now works amazingly well 2025-05-25 22:24:26 +02:00
filipriec
d8f9372bbd killing buffers 2025-05-25 22:02:18 +02:00
filipriec
6e1997fd9d storage in the system is now storing log in details properly well 2025-05-25 21:33:24 +02:00
filipriec
4e7213d1aa automcomplete running and working now 2025-05-25 19:26:30 +02:00
filipriec
5afb427bb4 neccessary hardcode changes to fix the last changes introducing bug. general solution soon 2025-05-25 19:16:42 +02:00
filipriec
685361a11a server table structure response is now generalized 2025-05-25 18:57:13 +02:00
filipriec
bd7c97ca91 required table to access logic 2025-05-25 17:53:06 +02:00
filipriec
81235c67dc add script now has a proper way of doing things 2025-05-25 15:46:06 +02:00
filipriec
65e8e03224 better and better add script 2025-05-25 15:27:41 +02:00
filipriec
85eb3adec7 logic is being implemented properly well 2025-05-25 15:09:38 +02:00
filipriec
5d0f958a68 working cursor tracking in the add_table 2025-05-25 14:08:50 +02:00
filipriec
b82f50b76b movement in the add_table now fixed 2025-05-25 12:40:19 +02:00
filipriec
0ab11a9bf9 add table movement adjustements 2025-05-25 12:27:30 +02:00
filipriec
d28c310704 forbid jump from the last to the first and vice versa 2025-05-25 11:39:25 +02:00
filipriec
2e1d7fdf2b admin panel fixed completely 2025-05-25 11:26:19 +02:00
filipriec
82e96f7b86 vim or default mode workin properly now 2025-05-24 19:25:35 +02:00
filipriec
7229e2abbd weird highlight is gone 2025-05-24 18:53:06 +02:00
filipriec
4e35043da0 vim keybinings are now working properly well 2025-05-24 18:41:41 +02:00
filipriec
56fe1c2ccc text area working now perfectly well 2025-05-24 16:34:59 +02:00
filipriec
a874edf2a1 text area on focus is now big 2025-05-24 14:24:19 +02:00
filipriec
9d55ec3e43 h and l movements are now working 2025-05-23 16:59:12 +02:00
filipriec
05580ac978 better and better 2025-05-23 15:40:16 +02:00
filipriec
667eb4809d compiled but the readonly and edit mode is not working 2025-05-23 15:22:21 +02:00
filipriec
58fdaa8298 add logic, not working tho 2025-05-23 13:34:49 +02:00
filipriec
5478a2ac27 server changes for the ID in the tree 2025-05-23 13:34:39 +02:00
filipriec
ad37990da9 server is being licensed as AGPL instead of GPL 2025-05-21 10:29:22 +02:00
filipriec
66824030f2 refresh of the admin panel after adding the table 2025-04-23 12:31:47 +02:00
filipriec
90ca8cf97c working dialog is now at the correct place 2025-04-23 12:13:56 +02:00
filipriec
3c8ea28da1 dialog on add table save working 2025-04-23 12:04:54 +02:00
filipriec
5c352eb863 tracing running only when enabled 2025-04-23 11:29:00 +02:00
filipriec
8c312bc163 tracing on add_table 2025-04-23 11:02:17 +02:00
filipriec
6fa8b06063 grpc post request to the table definition from add table, not working, major bug, needs debugging to make it work 2025-04-22 23:22:59 +02:00
filipriec
2992f122bc PROTOBUF CHANGED I HOPE IT WAS ONLY A MISTAKE OTHERWISE IM SCREWING SOMETHING UP AND I HAVE NO CLUE WHAT 2025-04-22 22:38:52 +02:00
filipriec
e507993065 ui imporvements 2025-04-22 21:21:49 +02:00
filipriec
6f22aad6f4 working perfectly well admin panel for admin 2025-04-22 18:17:41 +02:00
filipriec
097264040f admin_panel for admins have now some adjustements 2025-04-22 18:03:22 +02:00
filipriec
2c03ee6af0 add_table ready to be used as a post request, only small changes are needed 2025-04-22 17:36:00 +02:00
filipriec
ec596b2ada indexing now works amazingly well 2025-04-22 16:03:16 +02:00
filipriec
b01ba0b2d9 cargo fix on the server 2025-04-19 19:00:14 +02:00
filipriec
f74a6ef093 upgraded to tonic 0.13 2025-04-19 18:55:26 +02:00
filipriec
ee687fafbe upgrading and updating my repo 2025-04-19 18:36:26 +02:00
filipriec
60ba17cfea TCP connection creation overhead fixed by cloning once created TCP connection. Huge performance gain on login and register. Utilizing gRPC 2025-04-19 15:54:58 +02:00
filipriec
8b3aa5891e sidebar redesigned correctly and properly 2025-04-19 00:53:57 +02:00
filipriec
3ff9399b81 cargo fix 2025-04-18 23:27:14 +02:00
filipriec
d18f7862ab login refactored 2025-04-18 23:26:48 +02:00
filipriec
dc6c1ce43c refactor happend and its perfectly fine 2025-04-18 23:16:22 +02:00
filipriec
8d1adccec6 async registration working 2025-04-18 22:36:30 +02:00
filipriec
420ce71fb2 asnyc gRPC call that needs nonblocking waiting operation with redraws docs describing setup on what to do 2025-04-18 22:15:08 +02:00
filipriec
8e5a269ff0 finally working amazingly well 2025-04-18 22:09:07 +02:00
filipriec
f357d6f0ee working blocked constant redraws 2025-04-18 21:43:54 +02:00
filipriec
a0467d17a8 cleanup 2025-04-18 21:11:49 +02:00
filipriec
ef3ecfc73f we compiled 2025-04-18 21:04:36 +02:00
filipriec
d3fcb23e22 fixing, nothing works lmao 2025-04-18 20:48:39 +02:00
filipriec
5a029283a1 anyhow used 2025-04-18 19:04:05 +02:00
filipriec
09ccad2bd4 time for changing it all 2025-04-18 18:11:12 +02:00
filipriec
bdcc10bd40 auth verification of emptiness 2025-04-18 17:08:38 +02:00
filipriec
2a1fafc3f9 warnings fixed 2025-04-18 16:17:02 +02:00
filipriec
6010b9a0af fixing warnings 2025-04-18 15:50:47 +02:00
filipriec
11e8f87fe6 cargo fix 2025-04-18 14:59:34 +02:00
filipriec
14b81cba19 fixed, removed log library 2025-04-18 14:58:05 +02:00
filipriec
2b37de3b4d login waiting dialog works, THIS COMMIT NEEDS TO BE REFACTORED 2025-04-18 14:44:04 +02:00
filipriec
73d9a6367c canvas edit mode movement fixed 2025-04-18 13:10:39 +02:00
filipriec
c90233b56f dialog in add_table is now working properly well 2025-04-18 12:29:29 +02:00
filipriec
39dcf38462 add_table dialog is now working properly well 2025-04-18 12:20:08 +02:00
filipriec
efa27cd2dd highlight restored 2025-04-18 11:35:15 +02:00
filipriec
ca231964f2 fullscreen on enter for add_table 2025-04-18 11:31:50 +02:00
filipriec
2bb83cb990 gui changes 2025-04-18 11:29:11 +02:00
filipriec
305bcfcf62 deletion of the selected works 2025-04-18 11:15:15 +02:00
filipriec
92a9011f27 buttons are only border and text colors now in add_table 2025-04-18 11:04:13 +02:00
filipriec
e64cebdfc2 deselect have no highlight now 2025-04-18 10:52:18 +02:00
filipriec
0db426d278 h l movement in add_table fixed for now 2025-04-18 10:48:52 +02:00
filipriec
6bfef1c7a0 exit in the general mode is on esc and select is not escaping in the add_table anymore 2025-04-18 10:37:52 +02:00
filipriec
f50fe788cb scrolling in the add table doesnt highlight first item anymore 2025-04-18 09:27:28 +02:00
filipriec
4db78ecf1b working properly well to distinguish enter in the edit mode now 2025-04-18 00:15:34 +02:00
filipriec
f22dd7749f proper scroll behaviour on the page now 2025-04-17 23:37:58 +02:00
filipriec
75af0c3be1 the profile name is now passed from admin panel to the add table page 2025-04-17 21:59:19 +02:00
filipriec
6f36b84f85 delete selected button now in the add table page working 2025-04-17 21:21:37 +02:00
filipriec
e723198b72 best ever, working as intended 2025-04-17 21:07:07 +02:00
filipriec
8f74febff1 combined, full width need a table name that is missing now 2025-04-17 20:41:48 +02:00
filipriec
d9bd6f8e1d working, but full width is not working, lets combine it with the full width now 2025-04-17 20:17:21 +02:00
filipriec
bf55417901 professional layout working 2025-04-17 19:50:48 +02:00
filipriec
9511970a1a removed placeholder 2025-04-17 19:12:28 +02:00
filipriec
5c8557b369 adding stuff to the column 2025-04-17 19:06:54 +02:00
filipriec
5e47c53fcf canvas required fields 2025-04-17 18:28:06 +02:00
filipriec
f7493a8bc4 canvas properly updating table name 2025-04-17 18:17:01 +02:00
filipriec
4f39b93edd logic of the add button, needs redesign 2025-04-17 16:48:03 +02:00
filipriec
ff8b4eb0f6 canvas now working properly well 2025-04-17 15:40:07 +02:00
filipriec
e921862a7f compiled, still not working for canvas 2025-04-17 14:41:09 +02:00
filipriec
4d7177f15a mistake fixed 2025-04-17 14:21:39 +02:00
filipriec
c5d7f56399 readonly and edit functionality to add table 2025-04-17 14:14:36 +02:00
filipriec
57f789290d needs improvements but at least it looks like it exists 2025-04-17 11:53:31 +02:00
filipriec
f34317e504 from template to the working page 2025-04-17 11:11:33 +02:00
filipriec
8159a84447 movement 2025-04-16 23:31:28 +02:00
filipriec
f4db0e384c add table page1 2025-04-16 22:23:30 +02:00
filipriec
69953401b1 NEW PAGE ADD TABLE 2025-04-16 22:07:07 +02:00
filipriec
93a3c246c6 buttons are now added in the admin panel 2025-04-16 21:43:21 +02:00
filipriec
6505e18b0b working admin panel, needs to do buttons for navigation next 2025-04-16 21:22:34 +02:00
filipriec
51ab73014f removing debug statement 2025-04-16 19:19:15 +02:00
filipriec
05d9e6e46b working selection in the admin panel for the admin perfectly well 2025-04-16 19:15:05 +02:00
filipriec
8ea9965ee3 temp debug 2025-04-16 19:10:02 +02:00
filipriec
486df65aa3 better admin panel, main problem not fixed, needs fix now 2025-04-16 18:53:09 +02:00
filipriec
8044696d7c needed fix done 2025-04-16 18:33:00 +02:00
filipriec
04a7d86636 better movement 2025-04-16 16:30:11 +02:00
filipriec
d0e2f31ce8 admin panel improvements 2025-04-16 14:11:52 +02:00
filipriec
50fcb09758 admin panel admin width fixed 2025-04-16 13:58:34 +02:00
filipriec
6d3c09d57a CRUCIAL bug fixed in the admin panel non admin user 2025-04-16 13:43:44 +02:00
filipriec
cc994fb940 admin panel for admin 2025-04-16 13:21:59 +02:00
filipriec
eee12513dd admin panel from scratch 2025-04-16 12:56:57 +02:00
filipriec
055b6a0a43 admin panel bombarded like never before 2025-04-16 12:47:33 +02:00
filipriec
26b899df16 fixed highlight logic 2025-04-16 09:48:39 +02:00
filipriec
afc8e1a1e5 highlight mode to full line highlightmode 2025-04-16 09:08:40 +02:00
filipriec
b6c4d3308d its now using enum fully for the highlight mode 2025-04-16 00:11:41 +02:00
filipriec
af4567aa3d highlight is now working properly well, can keep on going 2025-04-15 23:46:57 +02:00
filipriec
415bc2044d highlight through many lines working 2025-04-15 23:07:55 +02:00
filipriec
91ad2b0caf FIXED CRUCIAL BUG of two same shortcuts defined in the config 2025-04-15 22:28:12 +02:00
filipriec
bc6471fa54 misname fixed, highlight kinda working 2025-04-15 21:38:32 +02:00
filipriec
0704668d8d mistakes in config.toml fixed, needs more fixes before the real implementation 2025-04-15 21:22:13 +02:00
filipriec
2e9f8815d2 HIGHLIGHT MODE 2025-04-15 21:17:04 +02:00
filipriec
f4689125e0 minor changes 2025-04-15 20:28:31 +02:00
filipriec
bbba67a253 buffer for form fix performed 2025-04-15 20:19:30 +02:00
filipriec
800b857e53 buffers are in the layers now 2025-04-15 19:42:12 +02:00
filipriec
921059bed8 buffer logic going hard, we are killing buffers from login and register now 2025-04-15 18:56:11 +02:00
filipriec
e8b585dc07 killing of the buffers now works 2025-04-15 18:43:36 +02:00
filipriec
afce27184a better defaults 2025-04-15 18:37:27 +02:00
filipriec
8d41beef0b killing of the buffer working 2025-04-15 18:33:42 +02:00
filipriec
5e482cd77b buffer close, not kill implemented yet 2025-04-15 18:29:59 +02:00
filipriec
09068fd4e5 FPS counter added 2025-04-15 18:11:26 +02:00
filipriec
d8c2b9089b fixes are now in place properly well 2025-04-15 17:52:58 +02:00
filipriec
bb577bc276 now buffer handling is global but not allowed from edit mode 2025-04-15 16:23:26 +02:00
filipriec
6267e3d593 working switching of the buffers properly well now 2025-04-15 16:03:14 +02:00
filipriec
c091a39802 compiled 2025-04-15 14:12:42 +02:00
filipriec
f94006dd08 buffer its independent, needs fixes 2025-04-15 13:57:17 +02:00
filipriec
f42790980d perfectly working buffer now 2025-04-15 00:18:42 +02:00
filipriec
779683de4b buffer movement is now working as I would only wish it would. Needs layers implementation, will do in the future 2025-04-15 00:10:28 +02:00
filipriec
aa8887318f history of buffers implemented now 2025-04-14 23:58:58 +02:00
filipriec
3ad8dc6490 better buffer list 2025-04-14 23:35:56 +02:00
filipriec
20f9fae141 buffer working 2025-04-14 22:34:22 +02:00
filipriec
ec062bbf24 its on the top of the screen now 2025-04-14 22:17:24 +02:00
filipriec
8745c9ea2f sidebar fixed bugs and buffer implementation 1 2025-04-14 22:14:01 +02:00
filipriec
0917654361 admin panel now distinguish admin and other roles of the user 2025-04-14 21:04:55 +02:00
filipriec
d892d1cfa0 role restricted admin panel 2025-04-14 20:50:02 +02:00
filipriec
e31138c250 DONE we have now intro state separate from others completely 2025-04-14 20:00:02 +02:00
filipriec
7cbe5ce8be intro movement fixed 2025-04-14 16:25:54 +02:00
filipriec
1c31d4cd1c moved introstate into the state folder 2025-04-14 16:24:21 +02:00
filipriec
990ec9317f admin panel tiny improvement. STARTING OF ADMIN PANEL BUILDING 2025-04-14 15:27:26 +02:00
filipriec
718ceac17e cargo fix 2025-04-14 15:04:24 +02:00
filipriec
d154ba6b89 we compiled for now 2025-04-14 14:28:36 +02:00
filipriec
f2a63476b3 continuation of the fixes 2025-04-14 14:05:20 +02:00
filipriec
adcd3b37fa fixing this 2025-04-14 13:23:09 +02:00
filipriec
71dabc1e37 very bald changes, still destroyed 2025-04-14 12:07:15 +02:00
filipriec
2d724876eb going directly into adminstate from appstate for the admin page. DESTROYED 2025-04-14 11:24:56 +02:00
filipriec
1927d1fa4d appstate moved to its folder also 2025-04-13 22:52:15 +02:00
filipriec
d995fab0e4 moved canvasstate to the pages folder 2025-04-13 22:45:26 +02:00
filipriec
b4135c1626 attempt for dropdown generalization, minor change really 2025-04-13 22:14:34 +02:00
filipriec
3d0a9f2082 we successfully compiled and wen from auth state to login state and auth state 2025-04-13 17:47:00 +02:00
filipriec
1dd5f685a6 authstate splitting into login state and auth state, quite before maddness 2025-04-13 17:06:59 +02:00
filipriec
c1c4394f94 ... at the end of the dialog truncation 2025-04-13 14:58:41 +02:00
filipriec
16d9fcdadc only important stuff in the response of the login 2025-04-13 14:09:57 +02:00
filipriec
5b1db01fe6 displaying username and username contained in the jwt now 2025-04-13 14:04:30 +02:00
filipriec
e856e9d6c7 response contains username but jwt is holding username also 2025-04-13 13:45:22 +02:00
filipriec
ad2c783870 improvements on the register.rs 2025-04-13 00:09:12 +02:00
filipriec
5e101bef14 tab is triggering the suggestion dropdown menu, but ctrl+n and ctrl+p only cycle through it 2025-04-12 23:56:14 +02:00
filipriec
149949ad99 better dropdown gui 2025-04-12 22:42:06 +02:00
filipriec
741cc952fa fixed more dropdown menu 2025-04-12 18:06:19 +02:00
filipriec
d2bb7a0bf4 forgotten cargo lock 2025-04-12 17:48:35 +02:00
filipriec
d4d5bcec9e width for the dropdown fixed 2025-04-12 17:41:37 +02:00
filipriec
b4053a7b94 working perfectly well 2025-04-12 17:34:17 +02:00
filipriec
7b27d00972 working suggestions but position is wrong 2025-04-12 16:22:07 +02:00
filipriec
f4d234089f much better, still not it 2025-04-12 15:52:12 +02:00
filipriec
50a329fc0d suggestions on tab, still not working yet 2025-04-12 15:31:29 +02:00
filipriec
6d6df7ca5c still not working autocomplete 2025-04-12 00:13:28 +02:00
filipriec
6b16068706 moved to auth_e functions for dropdown from edit mode file 2025-04-11 23:42:18 +02:00
filipriec
024a0de4af completion not working yet 2025-04-11 22:54:44 +02:00
filipriec
e145d63ba6 fixing autocomplete, bombarded code, nothing in here 2025-04-11 22:39:09 +02:00
filipriec
e7e8b0b3f6 reg proper movement 2025-04-11 21:42:59 +02:00
filipriec
a7389db674 trigger dropdown, not working at all, needs proper implementation, ready for debug 2025-04-11 15:40:50 +02:00
filipriec
cf1aa4fd2a register form now has optional field role 2025-04-11 13:51:05 +02:00
filipriec
0fd2a589eb register with optional role in the post request 2025-04-11 13:37:57 +02:00
filipriec
944131d5a6 from 4e01740a61 till now, fully integrated working register 2025-04-11 08:52:01 +02:00
filipriec
eff46ac9bf working read only mode in login and register properly workig now 2025-04-11 08:50:54 +02:00
filipriec
14f9b254b2 read only mode needs adjustement to 4 fields from 2 2025-04-10 23:04:25 +02:00
filipriec
337d6050ff common mode register fix 2025-04-10 22:18:01 +02:00
filipriec
d36348b84f gRPC implementation of a registration working 2025-04-10 21:14:32 +02:00
filipriec
dfb6f5b375 registration auth services added 2025-04-10 21:00:41 +02:00
filipriec
a9089bc2ff displaying register 2025-04-10 20:01:03 +02:00
filipriec
5f5d690ff3 compiled time to render register properly well 2025-04-10 19:32:49 +02:00
filipriec
431882ece9 register 2 2025-04-10 19:27:04 +02:00
filipriec
b51e76e366 register on the way 2025-04-10 18:50:54 +02:00
filipriec
4e01740a61 fixed exit in the login dialog 2025-04-10 16:34:29 +02:00
filipriec
e729ed9df3 compiled 2025-04-10 16:05:06 +02:00
filipriec
6b241304fb dialog login functionality 2025-04-10 15:36:43 +02:00
filipriec
3ed8764087 cargo fix 2025-04-10 14:45:16 +02:00
filipriec
df61b245ab back to main 2025-04-10 14:42:11 +02:00
filipriec
7a364d654c repaired 2025-04-10 14:22:30 +02:00
filipriec
0d1a0be1a0 dialog movement fixed 2025-04-10 14:13:39 +02:00
filipriec
5da9f5aaf4 better, but more dialog logic is needed 2025-04-10 13:45:36 +02:00
filipriec
2bec0f5850 dialog 2 2025-04-10 13:40:24 +02:00
filipriec
5bc28ec38b dialog 2025-04-10 13:37:15 +02:00
filipriec
8c7a0a1ec0 we compiled centralized system for select 2025-04-10 12:13:04 +02:00
filipriec
a8eef8107b cursor fixed 2025-04-07 23:39:27 +02:00
filipriec
75cd942f39 fixed and now fully functional 2025-04-07 23:06:58 +02:00
filipriec
fc04af148d minor error fix 2025-04-07 22:55:07 +02:00
filipriec
d7d7fd614b fixing more errors, last to go 2025-04-07 22:46:00 +02:00
filipriec
10e9c3ead0 minor changes 2025-04-07 21:32:02 +02:00
filipriec
70678432c6 BREAKING CHANGES updating gRPC based on the enum now 2025-04-07 21:27:01 +02:00
filipriec
bb103fac6c minor changes 2025-04-07 17:36:59 +02:00
filipriec
0e1fc3f5fa cursor is disabled instead of hidden now 2025-04-07 17:23:42 +02:00
filipriec
7830ebdb3b cursor hidden if not active 2025-04-07 17:16:36 +02:00
filipriec
b061dd3395 BREAKING UPDATE COUNT REMOVED FROM THE MAIN LOOP 2025-04-07 16:27:40 +02:00
filipriec
a6f2fa8a88 button highlight now working perfectly well 2025-04-07 14:05:28 +02:00
filipriec
37f12ea6f0 working needs a small fix 2025-04-07 13:24:22 +02:00
filipriec
e29b576102 removed unused imports 2025-04-07 12:19:35 +02:00
filipriec
b1b3cf6136 changes for revert save in the readonly mode and not only the edit mode finished. 2025-04-07 12:18:03 +02:00
filipriec
173c4c98b8 feat: Prevent form navigation with unsaved changes 2025-04-07 12:03:17 +02:00
filipriec
5a3067c8e5 edit to read_only mode without save 2025-04-07 10:13:39 +02:00
filipriec
803c748738 only to the top and to the bottom in the canvas, cant jump around anymore 2025-04-06 23:16:15 +02:00
filipriec
5879b40e8c exit menu buttons upon success 2025-04-05 19:42:29 +02:00
filipriec
c3decdac13 working dialog now better 2025-04-05 19:37:51 +02:00
filipriec
1baf89dde6 login with dialog menu 2025-04-05 17:52:37 +02:00
filipriec
0219dc0ede border fixed 2025-04-05 17:34:26 +02:00
filipriec
1c662888c6 minor changes 2025-04-05 17:21:07 +02:00
filipriec
7fd1c1e41d working inputting characters 2025-04-05 13:34:38 +02:00
filipriec
1eaa716f42 working now, able to switch between form_e and auth_e 2025-04-05 00:42:56 +02:00
filipriec
704bb7401a copied form_e to auth_e 2025-04-04 23:37:19 +02:00
filipriec
b0c3fdbdc6 added in here src/functions/modes/read_only/auth_ro.rs missing functions, not working tho 2025-04-04 23:34:56 +02:00
filipriec
1d0ceb7045 minor changes 2025-04-04 23:28:30 +02:00
filipriec
ce85a050d3 command mode handler moved 2025-04-04 22:34:04 +02:00
filipriec
94ecbcd639 moved common to common_mode 2025-04-04 20:49:12 +02:00
filipriec
ef1d8bcc9c fully moved now 2025-04-04 20:39:23 +02:00
filipriec
1a7bbdc541 moving back to tui nonmode functions for now 2025-04-04 20:37:02 +02:00
filipriec
3dc5a89459 splitting modes into functions and how it works vs logic to handle it all 2025-04-03 23:23:25 +02:00
filipriec
9f6268dbc9 minor change 2025-04-03 23:00:34 +02:00
filipriec
2e912dd261 moved to form_ro 2025-04-03 22:12:20 +02:00
filipriec
6d6fff474f switching now execute action functions left to right 2025-04-03 20:04:01 +02:00
filipriec
e725c70570 splitting read_only mode 2025-04-03 19:50:35 +02:00
filipriec
07aeecbbfc functions created and now working, lets move modes functions here now 2025-04-03 19:43:59 +02:00
filipriec
9bfac04c44 modes functions dir 2025-04-03 12:34:37 +02:00
filipriec
6acf0d6378 new function dir for functions 2025-04-03 12:32:53 +02:00
filipriec
1a09624242 e fixed 2025-04-02 22:26:35 +02:00
filipriec
9e36385e63 decimated edit mode also 2025-04-02 21:13:18 +02:00
filipriec
43edadde0c trying restoring functionality we are not in there correctly yet 2025-04-02 21:06:49 +02:00
filipriec
d577ff6715 edit mode is like a readonly mode 2025-03-31 23:56:58 +02:00
filipriec
ca75c90ee9 changed read_only mode also 2025-03-31 23:37:02 +02:00
filipriec
8e0fae26ee we compiled 2025-03-31 22:08:39 +02:00
filipriec
bee95c3755 fixes, still 2 errors remaining 2025-03-31 20:10:53 +02:00
filipriec
306f4de14f smart way, but introduced many errors 2025-03-31 17:55:53 +02:00
filipriec
b2fc681e73 cargo fix 2025-03-31 17:16:10 +02:00
filipriec
060cff3f0f compiled 2025-03-31 17:10:51 +02:00
filipriec
07c48985e4 proper pass of the arguments 2025-03-31 16:25:11 +02:00
filipriec
3d266c2ad0 switching of the views done 2025-03-31 16:23:25 +02:00
filipriec
e2c326bf1e login gRPC 2025-03-31 16:16:07 +02:00
filipriec
7f5b671084 switch between login or form in the save request 2025-03-31 15:55:54 +02:00
filipriec
3ccf71ed0f switcher, have bugs 2025-03-31 15:42:48 +02:00
filipriec
efb59c5571 split config in modes/canvas is fixed and working properly well 2025-03-31 15:12:50 +02:00
filipriec
26cf06df14 needs fixes of the errors 2025-03-31 14:58:10 +02:00
filipriec
c82b62f72b moving modes/canvas/common.rs into the functions 2025-03-31 13:22:08 +02:00
filipriec
87cdb8048d we compiled fully, time to move grpc calls now 2025-03-31 12:07:01 +02:00
filipriec
f71498703a implementation of login 2025-03-31 10:40:34 +02:00
filipriec
94eea47b76 cargo fix 2025-03-31 10:24:20 +02:00
filipriec
ac833294c4 moved login from grpc_client 2025-03-31 10:23:23 +02:00
filipriec
81f0527085 ui.rs gRPC code removed 2025-03-31 09:54:16 +02:00
filipriec
dd4d9e88c6 ready to move gRPC into a single services folder, time to do the changes now 2025-03-31 09:39:47 +02:00
filipriec
f66d67c238 grpc_service moved 2025-03-31 07:13:24 +02:00
filipriec
9cf25afa52 moving grpc_client, needs import fixes 2025-03-31 07:08:51 +02:00
filipriec
2ed2419f9e Add auth service client and auth state fields 2025-03-30 21:44:45 +02:00
filipriec
e19b30f4f4 auth dialog implemented 2025-03-30 20:38:09 +02:00
filipriec
f6e21b6a61 is edit mode passing properly 2025-03-30 19:03:58 +02:00
filipriec
2180d8decf step2 compiled 2025-03-30 18:41:27 +02:00
filipriec
12aec72141 added fields to the traits 2025-03-30 18:33:21 +02:00
filipriec
0568441e46 minor ui stuff 2025-03-30 18:29:33 +02:00
filipriec
301189bd85 login is not dependent on form/form.rs and only on canvas now 2025-03-30 17:00:51 +02:00
filipriec
81767f376f button sizes 2025-03-30 16:35:38 +02:00
filipriec
e36b1817bc working, split config where functions for each page are defined for this page, if the functions are not general for each page. Huge update, works for tui/functions/form and fui/functions/login. Working, time to move to other things 2025-03-30 15:46:49 +02:00
filipriec
13d4db6bdc properly handling up and down in the form and login, login needs logic implementation 2025-03-30 15:11:02 +02:00
filipriec
b5a5ebd7c0 working exactly as i want, now making the login up and down to work properly well 2025-03-30 15:03:12 +02:00
filipriec
6e04c1f267 original form fully working in the tui functions is the logic for the form 2025-03-30 14:24:12 +02:00
filipriec
a0d96cb87a moving to tui/functions/form.rs read_only functions 2025-03-30 14:13:07 +02:00
filipriec
4052a5b81b fixed unused imports 2025-03-30 14:01:08 +02:00
filipriec
ed99ebf541 compiled if statement in the read_only mode 2025-03-30 13:57:22 +02:00
filipriec
0a4f59cf8e reverting back fully 2025-03-30 13:15:51 +02:00
filipriec
fd6a9b73be reverting back 2025-03-30 13:11:04 +02:00
filipriec
dc994f6ee1 login in the functions 2025-03-30 13:04:48 +02:00
filipriec
e234dd1785 down and up now added to work in the original form 2025-03-30 12:09:20 +02:00
filipriec
6e0943f0cc compiled 2025-03-30 00:51:11 +01:00
filipriec
9622e0bd3c moving read_only functions for the form specific 2025-03-29 22:36:03 +01:00
filipriec
ee666e91ed adjusted login not working yet, wrong changes 2025-03-29 00:09:06 +01:00
filipriec
4f8f2f4a40 step4 2025-03-28 20:48:06 +01:00
filipriec
f21953147b step 3 compiled 2025-03-28 14:35:16 +01:00
filipriec
48b2658b55 step2 2025-03-28 14:29:36 +01:00
filipriec
37b08fdd10 implementation of canvas for multiple pages step 1 2025-03-28 14:26:18 +01:00
filipriec
d4efdf4833 login improvements 2025-03-27 12:27:43 +01:00
filipriec
b408abe8c6 quick prod check .sqlx 2025-03-26 00:50:25 +01:00
filipriec
722c63af54 fixed the navigation previous function, therefore we are now moving into the login properly, lets implement the auth now 2025-03-26 00:46:59 +01:00
filipriec
4601ba4094 unused stuff removed 2025-03-26 00:43:42 +01:00
filipriec
3e2b8a36df fixing warnings 2025-03-26 00:39:27 +01:00
filipriec
11214734ae functions are now in the file dedicated to the functions belonging to the page 2025-03-26 00:26:36 +01:00
filipriec
92045a4e67 unused stuff 2025-03-26 00:16:38 +01:00
250 changed files with 3905 additions and 14327 deletions

View File

@@ -4,3 +4,5 @@ RUST_DB_USER=multi_psql_dev
RUST_DB_PASSWORD=3
RUST_DB_HOST=localhost
RUST_DB_PORT=5432
JWT_SECRET=<YOUR-JWT-HERE>

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

6
.gitignore vendored
View File

@@ -1,2 +1,8 @@
/target
.env
/tantivy_indexes
server/tantivy_indexes
steel_decimal/tests/property_tests.proptest-regressions
.direnv/
canvas/*.toml
.aider*

9
.gitmodules vendored Normal file
View File

@@ -0,0 +1,9 @@
[submodule "client"]
path = client
url = git@gitlab.com:filipriec/komp_ac_client.git
[submodule "canvas"]
path = canvas
url = git@gitlab.com:filipriec/tui-canvas.git
[submodule "server"]
path = server
url = git@gitlab.com:filipriec/komp_ac_server.git

2217
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,55 @@
[workspace]
members = ["client", "server", "common"]
members = ["client", "server", "common", "search", "canvas"]
resolver = "2"
[workspace.package]
# TODO: idk how to do the name, fix later
# name = "Multieko2"
version = "0.2.0"
# name = "komp_ac"
version = "0.5.0"
edition = "2021"
license = "GPL-3.0-or-later"
authors = ["Filip Priečinský <filippriec@gmail.com>"]
description = "Poriadny uctovnicky software."
readme = "README.md"
repository = "https://gitlab.com/filipriec/multieko2"
repository = "https://gitlab.com/filipriec/komp_ac"
categories = ["command-line-interface"]
# [workspace.metadata]
# TODO:
# documentation = "https://docs.rs/accounting-client"`
# documentation = "https://docs.rs/accounting-client"
[workspace.dependencies]
# Async and gRPC
tokio = { version = "1.44.2", features = ["full"] }
tonic = "0.13.0"
prost = "0.13.5"
async-trait = "0.1.88"
prost-types = "0.13.0"
# Data Handling & Serialization
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
time = "0.3.41"
# Utilities & Error Handling
anyhow = "1.0.98"
dotenvy = "0.15.7"
lazy_static = "1.5.0"
tracing = "0.1.41"
# Search crate
tantivy = "0.24.1"
# Steel_decimal crate
rust_decimal = { version = "1.37.2", features = ["maths", "serde"] }
rust_decimal_macros = "1.37.1"
thiserror = "2.0.12"
regex = "1.11.1"
# Canvas crate
ratatui = { version = "0.29.0", features = ["crossterm"] }
crossterm = "0.28.1"
toml = "0.8.20"
unicode-width = "0.2.0"
common = { path = "./common" }

View File

@@ -1,5 +1,7 @@
# Hey
This is only work in progress, until release 1.0.0 this is for development use cases only.
I run development like this:
Server:
@@ -12,3 +14,12 @@ Client:
cargo watch -x 'run --package client -- client'
```
Client with tracing:
```
ENABLE_TRACING=1 RUST_LOG=client=debug cargo watch -x 'run --package client -- client'
```
Client with debug that cant be traced
```
cargo run --package client --features ui-debug -- client
```

1
canvas Submodule

Submodule canvas added at 29fdc5a6c7

1
client Submodule

Submodule client added at c1839bd960

View File

@@ -1,19 +0,0 @@
[package]
name = "client"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
common = { path = "../common" }
crossterm = "0.28.1"
dirs = "6.0.0"
dotenvy = "0.15.7"
prost = "0.13.5"
ratatui = "0.29.0"
serde = { version = "1.0.218", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full", "macros"] }
toml = "0.8.20"
tonic = "0.12.3"
tracing = "0.1.41"

View File

@@ -1,70 +0,0 @@
# config.toml
[keybindings]
enter_command_mode = [":", "ctrl+;"]
[keybindings.general]
move_up = ["k", "Up"]
move_down = ["j", "Down"]
next_option = ["l", "Right"]
previous_option = ["h", "Left"]
select = ["Enter"]
toggle_sidebar = ["ctrl+t"]
next_field = ["Tab"]
prev_field = ["Shift+Tab"]
[keybindings.common]
save = ["ctrl+s"]
quit = ["ctrl+q"]
# !!!change to space b r in the future and from edit mode
revert = ["ctrl+r"]
force_quit = ["ctrl+shift+q"]
save_and_quit = ["ctrl+shift+s"]
move_up = ["Up"]
move_down = ["Down"]
toggle_sidebar = ["ctrl+t"]
# MODE SPECIFIC
# READ ONLY MODE
[keybindings.read_only]
enter_edit_mode_before = ["i"]
enter_edit_mode_after = ["a"]
previous_entry = ["left","q"]
next_entry = ["right","1"]
move_left = ["h"]
move_right = ["l"]
move_up = ["k"]
move_down = ["j"]
move_word_next = ["w"]
move_word_end = ["e"]
move_word_prev = ["b"]
move_word_end_prev = ["ge"]
move_line_start = ["0"]
move_line_end = ["$"]
move_first_line = ["gg"]
move_last_line = ["x"]
[keybindings.edit]
exit_edit_mode = ["esc","ctrl+e"]
delete_char_forward = ["delete"]
delete_char_backward = ["backspace"]
next_field = ["tab", "enter"]
prev_field = ["shift+tab", "backtab"]
move_left = ["left"]
move_right = ["right"]
[keybindings.command]
exit_command_mode = ["ctrl+g", "esc"]
command_execute = ["enter"]
command_backspace = ["backspace"]
save = ["w"]
quit = ["q"]
force_quit = ["q!"]
save_and_quit = ["wq"]
revert = ["r"]
[colors]
theme = "dark"
# Options: "light", "dark", "high_contrast"

View File

@@ -1,49 +0,0 @@
client/
├── Cargo.toml
├── config.toml
└── src/
├── main.rs # Entry point with minimal code
├── lib.rs # Core exports
├── app.rs # Application lifecycle and main loop
├── ui/ # UI components and rendering
│ ├── mod.rs
│ ├── theme.rs # Theme definitions (from colors.rs)
│ ├── layout.rs # Layout definitions
│ ├── render.rs # Main render coordinator
│ └── components/ # UI components
│ ├── mod.rs
│ ├── command_line.rs
│ ├── form.rs
│ ├── preview_card.rs
│ └── status_line.rs
├── input/ # Input handling
│ ├── mod.rs
│ ├── handler.rs # Main input handler (lightweight coordinator)
│ ├── commands.rs # Command processing
│ ├── navigation.rs # Navigation between entries and fields
│ └── edit.rs # Edit mode logic
├── editor/ # Text editing functionality
│ ├── mod.rs
│ ├── cursor.rs # Cursor movement
│ └── text.rs # Text manipulation (word movements, etc.)
├── state/ # Application state
│ ├── mod.rs
│ ├── app_state.rs # Main application state
│ └── form_state.rs # Form state
├── model/ # Data models
│ ├── mod.rs
│ └── entry.rs # Entry model with business logic
├── service/ # External services
│ ├── mod.rs
│ ├── terminal.rs # Terminal setup and management
│ └── grpc.rs # gRPC client (extracted from terminal.rs)
└── config/ # Configuration
├── mod.rs
└── keybindings.rs # Keybinding definitions and matching

View File

@@ -1,4 +0,0 @@
// src/components/admin.rs
pub mod admin_panel;
pub use admin_panel::*;

View File

@@ -1,118 +0,0 @@
// src/components/admin/admin_panel.rs
use ratatui::{
widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
style::Style,
text::{Line, Span, Text},
layout::{Alignment, Constraint, Direction, Layout, Rect},
Frame,
};
use common::proto::multieko2::table_definition::ProfileTreeResponse;
use crate::config::colors::themes::Theme;
pub struct AdminPanelState {
pub list_state: ListState,
pub profiles: Vec<String>,
}
impl AdminPanelState {
pub fn new(profiles: Vec<String>) -> Self {
let mut list_state = ListState::default();
if !profiles.is_empty() {
list_state.select(Some(0));
}
Self { list_state, profiles }
}
pub fn next(&mut self) {
let i = self.list_state.selected().map_or(0, |i|
if i >= self.profiles.len() - 1 { 0 } else { i + 1 });
self.list_state.select(Some(i));
}
pub fn previous(&mut self) {
let i = self.list_state.selected().map_or(0, |i|
if i == 0 { self.profiles.len() - 1 } else { i - 1 });
self.list_state.select(Some(i));
}
pub fn render(
&mut self,
f: &mut Frame,
area: Rect,
theme: &Theme,
profile_tree: &ProfileTreeResponse,
selected_profile: &Option<String>,
) {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent))
.style(Style::default().bg(theme.bg));
let inner_area = block.inner(area);
f.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(1)])
.split(inner_area);
// Title
let title = Line::from(Span::styled("Admin Panel", Style::default().fg(theme.highlight)));
let title_widget = Paragraph::new(title).alignment(Alignment::Center);
f.render_widget(title_widget, chunks[0]);
// Content
let content_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(chunks[1]);
// Profile list
let items: Vec<ListItem> = self.profiles.iter()
.map(|p| ListItem::new(Line::from(vec![
Span::styled(
if Some(p) == selected_profile.as_ref() { "" } else { " " },
Style::default().fg(theme.accent)
),
Span::styled(p, Style::default().fg(theme.fg)),
])))
.collect();
let list = List::new(items)
.block(Block::default().title("Profiles"))
.highlight_style(Style::default().bg(theme.highlight).fg(theme.bg));
f.render_stateful_widget(list, content_chunks[0], &mut self.list_state);
// Profile details
if let Some(profile) = self.list_state.selected()
.and_then(|i| profile_tree.profiles.get(i))
{
let mut text = Text::default();
text.lines.push(Line::from(vec![
Span::styled("Profile: ", Style::default().fg(theme.accent)),
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
]));
text.lines.push(Line::from(""));
text.lines.push(Line::from(Span::styled("Tables:", Style::default().fg(theme.accent))));
for table in &profile.tables {
let mut line = vec![Span::styled(format!("├─ {}", table.name), theme.fg)];
if !table.depends_on.is_empty() {
line.push(Span::styled(
format!("{}", table.depends_on.join(", ")),
Style::default().fg(theme.secondary)
));
}
text.lines.push(Line::from(line));
}
let details_widget = Paragraph::new(text)
.block(Block::default().title("Details"));
f.render_widget(details_widget, content_chunks[1]);
}
}
}

View File

@@ -1,6 +0,0 @@
// src/components/form.rs
pub mod login;
pub mod register;
pub use login::*;
pub use register::*;

View File

@@ -1,58 +0,0 @@
// src/components/auth/login.rs
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::Style,
widgets::{Block, BorderType, Borders, Paragraph},
Frame,
};
use crate::{
config::colors::themes::Theme,
state::pages::auth::AuthState
};
pub fn render_login(f: &mut Frame, area: Rect, theme: &Theme, state: &mut AuthState) {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent))
.style(Style::default().bg(theme.bg))
.title(" Login ");
let inner_area = block.inner(area);
f.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(40),
Constraint::Length(3),
Constraint::Percentage(40),
])
.split(inner_area);
let button_style = if state.return_selected {
Style::default()
.fg(theme.highlight)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
Style::default().fg(theme.fg)
};
f.render_widget(
Paragraph::new("Return to Intro")
.style(button_style)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Plain)
.border_style(if state.return_selected {
Style::default().fg(theme.accent)
} else {
Style::default().fg(theme.border)
}),
),
chunks[1]
);
}

View File

@@ -1,8 +0,0 @@
// src/components/common.rs
pub mod command_line;
pub mod status_line;
pub mod background;
pub use command_line::*;
pub use status_line::*;
pub use background::*;

View File

@@ -1,15 +0,0 @@
// src/components/handlers/background.rs
use ratatui::{
widgets::{Block},
layout::Rect,
style::Style,
Frame,
};
use crate::config::colors::themes::Theme;
pub fn render_background(f: &mut Frame, area: Rect, theme: &Theme) {
let background = Block::default()
.style(Style::default().bg(theme.bg));
f.render_widget(background, area);
}

View File

@@ -1,35 +0,0 @@
// src/client/components/command_line.rs
use ratatui::{
widgets::{Block, Paragraph},
style::Style,
layout::Rect,
Frame,
};
use crate::config::colors::themes::Theme;
pub fn render_command_line(f: &mut Frame, area: Rect, input: &str, active: bool, theme: &Theme, message: &str) {
let prompt = if active {
":"
} else {
""
};
// Combine the prompt, input, and message
let display_text = if message.is_empty() {
format!("{}{}", prompt, input)
} else {
format!("{}{} | {}", prompt, input, message)
};
let style = if active {
Style::default().fg(theme.accent)
} else {
Style::default().fg(theme.fg)
};
let paragraph = Paragraph::new(display_text)
.block(Block::default().style(Style::default().bg(theme.bg)))
.style(style);
f.render_widget(paragraph, area);
}

View File

@@ -1,80 +0,0 @@
// src/client/components/handlers/status_line.rs
use ratatui::{
widgets::Paragraph,
style::Style,
layout::Rect,
Frame,
text::{Line, Span},
};
use crate::config::colors::themes::Theme;
use std::path::Path;
pub fn render_status_line(
f: &mut Frame,
area: Rect,
current_dir: &str,
theme: &Theme,
is_edit_mode: bool,
) {
// Program name and version
let program_info = format!("multieko2 v{}", env!("CARGO_PKG_VERSION"));
let mode_text = if is_edit_mode {
"[EDIT]"
} else {
"[READ-ONLY]"
};
// Shorten the current directory path
let home_dir = dirs::home_dir().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
let display_dir = if current_dir.starts_with(&home_dir) {
current_dir.replacen(&home_dir, "~", 1)
} else {
current_dir.to_string()
};
// Create the full status line text
let full_text = format!("{} | {} | {}", mode_text, display_dir, program_info);
// Check if the full text fits in the available width
let available_width = area.width as usize;
let mut display_text = if full_text.len() <= available_width {
// If it fits, use the full text
full_text
} else {
// If it doesn't fit, prioritize mode and program info, and show only the directory name
let dir_name = Path::new(current_dir)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(current_dir);
format!("{} | {} | {}", mode_text, dir_name, program_info)
};
// If even the shortened version overflows, truncate it
if display_text.len() > available_width {
display_text = display_text.chars().take(available_width).collect();
}
// Create the status line text using Line and Span
let status_line = Line::from(vec![
Span::styled(mode_text, Style::default().fg(theme.accent)),
Span::styled(" | ", Style::default().fg(theme.border)),
Span::styled(
display_text.split(" | ").nth(1).unwrap_or(""), // Directory part
Style::default().fg(theme.fg),
),
Span::styled(" | ", Style::default().fg(theme.border)),
Span::styled(
program_info,
Style::default()
.fg(theme.secondary)
.add_modifier(ratatui::style::Modifier::BOLD),
),
]);
// Render the status line
let paragraph = Paragraph::new(status_line)
.style(Style::default().bg(theme.bg));
f.render_widget(paragraph, area);
}

View File

@@ -1,4 +0,0 @@
// src/components/form.rs
pub mod form;
pub use form::*;

View File

@@ -1,66 +0,0 @@
// src/components/handlers/form.rs
use ratatui::{
widgets::{Paragraph, Block, Borders},
layout::{Layout, Constraint, Direction, Rect, Margin, Alignment},
style::Style,
Frame,
};
use crate::config::colors::themes::Theme;
use crate::state::pages::form::FormState;
use crate::components::handlers::canvas::render_canvas;
pub fn render_form(
f: &mut Frame,
area: Rect,
form_state: &FormState,
fields: &[&str],
current_field: &usize,
inputs: &[&String],
theme: &Theme,
is_edit_mode: bool,
total_count: u64,
current_position: u64,
) {
// Create Adresar card
let adresar_card = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.border))
.title(" Adresar ")
.style(Style::default().bg(theme.bg).fg(theme.fg));
f.render_widget(adresar_card, area);
// Define inner area
let inner_area = area.inner(Margin {
horizontal: 1,
vertical: 1,
});
// Create main layout
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Min(1),
])
.split(inner_area);
// Render count/position
let count_position_text = format!("Total: {} | Position: {}", total_count, current_position);
let count_para = Paragraph::new(count_position_text)
.style(Style::default().fg(theme.fg))
.alignment(Alignment::Left);
f.render_widget(count_para, main_layout[0]);
// Delegate input handling to canvas
render_canvas(
f,
main_layout[1],
form_state,
fields,
current_field,
inputs,
theme,
is_edit_mode,
);
}

View File

@@ -1,6 +0,0 @@
// src/components/handlers.rs
pub mod canvas;
pub mod sidebar;
pub use canvas::*;
pub use sidebar::*;

View File

@@ -1,89 +0,0 @@
// src/components/handlers/canvas.rs
use ratatui::{
widgets::{Paragraph, Block, Borders},
layout::{Layout, Constraint, Direction, Rect},
style::Style,
text::{Line, Span},
Frame,
prelude::Alignment,
};
use crate::config::colors::themes::Theme;
use crate::state::pages::form::FormState;
pub fn render_canvas(
f: &mut Frame,
area: Rect,
form_state: &FormState,
fields: &[&str],
current_field: &usize,
inputs: &[&String],
theme: &Theme,
is_edit_mode: bool,
) {
// Split area into columns
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(area);
// Input container styling
let input_container = Block::default()
.borders(Borders::ALL)
.border_style(if is_edit_mode {
form_state.has_unsaved_changes.then(|| theme.warning).unwrap_or(theme.accent)
} else {
theme.secondary
})
.style(Style::default().bg(theme.bg));
// Input block dimensions
let input_block = Rect {
x: columns[1].x,
y: columns[1].y,
width: columns[1].width,
height: fields.len() as u16 + 2,
};
f.render_widget(&input_container, input_block);
// Input rows layout
let input_area = input_container.inner(input_block);
let input_rows = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); fields.len()])
.split(input_area);
// Render labels
for (i, field) in fields.iter().enumerate() {
let label = Paragraph::new(Line::from(Span::styled(
format!("{}:", field),
Style::default().fg(theme.fg)),
));
f.render_widget(label, Rect {
x: columns[0].x,
y: input_block.y + 1 + i as u16,
width: columns[0].width,
height: 1,
});
}
// Render inputs and cursor
for (i, input) in inputs.iter().enumerate() {
let is_active = i == *current_field;
let input_display = Paragraph::new(input.as_str())
.alignment(Alignment::Left)
.style(if is_active {
Style::default().fg(theme.highlight)
} else {
Style::default().fg(theme.fg)
});
f.render_widget(input_display, input_rows[i]);
if is_active {
let cursor_x = input_rows[i].x + form_state.current_cursor_pos as u16;
let cursor_y = input_rows[i].y;
f.set_cursor_position((cursor_x, cursor_y));
}
}
}

View File

@@ -1,109 +0,0 @@
// src/components/handlers/sidebar.rs
use ratatui::{
widgets::{Block, List, ListItem},
layout::{Rect, Direction, Layout, Constraint},
style::Style,
Frame,
};
use crate::config::colors::themes::Theme;
use common::proto::multieko2::table_definition::{ProfileTreeResponse};
use ratatui::text::{Span, Line};
// Reduced sidebar width
const SIDEBAR_WIDTH: u16 = 12;
pub fn calculate_sidebar_layout(show_sidebar: bool, main_content_area: Rect) -> (Option<Rect>, Rect) {
if show_sidebar {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(SIDEBAR_WIDTH),
Constraint::Min(0),
])
.split(main_content_area);
(Some(chunks[0]), chunks[1])
} else {
(None, main_content_area)
}
}
pub fn render_sidebar(
f: &mut Frame,
area: Rect,
theme: &Theme,
profile_tree: &ProfileTreeResponse,
selected_profile: &Option<String>,
) {
let sidebar_block = Block::default().style(Style::default().bg(theme.bg));
let mut items = Vec::new();
if let Some(profile_name) = selected_profile {
// Existing code for when a profile is selected...
} else {
// Show full profile tree when no profile is selected (compact version)
for (profile_idx, profile) in profile_tree.profiles.iter().enumerate() {
// Profile header - more compact
items.push(ListItem::new(Line::from(vec![
Span::styled("", Style::default().fg(theme.accent)),
Span::styled(&profile.name, Style::default().fg(theme.highlight)),
])));
// Tables with compact prefixes
for (table_idx, table) in profile.tables.iter().enumerate() {
let is_last_table = table_idx == profile.tables.len() - 1;
let is_last_profile = profile_idx == profile_tree.profiles.len() - 1;
// Shorter prefix characters
let prefix = match (is_last_profile, is_last_table) {
(true, true) => "",
(true, false) => "",
(false, true) => "│└",
(false, false) => "│├",
};
// Get table name without year prefix to save space
let display_name = if table.name.starts_with("2025_") {
&table.name[5..] // Skip "2025_" prefix
} else {
&table.name
};
let mut line = vec![
Span::styled(prefix, Style::default().fg(theme.fg)),
Span::styled(display_name, Style::default().fg(theme.fg)),
];
// Show a simple indicator for dependencies instead of listing them
if !table.depends_on.is_empty() {
line.push(Span::styled(
"",
Style::default().fg(theme.secondary)
));
}
items.push(ListItem::new(Line::from(line)));
}
// Compact separator between profiles
if profile_idx < profile_tree.profiles.len() - 1 {
items.push(ListItem::new(Line::from(
Span::styled("", Style::default().fg(theme.secondary))
)));
}
}
if profile_tree.profiles.is_empty() {
items.push(ListItem::new(Span::styled(
"No profiles",
Style::default().fg(theme.secondary)
)));
}
}
let list = List::new(items)
.block(sidebar_block)
.highlight_style(Style::default().fg(theme.highlight))
.highlight_symbol(">");
f.render_widget(list, area);
}

View File

@@ -1,4 +0,0 @@
// src/components/intro.rs
pub mod intro;
pub use intro::*;

View File

@@ -1,131 +0,0 @@
// src/components/intro/intro.rs
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::Style,
text::{Line, Span},
widgets::{Block, BorderType, Borders, Paragraph},
prelude::Margin,
Frame,
};
use crate::config::colors::themes::Theme;
pub struct IntroState {
pub selected_option: usize,
}
impl IntroState {
pub fn new() -> Self {
Self { selected_option: 0 }
}
pub fn render(&self, f: &mut Frame, area: Rect, theme: &Theme) {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(theme.accent))
.style(Style::default().bg(theme.bg));
let inner_area = block.inner(area);
f.render_widget(block, area);
// Center layout
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(35),
Constraint::Length(7), // Increased to accommodate 3 buttons
Constraint::Percentage(35),
])
.split(inner_area);
// Title
let title = Line::from(vec![
Span::styled("multieko2", Style::default().fg(theme.highlight)),
Span::styled(" v", Style::default().fg(theme.fg)),
Span::styled(env!("CARGO_PKG_VERSION"), Style::default().fg(theme.secondary)),
]);
let title_para = Paragraph::new(title)
.alignment(Alignment::Center);
f.render_widget(title_para, chunks[1]);
// Buttons - now with 3 options
let button_area = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(33),
Constraint::Percentage(33),
Constraint::Percentage(33),
])
.split(chunks[1].inner(Margin {
horizontal: 1,
vertical: 1
}));
self.render_button(
f,
button_area[0],
"Continue",
self.selected_option == 0,
theme,
);
self.render_button(
f,
button_area[1],
"Admin",
self.selected_option == 1,
theme,
);
self.render_button(
f,
button_area[2],
"Login",
self.selected_option == 2,
theme,
);
}
fn render_button(&self, f: &mut Frame, area: Rect, text: &str, selected: bool, theme: &Theme) {
let button_style = if selected {
Style::default()
.fg(theme.highlight)
.bg(theme.bg)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
Style::default().fg(theme.fg).bg(theme.bg)
};
let button = Paragraph::new(text)
.style(button_style)
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Double)
.border_style(if selected {
Style::default().fg(theme.accent)
} else {
Style::default().fg(theme.border)
}),
);
f.render_widget(button, area);
}
pub fn next_option(&mut self) {
self.selected_option = (self.selected_option + 1) % 3;
}
pub fn previous_option(&mut self) {
self.selected_option = if self.selected_option == 0 { 2 } else { self.selected_option - 1 };
}
pub fn handle_selection(&self, app_state: &mut crate::state::state::AppState) {
match self.selected_option {
0 => { /* Continue logic */ }
1 => {}
2 => {}
_ => {}
}
}
}

View File

@@ -1,14 +0,0 @@
// src/components/mod.rs
pub mod handlers;
pub mod intro;
pub mod admin;
pub mod common;
pub mod form;
pub mod auth;
pub use handlers::*;
pub use intro::*;
pub use admin::*;
pub use common::*;
pub use form::*;
pub use auth::*;

View File

@@ -1,7 +0,0 @@
// src/config/binds.rs
pub mod config;
pub mod key_sequences;
pub use config::*;
pub use key_sequences::*;

View File

@@ -1,542 +0,0 @@
// src/config/binds/config.rs
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
use crossterm::event::{KeyCode, KeyModifiers};
#[derive(Debug, Deserialize, Default)]
pub struct ColorsConfig {
#[serde(default = "default_theme")]
pub theme: String,
}
fn default_theme() -> String {
"light".to_string()
}
#[derive(Debug, Deserialize)]
pub struct Config {
#[serde(rename = "keybindings")]
pub keybindings: ModeKeybindings,
#[serde(default)]
pub colors: ColorsConfig,
}
#[derive(Debug, Deserialize)]
pub struct ModeKeybindings {
#[serde(default)]
pub general: HashMap<String, Vec<String>>,
#[serde(default)]
pub read_only: HashMap<String, Vec<String>>,
#[serde(default)]
pub edit: HashMap<String, Vec<String>>,
#[serde(default)]
pub command: HashMap<String, Vec<String>>,
#[serde(default)]
pub common: HashMap<String, Vec<String>>,
#[serde(flatten)]
pub global: HashMap<String, Vec<String>>,
}
impl Config {
/// Loads the configuration from "config.toml" in the client crate directory.
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let config_path = Path::new(manifest_dir).join("config.toml");
let config_str = std::fs::read_to_string(&config_path)
.map_err(|e| format!("Failed to read config file at {:?}: {}", config_path, e))?;
let config: Config = toml::from_str(&config_str)?;
Ok(config)
}
pub fn get_general_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_for_key_in_mode(&self.keybindings.general, key, modifiers)
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
}
/// Common actions for Edit/Read-only modes
pub fn get_common_action(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers)
}
/// Gets an action for a key in Read-Only mode, also checking common keybindings.
pub fn get_read_only_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_for_key_in_mode(&self.keybindings.read_only, key, modifiers)
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers))
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
}
/// Gets an action for a key in Edit mode, also checking common keybindings.
pub fn get_edit_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_for_key_in_mode(&self.keybindings.edit, key, modifiers)
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers))
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
}
/// Gets an action for a key in Command mode, also checking common keybindings.
pub fn get_command_action_for_key(&self, key: KeyCode, modifiers: KeyModifiers) -> Option<&str> {
self.get_action_for_key_in_mode(&self.keybindings.command, key, modifiers)
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.common, key, modifiers))
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers))
}
/// Context-aware keybinding resolution
pub fn get_action_for_current_context(
&self,
is_edit_mode: bool,
command_mode: bool,
key: KeyCode,
modifiers: KeyModifiers
) -> Option<&str> {
match (command_mode, is_edit_mode) {
(true, _) => self.get_command_action_for_key(key, modifiers),
(_, true) => self.get_edit_action_for_key(key, modifiers)
.or_else(|| self.get_common_action(key, modifiers)),
_ => self.get_read_only_action_for_key(key, modifiers)
.or_else(|| self.get_common_action(key, modifiers))
// Add global bindings check for read-only mode
.or_else(|| self.get_action_for_key_in_mode(&self.keybindings.global, key, modifiers)),
}
}
/// Helper function to get an action for a key in a specific mode.
pub fn get_action_for_key_in_mode<'a>(
&self,
mode_bindings: &'a HashMap<String, Vec<String>>,
key: KeyCode,
modifiers: KeyModifiers,
) -> Option<&'a str> {
for (action, bindings) in mode_bindings {
for binding in bindings {
if Self::matches_keybinding(binding, key, modifiers) {
return Some(action.as_str());
}
}
}
None
}
/// Checks if a sequence of keys matches any keybinding.
pub fn matches_key_sequence(&self, sequence: &[KeyCode]) -> Option<&str> {
if sequence.is_empty() {
return None;
}
// Convert key sequence to a string (for simple character sequences).
let sequence_str: String = sequence.iter().filter_map(|key| {
if let KeyCode::Char(c) = key {
Some(*c)
} else {
None
}
}).collect();
if sequence_str.is_empty() {
return None;
}
// Check if this sequence matches any binding in the mode-specific sections.
for (action, bindings) in &self.keybindings.read_only {
for binding in bindings {
if binding == &sequence_str {
return Some(action);
}
}
}
for (action, bindings) in &self.keybindings.edit {
for binding in bindings {
if binding == &sequence_str {
return Some(action);
}
}
}
for (action, bindings) in &self.keybindings.command {
for binding in bindings {
if binding == &sequence_str {
return Some(action);
}
}
}
// Check common keybindings
for (action, bindings) in &self.keybindings.common {
for binding in bindings {
if binding == &sequence_str {
return Some(action);
}
}
}
// Finally check global bindings
for (action, bindings) in &self.keybindings.global {
for binding in bindings {
if binding == &sequence_str {
return Some(action);
}
}
}
None
}
/// Checks if a keybinding matches a key and modifiers.
fn matches_keybinding(
binding: &str,
key: KeyCode,
modifiers: KeyModifiers,
) -> bool {
// For multi-character bindings without modifiers, handle them in matches_key_sequence.
if binding.len() > 1 && !binding.contains('+') {
return match binding.to_lowercase().as_str() {
"left" => key == KeyCode::Left,
"right" => key == KeyCode::Right,
"up" => key == KeyCode::Up,
"down" => key == KeyCode::Down,
"esc" => key == KeyCode::Esc,
"enter" => key == KeyCode::Enter,
"delete" => key == KeyCode::Delete,
"backspace" => key == KeyCode::Backspace,
"tab" => key == KeyCode::Tab,
"backtab" => key == KeyCode::BackTab,
_ => false,
};
}
let parts: Vec<&str> = binding.split('+').collect();
let mut expected_modifiers = KeyModifiers::empty();
let mut expected_key = None;
for part in parts {
match part.to_lowercase().as_str() {
"ctrl" => expected_modifiers |= KeyModifiers::CONTROL,
"shift" => expected_modifiers |= KeyModifiers::SHIFT,
"alt" => expected_modifiers |= KeyModifiers::ALT,
"left" => expected_key = Some(KeyCode::Left),
"right" => expected_key = Some(KeyCode::Right),
"up" => expected_key = Some(KeyCode::Up),
"down" => expected_key = Some(KeyCode::Down),
"esc" => expected_key = Some(KeyCode::Esc),
"enter" => expected_key = Some(KeyCode::Enter),
"delete" => expected_key = Some(KeyCode::Delete),
"backspace" => expected_key = Some(KeyCode::Backspace),
"tab" => expected_key = Some(KeyCode::Tab),
"backtab" => expected_key = Some(KeyCode::BackTab),
":" => expected_key = Some(KeyCode::Char(':')),
part => {
if part.len() == 1 {
let c = part.chars().next().unwrap();
expected_key = Some(KeyCode::Char(c));
}
}
}
}
modifiers == expected_modifiers && Some(key) == expected_key
}
/// Gets an action for a command string.
pub fn get_action_for_command(&self, command: &str) -> Option<&str> {
// First check command mode bindings
for (action, bindings) in &self.keybindings.command {
for binding in bindings {
if binding == command {
return Some(action);
}
}
}
// Then check common bindings
for (action, bindings) in &self.keybindings.common {
for binding in bindings {
if binding == command {
return Some(action);
}
}
}
// Finally check global bindings
for (action, bindings) in &self.keybindings.global {
for binding in bindings {
if binding == command {
return Some(action);
}
}
}
None
}
/// Checks if a key is bound to entering Edit mode (before cursor).
pub fn is_enter_edit_mode_before(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
if let Some(bindings) = self.keybindings.read_only.get("enter_edit_mode_before") {
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
} else {
false
}
}
/// Checks if a key is bound to entering Edit mode (after cursor).
pub fn is_enter_edit_mode_after(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
if let Some(bindings) = self.keybindings.read_only.get("enter_edit_mode_after") {
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
} else {
false
}
}
/// Checks if a key is bound to entering Edit mode.
pub fn is_enter_edit_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
self.is_enter_edit_mode_before(key, modifiers) || self.is_enter_edit_mode_after(key, modifiers)
}
/// Checks if a key is bound to exiting Edit mode.
pub fn is_exit_edit_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
if let Some(bindings) = self.keybindings.edit.get("exit_edit_mode") {
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
} else {
false
}
}
/// Checks if a key is bound to entering Command mode.
/// This method is no longer used in event.rs since we now handle command mode entry only in read-only mode directly.
pub fn is_enter_command_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
if let Some(bindings) = self.keybindings.command.get("enter_command_mode") {
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
} else {
false
}
}
/// Checks if a key is bound to exiting Command mode.
pub fn is_exit_command_mode(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
if let Some(bindings) = self.keybindings.command.get("exit_command_mode") {
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
} else {
false
}
}
/// Checks if a key is bound to executing a command.
pub fn is_command_execute(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
if let Some(bindings) = self.keybindings.command.get("command_execute") {
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
} else {
// Fall back to Enter key if no command_execute is defined.
key == KeyCode::Enter && modifiers.is_empty()
}
}
/// Checks if a key is bound to backspacing in Command mode.
pub fn is_command_backspace(&self, key: KeyCode, modifiers: KeyModifiers) -> bool {
if let Some(bindings) = self.keybindings.command.get("command_backspace") {
bindings.iter().any(|b| Self::matches_keybinding(b, key, modifiers))
} else {
// Fall back to Backspace key if no command_backspace is defined.
key == KeyCode::Backspace && modifiers.is_empty()
}
}
/// Checks if a key is bound to a specific action.
pub fn has_key_for_action(&self, action: &str, key_char: char) -> bool {
// Check all mode-specific keybindings for the action
if let Some(bindings) = self.keybindings.read_only.get(action) {
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
return true;
}
}
if let Some(bindings) = self.keybindings.edit.get(action) {
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
return true;
}
}
if let Some(bindings) = self.keybindings.command.get(action) {
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
return true;
}
}
if let Some(bindings) = self.keybindings.common.get(action) {
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
return true;
}
}
if let Some(bindings) = self.keybindings.global.get(action) {
if bindings.iter().any(|binding| binding == &key_char.to_string()) {
return true;
}
}
false
}
/// This method handles all keybinding formats, both with and without +
pub fn matches_key_sequence_generalized(&self, sequence: &[KeyCode]) -> Option<&str> {
if sequence.is_empty() {
return None;
}
// Get string representations of the sequence
let sequence_str = sequence.iter()
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
.collect::<Vec<String>>()
.join("");
// Add the missing sequence_plus definition
let sequence_plus = sequence.iter()
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
.collect::<Vec<String>>()
.join("+");
// Check for matches in all binding formats across all modes
// First check read_only mode
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.read_only, &sequence_str, &sequence_plus, sequence) {
return Some(action);
}
// Then check edit mode
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.edit, &sequence_str, &sequence_plus, sequence) {
return Some(action);
}
// Then check command mode
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.command, &sequence_str, &sequence_plus, sequence) {
return Some(action);
}
// Then check common keybindings
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.common, &sequence_str, &sequence_plus, sequence) {
return Some(action);
}
// Finally check global bindings
if let Some(action) = self.check_bindings_for_sequence(&self.keybindings.global, &sequence_str, &sequence_plus, sequence) {
return Some(action);
}
None
}
/// Helper method to check a specific mode's bindings against a key sequence
fn check_bindings_for_sequence<'a>(
&self,
mode_bindings: &'a HashMap<String, Vec<String>>,
sequence_str: &str,
sequence_plus: &str,
sequence: &[KeyCode]
) -> Option<&'a str> {
for (action, bindings) in mode_bindings {
for binding in bindings {
let normalized_binding = binding.to_lowercase();
// Check if binding matches any of our formats
if normalized_binding == sequence_str || normalized_binding == sequence_plus {
return Some(action);
}
// Special case for + format in bindings
if binding.contains('+') {
let normalized_sequence = sequence.iter()
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
.collect::<Vec<String>>();
let binding_parts: Vec<&str> = binding.split('+').collect();
if binding_parts.len() == sequence.len() {
let matches = binding_parts.iter().enumerate().all(|(i, part)| {
part.to_lowercase() == normalized_sequence[i].to_lowercase()
});
if matches {
return Some(action);
}
}
}
}
}
None
}
/// Check if the current key sequence is a prefix of a longer binding
pub fn is_key_sequence_prefix(&self, sequence: &[KeyCode]) -> bool {
if sequence.is_empty() {
return false;
}
// Get string representation of the sequence
let sequence_str = sequence.iter()
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
.collect::<Vec<String>>()
.join("");
// Check in each mode if our sequence is a prefix
if self.is_prefix_in_mode(&self.keybindings.read_only, &sequence_str, sequence) {
return true;
}
if self.is_prefix_in_mode(&self.keybindings.edit, &sequence_str, sequence) {
return true;
}
if self.is_prefix_in_mode(&self.keybindings.command, &sequence_str, sequence) {
return true;
}
if self.is_prefix_in_mode(&self.keybindings.common, &sequence_str, sequence) {
return true;
}
if self.is_prefix_in_mode(&self.keybindings.global, &sequence_str, sequence) {
return true;
}
false
}
/// Helper method to check if a sequence is a prefix in a specific mode
fn is_prefix_in_mode(
&self,
mode_bindings: &HashMap<String, Vec<String>>,
sequence_str: &str,
sequence: &[KeyCode]
) -> bool {
for (_, bindings) in mode_bindings {
for binding in bindings {
let normalized_binding = binding.to_lowercase();
// Check standard format
if normalized_binding.starts_with(sequence_str) &&
normalized_binding.len() > sequence_str.len() {
return true;
}
// Check + format
if binding.contains('+') {
let binding_parts: Vec<&str> = binding.split('+').collect();
let sequence_parts = sequence.iter()
.map(|k| crate::config::binds::key_sequences::key_to_string(k))
.collect::<Vec<String>>();
if binding_parts.len() > sequence_parts.len() {
let prefix_matches = sequence_parts.iter().enumerate().all(|(i, part)| {
binding_parts.get(i).map_or(false, |b| b.to_lowercase() == part.to_lowercase())
});
if prefix_matches {
return true;
}
}
}
}
}
false
}
}

View File

@@ -1,172 +0,0 @@
// client/src/config/key_sequences.rs
use crossterm::event::{KeyCode, KeyModifiers};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedKey {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
#[derive(Debug, Clone)]
pub struct KeySequenceTracker {
pub current_sequence: Vec<KeyCode>,
pub last_key_time: Instant,
pub timeout: Duration,
}
impl KeySequenceTracker {
pub fn new(timeout_ms: u64) -> Self {
Self {
current_sequence: Vec::new(),
last_key_time: Instant::now(),
timeout: Duration::from_millis(timeout_ms),
}
}
pub fn reset(&mut self) {
self.current_sequence.clear();
self.last_key_time = Instant::now();
}
pub fn add_key(&mut self, key: KeyCode) -> bool {
// Check if timeout has expired
let now = Instant::now();
if now.duration_since(self.last_key_time) > self.timeout {
self.reset();
}
self.current_sequence.push(key);
self.last_key_time = now;
true
}
pub fn get_sequence(&self) -> Vec<KeyCode> {
self.current_sequence.clone()
}
// Convert a sequence of keys to a string representation
pub fn sequence_to_string(&self) -> String {
self.current_sequence.iter().map(|k| key_to_string(k)).collect()
}
// Convert a sequence to a format with + between keys
pub fn sequence_to_plus_format(&self) -> String {
if self.current_sequence.is_empty() {
return String::new();
}
let parts: Vec<String> = self.current_sequence.iter()
.map(|k| key_to_string(k))
.collect();
parts.join("+")
}
}
// Helper function to convert any KeyCode to a string representation
pub fn key_to_string(key: &KeyCode) -> String {
match key {
KeyCode::Char(c) => c.to_string(),
KeyCode::Left => "left".to_string(),
KeyCode::Right => "right".to_string(),
KeyCode::Up => "up".to_string(),
KeyCode::Down => "down".to_string(),
KeyCode::Esc => "esc".to_string(),
KeyCode::Enter => "enter".to_string(),
KeyCode::Backspace => "backspace".to_string(),
KeyCode::Delete => "delete".to_string(),
KeyCode::Tab => "tab".to_string(),
KeyCode::BackTab => "backtab".to_string(),
KeyCode::Home => "home".to_string(),
KeyCode::End => "end".to_string(),
KeyCode::PageUp => "pageup".to_string(),
KeyCode::PageDown => "pagedown".to_string(),
KeyCode::Insert => "insert".to_string(),
_ => format!("{:?}", key).to_lowercase(),
}
}
// Helper function to convert a string to a KeyCode
pub fn string_to_keycode(s: &str) -> Option<KeyCode> {
match s.to_lowercase().as_str() {
"left" => Some(KeyCode::Left),
"right" => Some(KeyCode::Right),
"up" => Some(KeyCode::Up),
"down" => Some(KeyCode::Down),
"esc" => Some(KeyCode::Esc),
"enter" => Some(KeyCode::Enter),
"backspace" => Some(KeyCode::Backspace),
"delete" => Some(KeyCode::Delete),
"tab" => Some(KeyCode::Tab),
"backtab" => Some(KeyCode::BackTab),
"home" => Some(KeyCode::Home),
"end" => Some(KeyCode::End),
"pageup" => Some(KeyCode::PageUp),
"pagedown" => Some(KeyCode::PageDown),
"insert" => Some(KeyCode::Insert),
s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
_ => None,
}
}
pub fn parse_binding(binding: &str) -> Vec<ParsedKey> {
let mut sequence = Vec::new();
// Handle different binding formats
let parts: Vec<String> = if binding.contains('+') {
// Format with explicit '+' separators like "g+left"
binding.split('+').map(|s| s.to_string()).collect()
} else if binding.contains(' ') {
// Format with spaces like "g left"
binding.split(' ').map(|s| s.to_string()).collect()
} else if is_compound_key(binding) {
// A single compound key like "left" or "enter"
vec![binding.to_string()]
} else {
// Simple character sequence like "gg"
binding.chars().map(|c| c.to_string()).collect()
};
for part in &parts {
if let Some(key) = parse_key_part(part) {
sequence.push(key);
}
}
sequence
}
fn is_compound_key(part: &str) -> bool {
matches!(part.to_lowercase().as_str(),
"esc" | "up" | "down" | "left" | "right" | "enter" |
"backspace" | "delete" | "tab" | "backtab" | "home" |
"end" | "pageup" | "pagedown" | "insert"
)
}
fn parse_key_part(part: &str) -> Option<ParsedKey> {
let mut modifiers = KeyModifiers::empty();
let mut code = None;
if part.contains('+') {
// This handles modifiers like "ctrl+s"
let components: Vec<&str> = part.split('+').collect();
for component in components {
match component.to_lowercase().as_str() {
"ctrl" => modifiers |= KeyModifiers::CONTROL,
"shift" => modifiers |= KeyModifiers::SHIFT,
"alt" => modifiers |= KeyModifiers::ALT,
_ => {
// Last component is the key
code = string_to_keycode(component);
}
}
}
} else {
// Simple key without modifiers
code = string_to_keycode(part);
}
code.map(|code| ParsedKey { code, modifiers })
}

View File

@@ -1,4 +0,0 @@
// src/config/colors.rs
pub mod themes;
pub use themes::*;

View File

@@ -1,68 +0,0 @@
// src/client/themes/colors.rs
use ratatui::style::Color;
#[derive(Debug, Clone)]
pub struct Theme {
pub bg: Color,
pub fg: Color,
pub accent: Color,
pub secondary: Color,
pub highlight: Color,
pub warning: Color,
pub border: Color,
}
impl Theme {
pub fn from_str(theme_name: &str) -> Self {
match theme_name.to_lowercase().as_str() {
"dark" => Self::dark(),
"high_contrast" => Self::high_contrast(),
_ => Self::light(),
}
}
// Default light theme
pub fn light() -> Self {
Self {
bg: Color::Rgb(245, 245, 245), // Light gray
fg: Color::Rgb(64, 64, 64), // Dark gray
accent: Color::Rgb(173, 216, 230), // Pastel blue
secondary: Color::Rgb(255, 165, 0), // Orange for secondary
highlight: Color::Rgb(152, 251, 152), // Pastel green
warning: Color::Rgb(255, 182, 193), // Pastel pink
border: Color::Rgb(220, 220, 220), // Light gray border
}
}
// High-contrast dark theme
pub fn dark() -> Self {
Self {
bg: Color::Rgb(30, 30, 30), // Dark background
fg: Color::Rgb(255, 255, 255), // White text
accent: Color::Rgb(0, 191, 255), // Bright blue
secondary: Color::Rgb(255, 215, 0), // Gold for secondary
highlight: Color::Rgb(50, 205, 50), // Bright green
warning: Color::Rgb(255, 99, 71), // Bright red
border: Color::Rgb(100, 100, 100), // Medium gray border
}
}
// High-contrast light theme
pub fn high_contrast() -> Self {
Self {
bg: Color::Rgb(255, 255, 255), // White background
fg: Color::Rgb(0, 0, 0), // Black text
accent: Color::Rgb(0, 0, 255), // Blue
secondary: Color::Rgb(255, 140, 0), // Dark orange for secondary
highlight: Color::Rgb(0, 128, 0), // Green
warning: Color::Rgb(255, 0, 0), // Red
border: Color::Rgb(0, 0, 0), // Black border
}
}
}
impl Default for Theme {
fn default() -> Self {
Self::light() // Default to light theme
}
}

View File

@@ -1,4 +0,0 @@
// src/config/mod.rs
pub mod binds;
pub mod colors;

View File

@@ -1,10 +0,0 @@
// client/src/lib.rs
pub mod ui;
pub mod tui;
pub mod config;
pub mod state;
pub mod components;
pub mod modes;
pub use ui::run_ui;

View File

@@ -1,10 +0,0 @@
// client/src/main.rs
use client::run_ui;
use dotenvy::dotenv;
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
dotenv().ok();
run_ui().await
}

View File

@@ -1,4 +0,0 @@
// src/client/modes/canvas.rs
pub mod edit;
pub mod common;
pub mod read_only;

View File

@@ -1,181 +0,0 @@
// src/modes/canvas/common.rs
use crossterm::event::{KeyEvent};
use crate::config::binds::config::Config;
use crate::tui::terminal::grpc_client::GrpcClient;
use crate::tui::terminal::core::TerminalCore;
use crate::tui::controls::commands::CommandHandler;
use crate::state::pages::form::FormState;
use crate::state::state::AppState;
use common::proto::multieko2::adresar::{PostAdresarRequest, PutAdresarRequest};
/// Main handler for common core actions
pub async fn handle_core_action(
action: &str,
form_state: &mut FormState,
grpc_client: &mut GrpcClient,
command_handler: &mut CommandHandler,
terminal: &mut TerminalCore,
app_state: &mut AppState,
current_position: &mut u64,
total_count: u64,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
match action {
"save" => {
let message = save(
form_state,
grpc_client,
&mut app_state.ui.is_saved,
current_position,
total_count,
).await?;
Ok((false, message))
},
"force_quit" => {
terminal.cleanup()?;
Ok((true, "Force exiting without saving.".to_string()))
},
"save_and_quit" => {
let message = save(
form_state,
grpc_client,
&mut app_state.ui.is_saved,
current_position,
total_count,
).await?;
terminal.cleanup()?;
Ok((true, format!("{}. Exiting application.", message)))
},
"revert" => {
let message = revert(
form_state,
grpc_client,
current_position,
total_count,
).await?;
Ok((false, message))
},
// We should never hit this case with proper filtering
_ => Ok((false, format!("Core action not handled: {}", action))),
}
}
/// Helper function to check if a key event should trigger a core action
pub fn is_core_action(config: &Config, key_code: crossterm::event::KeyCode, modifiers: crossterm::event::KeyModifiers) -> Option<String> {
// Check for core application actions (save, quit, etc.)
if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.common,
key_code,
modifiers
) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return Some(action.to_string())
},
_ => {} // Other actions are handled by their respective mode handlers
}
}
None
}
/// Shared logic for saving the current form state
pub async fn save(
form_state: &mut FormState,
grpc_client: &mut GrpcClient,
is_saved: &mut bool,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
let is_new = *current_position == total_count + 1;
let message = if is_new {
let post_request = PostAdresarRequest {
firma: form_state.values[0].clone(),
kz: form_state.values[1].clone(),
drc: form_state.values[2].clone(),
ulica: form_state.values[3].clone(),
psc: form_state.values[4].clone(),
mesto: form_state.values[5].clone(),
stat: form_state.values[6].clone(),
banka: form_state.values[7].clone(),
ucet: form_state.values[8].clone(),
skladm: form_state.values[9].clone(),
ico: form_state.values[10].clone(),
kontakt: form_state.values[11].clone(),
telefon: form_state.values[12].clone(),
skladu: form_state.values[13].clone(),
fax: form_state.values[14].clone(),
};
let response = grpc_client.post_adresar(post_request).await?;
let new_total = grpc_client.get_adresar_count().await?;
*current_position = new_total;
form_state.id = response.into_inner().id;
"New entry created".to_string()
} else {
let put_request = PutAdresarRequest {
id: form_state.id,
firma: form_state.values[0].clone(),
kz: form_state.values[1].clone(),
drc: form_state.values[2].clone(),
ulica: form_state.values[3].clone(),
psc: form_state.values[4].clone(),
mesto: form_state.values[5].clone(),
stat: form_state.values[6].clone(),
banka: form_state.values[7].clone(),
ucet: form_state.values[8].clone(),
skladm: form_state.values[9].clone(),
ico: form_state.values[10].clone(),
kontakt: form_state.values[11].clone(),
telefon: form_state.values[12].clone(),
skladu: form_state.values[13].clone(),
fax: form_state.values[14].clone(),
};
let _ = grpc_client.put_adresar(put_request).await?;
"Entry updated".to_string()
};
*is_saved = true;
form_state.has_unsaved_changes = false;
Ok(message)
}
/// Discard changes since last save
pub async fn revert(
form_state: &mut FormState,
grpc_client: &mut GrpcClient,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
let is_new = *current_position == total_count + 1;
if is_new {
// Clear all fields for new entries
form_state.values.iter_mut().for_each(|v| *v = String::new());
form_state.has_unsaved_changes = false;
return Ok("New entry cleared".to_string());
}
let data = grpc_client.get_adresar_by_position(*current_position).await?;
// Update form fields with saved values
form_state.values = vec![
data.firma,
data.kz,
data.drc,
data.ulica,
data.psc,
data.mesto,
data.stat,
data.banka,
data.ucet,
data.skladm,
data.ico,
data.kontakt,
data.telefon,
data.skladu,
data.fax,
];
form_state.has_unsaved_changes = false;
Ok("Changes discarded, reloaded last saved version".to_string())
}

View File

@@ -1,474 +0,0 @@
// src/modes/canvas/edit.rs
// TODO THIS is freaking bloated with functions it never uses REFACTOR 200 LOC can be gone
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use crate::tui::terminal::{
grpc_client::GrpcClient,
};
use crate::config::binds::config::Config;
use crate::state::pages::form::FormState;
use crate::modes::canvas::common;
pub async fn handle_edit_event_internal(
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
ideal_cursor_column: &mut usize,
command_message: &mut String,
is_saved: &mut bool,
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient,
) -> Result<String, Box<dyn std::error::Error>> {
if let Some("enter_command_mode") = config.get_action_for_key_in_mode(&config.keybindings.global, key.code, key.modifiers) {
// Ignore in edit mode and process as normal input
handle_edit_specific_input(key, form_state, ideal_cursor_column);
return Ok(command_message.clone());
}
// Check common actions first
if let Some(action) = config.get_action_for_key_in_mode(&config.keybindings.common, key.code, key.modifiers) {
return execute_common_action(
action,
form_state,
grpc_client,
is_saved,
current_position,
total_count,
).await;
}
if let Some(action) = config.get_edit_action_for_key(key.code, key.modifiers) {
return execute_edit_action(
action,
form_state,
ideal_cursor_column,
grpc_client, // Changed from AppTerminal
is_saved,
current_position,
total_count,
).await;
}
handle_edit_specific_input(
key,
form_state,
ideal_cursor_column,
);
Ok(command_message.clone())
}
async fn execute_common_action(
action: &str,
form_state: &mut FormState,
grpc_client: &mut GrpcClient,
is_saved: &mut bool,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
match action {
"save" => {
common::save(
form_state,
grpc_client,
is_saved,
current_position,
total_count,
).await
},
"revert" => {
common::revert(
form_state,
grpc_client,
current_position,
total_count,
).await
},
"move_up" | "move_down" => {
// Reuse edit mode's existing logic
execute_edit_action(
action,
form_state,
&mut 0, // Dummy ideal_cursor_column (not used here)
grpc_client,
is_saved,
current_position,
total_count,
).await
},
_ => Ok(format!("Common action not handled: {}", action)),
}
}
fn handle_edit_specific_input(
key: KeyEvent,
form_state: &mut FormState,
ideal_cursor_column: &mut usize,
) {
match key.code {
KeyCode::Char(c) => {
let cursor_pos = form_state.current_cursor_pos;
let field_value = form_state.get_current_input_mut();
let mut chars: Vec<char> = field_value.chars().collect();
if cursor_pos <= chars.len() {
chars.insert(cursor_pos, c);
*field_value = chars.into_iter().collect();
form_state.current_cursor_pos = cursor_pos + 1;
*ideal_cursor_column = form_state.current_cursor_pos;
form_state.has_unsaved_changes = true;
}
}
KeyCode::Backspace => {
if form_state.current_cursor_pos > 0 {
let cursor_pos = form_state.current_cursor_pos;
let field_value = form_state.get_current_input_mut();
let mut chars: Vec<char> = field_value.chars().collect();
if cursor_pos <= chars.len() && cursor_pos > 0 {
chars.remove(cursor_pos - 1);
*field_value = chars.into_iter().collect();
form_state.current_cursor_pos = cursor_pos - 1;
*ideal_cursor_column = form_state.current_cursor_pos;
form_state.has_unsaved_changes = true;
}
}
}
KeyCode::Delete => {
let cursor_pos = form_state.current_cursor_pos;
let field_value = form_state.get_current_input_mut();
let chars: Vec<char> = field_value.chars().collect();
if cursor_pos < chars.len() {
let mut new_chars = chars.clone();
new_chars.remove(cursor_pos);
*field_value = new_chars.into_iter().collect();
form_state.has_unsaved_changes = true;
}
}
KeyCode::Tab => {
if key.modifiers.contains(KeyModifiers::SHIFT) {
if form_state.current_field == 0 {
form_state.current_field = form_state.fields.len() - 1;
} else {
form_state.current_field = form_state.current_field.saturating_sub(1);
}
} else {
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
}
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
}
KeyCode::Enter => {
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
}
_ => {}
}
}
async fn execute_edit_action(
action: &str,
form_state: &mut FormState,
ideal_cursor_column: &mut usize,
grpc_client: &mut GrpcClient, // Changed from AppTerminal
is_saved: &mut bool,
current_position: &mut u64,
total_count: u64,
) -> Result<String, Box<dyn std::error::Error>> {
match action {
"save" => {
common::save(
form_state,
grpc_client, // Changed from AppTerminal
is_saved,
current_position,
total_count,
).await
},
"move_left" => {
form_state.current_cursor_pos = form_state.current_cursor_pos.saturating_sub(1);
*ideal_cursor_column = form_state.current_cursor_pos;
Ok("".to_string())
}
"move_right" => {
let current_input = form_state.get_current_input();
if form_state.current_cursor_pos < current_input.len() {
form_state.current_cursor_pos += 1;
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_up" => {
if form_state.current_field == 0 {
form_state.current_field = form_state.fields.len() - 1;
} else {
form_state.current_field = form_state.current_field.saturating_sub(1);
}
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"move_down" => {
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"move_line_start" => {
form_state.current_cursor_pos = 0;
*ideal_cursor_column = form_state.current_cursor_pos;
Ok("".to_string())
}
"move_line_end" => {
let current_input = form_state.get_current_input();
form_state.current_cursor_pos = current_input.len();
*ideal_cursor_column = form_state.current_cursor_pos;
Ok("".to_string())
}
"move_first_line" => {
form_state.current_field = 0;
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("Moved to first line".to_string())
}
"move_last_line" => {
form_state.current_field = form_state.fields.len() - 1;
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("Moved to last line".to_string())
}
// Word movement actions
"move_word_next" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_next_word_start(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos.min(current_input.len());
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_word_end" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_word_end(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos.min(current_input.len());
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_word_prev" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_prev_word_start(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos;
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_word_end_prev" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_prev_word_end(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos;
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
// Edit-specific actions that can be bound to keys
"delete_char_forward" => {
let cursor_pos = form_state.current_cursor_pos;
let field_value = form_state.get_current_input_mut();
let chars: Vec<char> = field_value.chars().collect();
if cursor_pos < chars.len() {
let mut new_chars = chars.clone();
new_chars.remove(cursor_pos);
*field_value = new_chars.into_iter().collect();
form_state.has_unsaved_changes = true;
}
Ok("".to_string())
}
"delete_char_backward" => {
if form_state.current_cursor_pos > 0 {
let cursor_pos = form_state.current_cursor_pos;
let field_value = form_state.get_current_input_mut();
let mut chars: Vec<char> = field_value.chars().collect();
if cursor_pos <= chars.len() && cursor_pos > 0 {
chars.remove(cursor_pos - 1);
*field_value = chars.into_iter().collect();
form_state.current_cursor_pos = cursor_pos - 1;
*ideal_cursor_column = form_state.current_cursor_pos;
form_state.has_unsaved_changes = true;
}
}
Ok("".to_string())
}
"insert_char" => {
// This could be expanded to allow configurable character insertion
// For now, it's a placeholder that would need additional parameters
Ok("Character insertion requires configuration".to_string())
}
"next_field" => {
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"prev_field" => {
if form_state.current_field == 0 {
form_state.current_field = form_state.fields.len() - 1;
} else {
form_state.current_field = form_state.current_field.saturating_sub(1);
}
let current_input = form_state.get_current_input();
let max_cursor_pos = current_input.len();
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
// Fallback for unrecognized actions
_ => Ok(format!("Unknown action: {}", action)),
}
}
// Reuse these character and word navigation helper functions
#[derive(PartialEq)]
enum CharType {
Whitespace,
Alphanumeric,
Punctuation,
}
fn get_char_type(c: char) -> CharType {
if c.is_whitespace() {
CharType::Whitespace
} else if c.is_alphanumeric() {
CharType::Alphanumeric
} else {
CharType::Punctuation
}
}
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || current_pos >= chars.len() {
return current_pos;
}
let mut pos = current_pos;
let initial_type = get_char_type(chars[pos]);
while pos < chars.len() && get_char_type(chars[pos]) == initial_type {
pos += 1;
}
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
pos += 1;
}
pos
}
fn find_word_end(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() {
return 0;
}
if current_pos >= chars.len() - 1 {
return chars.len() - 1;
}
let mut pos = current_pos;
if get_char_type(chars[pos]) == CharType::Whitespace {
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
pos += 1;
}
} else {
let current_type = get_char_type(chars[pos]);
if pos + 1 < chars.len() && get_char_type(chars[pos + 1]) != current_type {
pos += 1;
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
pos += 1;
}
}
}
if pos >= chars.len() {
return chars.len() - 1;
}
let word_type = get_char_type(chars[pos]);
while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == word_type {
pos += 1;
}
pos
}
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || current_pos == 0 {
return 0;
}
let mut pos = current_pos.saturating_sub(1);
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
pos -= 1;
}
if get_char_type(chars[pos]) != CharType::Whitespace {
let word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
pos -= 1;
}
}
pos
}
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || current_pos <= 1 {
return 0;
}
let mut pos = current_pos.saturating_sub(1);
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
pos -= 1;
}
if pos > 0 && get_char_type(chars[pos]) != CharType::Whitespace {
let word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
pos -= 1;
}
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
pos -= 1;
}
if pos > 0 {
pos -= 1;
let prev_word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == prev_word_type {
pos -= 1;
}
while pos < chars.len() - 1 &&
get_char_type(chars[pos + 1]) == prev_word_type {
pos += 1;
}
}
}
pos
}

View File

@@ -1,458 +0,0 @@
// src/modes/handlers/read_only.rs
use crossterm::event::{KeyEvent};
use crate::config::binds::config::Config;
use crate::state::pages::form::FormState;
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::tui::terminal::grpc_client::GrpcClient;
#[derive(PartialEq)]
enum CharType {
Whitespace,
Alphanumeric,
Punctuation,
}
pub async fn handle_read_only_event(
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
key_sequence_tracker: &mut KeySequenceTracker,
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient,
command_message: &mut String,
edit_mode_cooldown: &mut bool,
ideal_cursor_column: &mut usize,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
// Check for entering Edit mode from Read-Only mode
if config.is_enter_edit_mode_before(key.code, key.modifiers) {
*edit_mode_cooldown = true;
*command_message = "Entering Edit mode".to_string();
return Ok((false, command_message.clone()));
}
if config.is_enter_edit_mode_after(key.code, key.modifiers) {
let current_input = form_state.get_current_input();
if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() {
form_state.current_cursor_pos += 1;
*ideal_cursor_column = form_state.current_cursor_pos;
}
*edit_mode_cooldown = true;
*command_message = "Entering Edit mode (after cursor)".to_string();
return Ok((false, command_message.clone()));
}
// Handle Read-Only mode keybindings
if key.modifiers.is_empty() {
key_sequence_tracker.add_key(key.code);
let sequence = key_sequence_tracker.get_sequence();
// Try to match the current sequence against Read-Only mode bindings
if let Some(action) = config.matches_key_sequence_generalized(&sequence) {
let result = execute_action(
action,
form_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
current_position,
total_count,
grpc_client,
).await?;
key_sequence_tracker.reset();
return Ok((false, result));
}
// Check if this might be a prefix of a longer sequence
if config.is_key_sequence_prefix(&sequence) {
return Ok((false, command_message.clone()));
}
// Since it's not part of a multi-key sequence, check for a direct action
if sequence.len() == 1 && !config.is_key_sequence_prefix(&sequence) {
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) {
let result = execute_action(
action,
form_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
current_position,
total_count,
grpc_client,
).await?;
key_sequence_tracker.reset();
return Ok((false, result));
}
}
} else {
// If modifiers are pressed, check for direct key bindings
key_sequence_tracker.reset();
if let Some(action) = config.get_read_only_action_for_key(key.code, key.modifiers) {
let result = execute_action(
action,
form_state,
ideal_cursor_column,
key_sequence_tracker,
command_message,
current_position,
total_count,
grpc_client,
).await?;
return Ok((false, result));
}
}
// Show a helpful message when no binding was found
if !*edit_mode_cooldown {
let default_key = "i".to_string();
let edit_key = config.keybindings.read_only.get("enter_edit_mode_before")
.and_then(|keys| keys.first())
.unwrap_or(&default_key);
*command_message = format!("Read-only mode - press {} to edit", edit_key);
}
Ok((false, command_message.clone()))
}
async fn execute_action(
action: &str,
form_state: &mut FormState,
ideal_cursor_column: &mut usize,
key_sequence_tracker: &mut KeySequenceTracker,
command_message: &mut String,
current_position: &mut u64,
total_count: u64,
grpc_client: &mut GrpcClient,
) -> Result<String, Box<dyn std::error::Error>> {
match action {
"previous_entry" => {
let new_position = current_position.saturating_sub(1);
if new_position >= 1 {
*current_position = new_position;
match grpc_client.get_adresar_by_position(*current_position).await {
Ok(response) => {
form_state.id = response.id;
form_state.values = vec![
response.firma, response.kz, response.drc,
response.ulica, response.psc, response.mesto,
response.stat, response.banka, response.ucet,
response.skladm, response.ico, response.kontakt,
response.telefon, response.skladu, response.fax,
];
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
form_state.has_unsaved_changes = false;
*command_message = format!("Loaded entry {}", *current_position);
}
Err(e) => {
*command_message = format!("Error loading entry: {}", e);
}
}
key_sequence_tracker.reset();
}
Ok(command_message.clone())
}
"next_entry" => {
if *current_position <= total_count {
*current_position += 1;
if *current_position <= total_count {
match grpc_client.get_adresar_by_position(*current_position).await {
Ok(response) => {
form_state.id = response.id;
form_state.values = vec![
response.firma, response.kz, response.drc,
response.ulica, response.psc, response.mesto,
response.stat, response.banka, response.ucet,
response.skladm, response.ico, response.kontakt,
response.telefon, response.skladu, response.fax,
];
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else { 0 };
form_state.current_cursor_pos = std::cmp::min(*ideal_cursor_column, max_cursor_pos);
form_state.has_unsaved_changes = false;
*command_message = format!("Loaded entry {}", *current_position);
}
Err(e) => {
*command_message = format!("Error loading entry: {}", e);
}
}
} else {
form_state.reset_to_empty();
form_state.current_field = 0;
form_state.current_cursor_pos = 0;
*ideal_cursor_column = 0;
*command_message = "New entry mode".to_string();
}
key_sequence_tracker.reset();
}
Ok(command_message.clone())
}
"exit_edit_mode" => {
key_sequence_tracker.reset();
command_message.clear();
Ok("".to_string())
}
"move_left" => {
let current_pos = form_state.current_cursor_pos;
form_state.current_cursor_pos = current_pos.saturating_sub(1);
*ideal_cursor_column = form_state.current_cursor_pos;
Ok("".to_string())
}
"move_right" => {
let current_input = form_state.get_current_input();
let current_pos = form_state.current_cursor_pos;
if !current_input.is_empty() && current_pos < current_input.len() - 1 {
form_state.current_cursor_pos += 1;
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_up" => {
// Change field first
if form_state.current_field == 0 {
form_state.current_field = form_state.fields.len() - 1;
} else {
form_state.current_field = form_state.current_field.saturating_sub(1);
}
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"move_down" => {
// Change field first
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
0
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("".to_string())
}
"move_word_next" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_next_word_start(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1));
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_word_end" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_word_end(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos.min(current_input.len().saturating_sub(1));
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_word_prev" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_prev_word_start(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos;
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_word_end_prev" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
let new_pos = find_prev_word_end(current_input, form_state.current_cursor_pos);
form_state.current_cursor_pos = new_pos;
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("Moved to previous word end".to_string())
}
"move_line_start" => {
form_state.current_cursor_pos = 0;
*ideal_cursor_column = form_state.current_cursor_pos;
Ok("".to_string())
}
"move_line_end" => {
let current_input = form_state.get_current_input();
if !current_input.is_empty() {
form_state.current_cursor_pos = current_input.len() - 1;
*ideal_cursor_column = form_state.current_cursor_pos;
}
Ok("".to_string())
}
"move_first_line" => {
// Change field first
form_state.current_field = 0;
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
current_input.len()
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("Moved to first line".to_string())
}
"move_last_line" => {
// Change field first
form_state.current_field = form_state.fields.len() - 1;
// Get current input AFTER changing field
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1
} else {
current_input.len()
};
form_state.current_cursor_pos = (*ideal_cursor_column).min(max_cursor_pos);
Ok("Moved to last line".to_string())
}
_ => Ok(format!("Unknown action: {}", action)),
}
}
fn get_char_type(c: char) -> CharType {
if c.is_whitespace() {
CharType::Whitespace
} else if c.is_alphanumeric() {
CharType::Alphanumeric
} else {
CharType::Punctuation
}
}
fn find_next_word_start(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || current_pos >= chars.len() {
return current_pos;
}
let mut pos = current_pos;
let initial_type = get_char_type(chars[pos]);
while pos < chars.len() && get_char_type(chars[pos]) == initial_type {
pos += 1;
}
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
pos += 1;
}
pos
}
fn find_word_end(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() {
return 0;
}
if current_pos >= chars.len() - 1 {
return chars.len() - 1;
}
let mut pos = current_pos;
if get_char_type(chars[pos]) == CharType::Whitespace {
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
pos += 1;
}
} else {
let current_type = get_char_type(chars[pos]);
if pos + 1 < chars.len() && get_char_type(chars[pos + 1]) != current_type {
pos += 1;
while pos < chars.len() && get_char_type(chars[pos]) == CharType::Whitespace {
pos += 1;
}
}
}
if pos >= chars.len() {
return chars.len() - 1;
}
let word_type = get_char_type(chars[pos]);
while pos + 1 < chars.len() && get_char_type(chars[pos + 1]) == word_type {
pos += 1;
}
pos
}
fn find_prev_word_start(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || current_pos == 0 {
return 0;
}
let mut pos = current_pos.saturating_sub(1);
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
pos -= 1;
}
if get_char_type(chars[pos]) != CharType::Whitespace {
let word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
pos -= 1;
}
}
pos
}
fn find_prev_word_end(text: &str, current_pos: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() || current_pos <= 1 {
return 0;
}
let mut pos = current_pos.saturating_sub(1);
while pos > 0 && get_char_type(chars[pos]) == CharType::Whitespace {
pos -= 1;
}
if pos > 0 && get_char_type(chars[pos]) != CharType::Whitespace {
let word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == word_type {
pos -= 1;
}
while pos > 0 && get_char_type(chars[pos - 1]) == CharType::Whitespace {
pos -= 1;
}
if pos > 0 {
pos -= 1;
let prev_word_type = get_char_type(chars[pos]);
while pos > 0 && get_char_type(chars[pos - 1]) == prev_word_type {
pos -= 1;
}
while pos < chars.len() - 1 &&
get_char_type(chars[pos + 1]) == prev_word_type {
pos += 1;
}
}
}
pos
}

View File

@@ -1,3 +0,0 @@
// src/client/modes/common.rs
pub mod command_mode;
pub mod highlight;

View File

@@ -1,131 +0,0 @@
// src/modes/handlers/command_mode.rs
use crossterm::event::{KeyEvent, KeyCode, KeyModifiers};
use crate::tui::terminal::grpc_client::GrpcClient;
use crate::config::binds::config::Config;
use crate::state::pages::form::FormState;
use crate::tui::controls::commands::CommandHandler;
use crate::tui::terminal::core::TerminalCore;
use crate::modes::{
canvas::{common},
};
pub async fn handle_command_event(
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
command_input: &mut String,
command_message: &mut String,
grpc_client: &mut GrpcClient,
command_handler: &mut CommandHandler,
terminal: &mut TerminalCore,
current_position: &mut u64,
total_count: u64,
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
// Return value: (should_exit, message, should_exit_command_mode)
// Exit command mode (via configurable keybinding)
if config.is_exit_command_mode(key.code, key.modifiers) {
command_input.clear();
*command_message = "".to_string();
return Ok((false, "".to_string(), true));
}
// Execute command (via configurable keybinding, defaults to Enter)
if config.is_command_execute(key.code, key.modifiers) {
return process_command(
config,
form_state,
command_input,
command_message,
grpc_client,
command_handler,
terminal,
current_position,
total_count,
).await;
}
// Backspace (via configurable keybinding, defaults to Backspace)
if config.is_command_backspace(key.code, key.modifiers) {
command_input.pop();
return Ok((false, "".to_string(), false));
}
// Regular character input - accept any character in command mode
if let KeyCode::Char(c) = key.code {
// Accept regular or shifted characters (e.g., 'a' or 'A')
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
command_input.push(c);
return Ok((false, "".to_string(), false));
}
}
// Ignore all other keys
Ok((false, "".to_string(), false))
}
async fn process_command(
config: &Config,
form_state: &mut FormState,
command_input: &mut String,
command_message: &mut String,
grpc_client: &mut GrpcClient,
command_handler: &mut CommandHandler,
terminal: &mut TerminalCore,
current_position: &mut u64,
total_count: u64,
) -> Result<(bool, String, bool), Box<dyn std::error::Error>> {
// Clone the trimmed command to avoid borrow issues
let command = command_input.trim().to_string();
if command.is_empty() {
*command_message = "Empty command".to_string();
return Ok((false, command_message.clone(), false));
}
// Get the action for the command (now checks global and common bindings too)
let action = config.get_action_for_command(&command)
.unwrap_or("unknown");
match action {
"force_quit" | "save_and_quit" | "quit" => {
let (should_exit, message) = command_handler
.handle_command(action, terminal)
.await?;
command_input.clear();
Ok((should_exit, message, true))
},
"save" => {
let message = common::save(
form_state,
grpc_client,
&mut command_handler.is_saved,
current_position,
total_count,
).await?;
command_input.clear();
return Ok((false, message, true));
},
"revert" => {
let message = common::revert(
form_state,
grpc_client,
current_position,
total_count,
).await?;
command_input.clear();
return Ok((false, message, true));
},
"unknown" => {
let message = format!("Unknown command: {}", command);
command_input.clear();
return Ok((false, message, true));
},
_ => {
let message = format!("Unhandled action: {}", action);
command_input.clear();
return Ok((false, message, true));
}
}
}

View File

@@ -1,2 +0,0 @@
// src/client/modes/general.rs
pub mod navigation;

View File

@@ -1,184 +0,0 @@
// src/modes/general/navigation.rs
use crossterm::event::KeyEvent;
use crate::config::binds::config::Config;
use crate::state::state::AppState;
use crate::state::pages::form::FormState;
pub async fn handle_navigation_event(
key: KeyEvent,
config: &Config,
form_state: &mut FormState,
app_state: &mut AppState,
command_mode: &mut bool,
command_input: &mut String,
command_message: &mut String,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
if let Some(action) = config.get_general_action(key.code, key.modifiers) {
match action {
"move_up" => {
move_up(app_state);
return Ok((false, String::new()));
}
"move_down" => {
let item_count = if app_state.ui.show_intro {
2 // Intro options count
} else {
app_state.profile_tree.profiles.len() // Admin panel items
};
move_down(app_state, item_count);
return Ok((false, String::new()));
}
"next_option" => {
next_option(app_state, 2); // Intro has 2 options
return Ok((false, String::new()));
}
"previous_option" => {
previous_option(app_state);
return Ok((false, String::new()));
}
"select" => {
select(app_state);
return Ok((false, "Selected".to_string()));
}
"toggle_sidebar" => {
toggle_sidebar(app_state);
return Ok((false, format!("Sidebar {}",
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
)));
}
"next_field" => {
next_field(form_state);
return Ok((false, String::new()));
}
"prev_field" => {
prev_field(form_state);
return Ok((false, String::new()));
}
"enter_command_mode" => {
handle_enter_command_mode(command_mode, command_input, command_message);
return Ok((false, String::new()));
}
_ => {}
}
}
Ok((false, String::new()))
}
pub fn move_up(app_state: &mut AppState) {
if app_state.ui.show_intro {
app_state.ui.intro_state.previous_option();
} else if app_state.ui.show_admin {
// Assuming profile_tree.profiles is the list we're navigating
let profile_count = app_state.profile_tree.profiles.len();
if profile_count == 0 {
return;
}
// Use general state for tracking selection in admin panel
if app_state.general.selected_item == 0 {
app_state.general.selected_item = profile_count - 1;
} else {
app_state.general.selected_item = app_state.general.selected_item.saturating_sub(1);
}
}
}
pub fn move_down(app_state: &mut AppState, item_count: usize) {
if app_state.ui.show_intro {
app_state.ui.intro_state.next_option();
} else if app_state.ui.show_admin {
// Assuming profile_tree.profiles is the list we're navigating
let profile_count = app_state.profile_tree.profiles.len();
if profile_count == 0 {
return;
}
app_state.general.selected_item = (app_state.general.selected_item + 1) % profile_count;
}
}
pub fn next_option(app_state: &mut AppState, option_count: usize) {
if app_state.ui.show_intro {
app_state.ui.intro_state.next_option();
} else {
// For other screens that might have options
app_state.general.current_option = (app_state.general.current_option + 1) % option_count;
}
}
pub fn previous_option(app_state: &mut AppState) {
if app_state.ui.show_intro {
app_state.ui.intro_state.previous_option();
} else {
// For other screens that might have options
if app_state.general.current_option == 0 {
// We'd need the option count here, but since it's not passed we can't wrap around correctly
// For now, just stay at 0
} else {
app_state.general.current_option -= 1;
}
}
}
pub fn select(app_state: &mut AppState) {
if app_state.ui.show_intro {
// Handle selection in intro screen
match app_state.ui.intro_state.selected_option {
0 => { // Continue - show form
app_state.ui.show_form = true;
app_state.ui.show_admin = false;
app_state.ui.show_login = false;
}
1 => { // Admin
app_state.ui.show_form = false;
app_state.ui.show_admin = true;
app_state.ui.show_login = false;
}
2 => { // Login
app_state.ui.show_form = false;
app_state.ui.show_admin = false;
app_state.ui.show_login = true;
}
_ => {} // Other options (shouldn't happen)
}
app_state.ui.show_intro = false;
} else if app_state.ui.show_admin {
// Handle selection in admin panel
let profiles = &app_state.profile_tree.profiles;
if !profiles.is_empty() && app_state.general.selected_item < profiles.len() {
// Set the selected profile
app_state.selected_profile = Some(profiles[app_state.general.selected_item].name.clone());
}
}
}
pub fn toggle_sidebar(app_state: &mut AppState) {
app_state.ui.show_sidebar = !app_state.ui.show_sidebar;
}
pub fn next_field(form_state: &mut FormState) {
if !form_state.fields.is_empty() {
form_state.current_field = (form_state.current_field + 1) % form_state.fields.len();
}
}
pub fn prev_field(form_state: &mut FormState) {
if !form_state.fields.is_empty() {
if form_state.current_field == 0 {
form_state.current_field = form_state.fields.len() - 1;
} else {
form_state.current_field -= 1;
}
}
}
pub fn handle_enter_command_mode(
command_mode: &mut bool,
command_input: &mut String,
command_message: &mut String
) {
*command_mode = true;
command_input.clear();
command_message.clear();
}

View File

@@ -1,3 +0,0 @@
// src/client/modes/handlers.rs
pub mod event;
pub mod mode_manager;

View File

@@ -1,248 +0,0 @@
// src/modes/handlers/event.rs
use crossterm::event::{Event, KeyEvent};
use crossterm::cursor::SetCursorStyle;
use crate::tui::terminal::{
core::TerminalCore,
grpc_client::GrpcClient,
};
use crate::tui::controls::commands::CommandHandler;
use crate::config::binds::config::Config;
use crate::state::pages::form::FormState;
use crate::ui::handlers::rat_state::UiStateHandler;
use crate::modes::{
common::{command_mode},
canvas::{edit, read_only, common},
general::navigation,
};
use crate::config::binds::key_sequences::KeySequenceTracker;
use crate::modes::handlers::mode_manager::{ModeManager, AppMode};
pub struct EventHandler {
pub command_mode: bool,
pub command_input: String,
pub command_message: String,
pub is_edit_mode: bool,
pub edit_mode_cooldown: bool,
pub ideal_cursor_column: usize,
pub key_sequence_tracker: KeySequenceTracker,
}
impl EventHandler {
pub fn new() -> Self {
EventHandler {
command_mode: false,
command_input: String::new(),
command_message: String::new(),
is_edit_mode: false,
edit_mode_cooldown: false,
ideal_cursor_column: 0,
key_sequence_tracker: KeySequenceTracker::new(800),
}
}
pub async fn handle_event(
&mut self,
event: Event,
config: &Config,
terminal: &mut TerminalCore,
grpc_client: &mut GrpcClient,
command_handler: &mut CommandHandler,
form_state: &mut FormState,
app_state: &mut crate::state::state::AppState,
total_count: u64,
current_position: &mut u64,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
// Determine current mode based on app state and event handler state
let current_mode = ModeManager::derive_mode(app_state, self);
app_state.update_mode(current_mode);
if let Event::Key(key) = event {
let key_code = key.code;
let modifiers = key.modifiers;
// Handle common actions across all modes
if UiStateHandler::toggle_sidebar(&mut app_state.ui, config, key_code, modifiers) {
return Ok((false, format!("Sidebar {}",
if app_state.ui.show_sidebar { "shown" } else { "hidden" }
)));
}
// Mode-specific handling
match current_mode {
AppMode::General => {
return navigation::handle_navigation_event(
key,
config,
form_state,
app_state,
&mut self.command_mode,
&mut self.command_input,
&mut self.command_message,
).await;
},
AppMode::ReadOnly => {
// Check for mode transitions first
if config.is_enter_edit_mode_before(key_code, modifiers) &&
ModeManager::can_enter_edit_mode(current_mode) {
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
self.command_message = "Edit mode".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok((false, self.command_message.clone()));
}
if config.is_enter_edit_mode_after(key_code, modifiers) &&
ModeManager::can_enter_edit_mode(current_mode) {
let current_input = form_state.get_current_input();
if !current_input.is_empty() && form_state.current_cursor_pos < current_input.len() {
form_state.current_cursor_pos += 1;
self.ideal_cursor_column = form_state.current_cursor_pos;
}
self.is_edit_mode = true;
self.edit_mode_cooldown = true;
self.command_message = "Edit mode (after cursor)".to_string();
terminal.set_cursor_style(SetCursorStyle::BlinkingBar)?;
return Ok((false, self.command_message.clone()));
}
// Check for entering command mode
if let Some(action) = config.get_read_only_action_for_key(key_code, modifiers) {
if action == "enter_command_mode" && ModeManager::can_enter_command_mode(current_mode) {
self.command_mode = true;
self.command_input.clear();
self.command_message.clear();
return Ok((false, String::new()));
}
}
// Check for core application actions (save, quit, etc.)
// ONLY handle a limited subset of core actions here
if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.common,
key_code,
modifiers
) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return common::handle_core_action(
action,
form_state,
grpc_client,
command_handler,
terminal,
app_state,
current_position,
total_count,
).await;
},
_ => {} // For other actions, let the mode-specific handler take care of it
}
}
// Let read_only mode handle its own actions (including navigation from common bindings)
return read_only::handle_read_only_event(
key,
config,
form_state,
&mut self.key_sequence_tracker,
current_position,
total_count,
grpc_client,
&mut self.command_message,
&mut self.edit_mode_cooldown,
&mut self.ideal_cursor_column,
).await;
},
AppMode::Edit => {
// Check for exiting edit mode
if config.is_exit_edit_mode(key_code, modifiers) {
if form_state.has_unsaved_changes {
self.command_message = "Unsaved changes! Use :w to save or :q! to discard".to_string();
return Ok((false, self.command_message.clone()));
}
self.is_edit_mode = false;
self.edit_mode_cooldown = true;
self.command_message = "Read-only mode".to_string();
terminal.set_cursor_style(SetCursorStyle::SteadyBlock)?;
let current_input = form_state.get_current_input();
if !current_input.is_empty() && form_state.current_cursor_pos >= current_input.len() {
form_state.current_cursor_pos = current_input.len() - 1;
self.ideal_cursor_column = form_state.current_cursor_pos;
}
return Ok((false, self.command_message.clone()));
}
// Check for core application actions (save, quit, etc.)
// ONLY handle a limited subset of core actions here
if let Some(action) = config.get_action_for_key_in_mode(
&config.keybindings.common,
key_code,
modifiers
) {
match action {
"save" | "force_quit" | "save_and_quit" | "revert" => {
return common::handle_core_action(
action,
form_state,
grpc_client,
command_handler,
terminal,
app_state,
current_position,
total_count,
).await;
},
_ => {} // For other actions, let the mode-specific handler take care of it
}
}
// Let edit mode handle its own actions (including navigation from common bindings)
let result = edit::handle_edit_event_internal(
key,
config,
form_state,
&mut self.ideal_cursor_column,
&mut self.command_message,
&mut app_state.ui.is_saved,
current_position,
total_count,
grpc_client,
).await?;
self.key_sequence_tracker.reset();
return Ok((false, result));
},
AppMode::Command => {
let (should_exit, message, exit_command_mode) = command_mode::handle_command_event(
key,
config,
form_state,
&mut self.command_input,
&mut self.command_message,
grpc_client,
command_handler,
terminal,
current_position,
total_count,
).await?;
if exit_command_mode {
self.command_mode = false;
}
return Ok((should_exit, message));
}
}
}
// Non-key events or if no specific handler was matched
self.edit_mode_cooldown = false;
Ok((false, self.command_message.clone()))
}
}

View File

@@ -1,50 +0,0 @@
// src/modes/handlers/mode_manager.rs
use crate::state::state::AppState;
use crate::modes::handlers::event::EventHandler;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode {
General, // For intro and admin screens
ReadOnly, // Canvas read-only mode
Edit, // Canvas edit mode
Command, // Command mode overlay
}
pub struct ModeManager;
impl ModeManager {
// Determine current mode based on app state
pub fn derive_mode(app_state: &AppState, event_handler: &EventHandler) -> AppMode {
// Command mode takes precedence if active
if event_handler.command_mode {
return AppMode::Command;
}
// Check UI state flags
if app_state.ui.show_intro || app_state.ui.show_admin {
AppMode::General
} else if app_state.ui.show_form {
if event_handler.is_edit_mode {
AppMode::Edit
} else {
AppMode::ReadOnly
}
} else {
// Fallback
AppMode::General
}
}
// Mode transition rules
pub fn can_enter_command_mode(current_mode: AppMode) -> bool {
!matches!(current_mode, AppMode::Edit) // Can't enter from Edit mode
}
pub fn can_enter_edit_mode(current_mode: AppMode) -> bool {
matches!(current_mode, AppMode::ReadOnly) // Only from ReadOnly
}
pub fn can_enter_read_only_mode(current_mode: AppMode) -> bool {
matches!(current_mode, AppMode::Edit | AppMode::Command)
}
}

View File

@@ -1,10 +0,0 @@
// src/client/modes/mod.rs
pub mod handlers;
pub mod canvas;
pub mod general;
pub mod common;
pub use handlers::*;
pub use canvas::*;
pub use general::*;
pub use common::*;

View File

@@ -1,3 +0,0 @@
// src/state/mod.rs
pub mod state;
pub mod pages;

View File

@@ -1,4 +0,0 @@
// src/state/pages.rs
pub mod form;
pub mod auth;

View File

@@ -1,20 +0,0 @@
// src/state/pages/auth.rs
#[derive(Default)]
pub struct AuthState {
pub return_selected: bool,
pub username: String,
pub password: String,
pub error_message: Option<String>,
}
impl AuthState {
pub fn new() -> Self {
Self {
return_selected: false,
username: String::new(),
password: String::new(),
error_message: None,
}
}
}

View File

@@ -1,73 +0,0 @@
// src/state/pages/form.rs
use crate::config::colors::themes::Theme;
use ratatui::layout::Rect;
use ratatui::Frame;
pub struct FormState {
pub id: i64,
pub fields: Vec<String>, // Use Vec<String> for dynamic field names
pub values: Vec<String>, // Store field values dynamically
pub current_field: usize,
pub has_unsaved_changes: bool,
pub current_cursor_pos: usize,
}
impl FormState {
/// Create a new FormState with dynamic fields.
pub fn new(fields: Vec<String>) -> Self {
let values = vec![String::new(); fields.len()]; // Initialize values for each field
FormState {
id: 0,
fields,
values,
current_field: 0,
has_unsaved_changes: false,
current_cursor_pos: 0,
}
}
pub fn render(
&self,
f: &mut Frame,
area: Rect,
theme: &Theme,
is_edit_mode: bool,
total_count: u64,
current_position: u64,
) {
let fields: Vec<&str> = self.fields.iter().map(|s| s.as_str()).collect();
let values: Vec<&String> = self.values.iter().collect();
crate::components::form::form::render_form(
f,
area,
self,
&fields,
&self.current_field,
&values,
theme,
is_edit_mode,
total_count,
current_position,
);
}
pub fn reset_to_empty(&mut self) {
self.id = 0; // Reset ID to 0 for new entries
self.values.iter_mut().for_each(|v| v.clear()); // Clear all values
self.has_unsaved_changes = false;
}
pub fn get_current_input(&self) -> &str {
self.values
.get(self.current_field)
.map(|s| s.as_str())
.unwrap_or("")
}
pub fn get_current_input_mut(&mut self) -> &mut String {
self.values
.get_mut(self.current_field)
.expect("Invalid current_field index")
}
}

View File

@@ -1,83 +0,0 @@
// src/state/state.rs
use std::env;
use common::proto::multieko2::table_definition::ProfileTreeResponse;
use crate::components::IntroState;
use crate::modes::handlers::mode_manager::AppMode;
pub struct UiState {
pub show_sidebar: bool,
pub is_saved: bool,
pub show_intro: bool,
pub show_admin: bool,
pub show_form: bool,
pub show_login: bool,
pub intro_state: IntroState,
}
pub struct GeneralState {
pub selected_item: usize,
pub current_option: usize,
}
pub struct AppState {
// Core editor state
pub current_dir: String,
pub total_count: u64,
pub current_position: u64,
pub profile_tree: ProfileTreeResponse,
pub selected_profile: Option<String>,
pub current_mode: AppMode,
// UI preferences
pub ui: UiState,
pub general: GeneralState,
}
impl AppState {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let current_dir = env::current_dir()?
.to_string_lossy()
.to_string();
Ok(AppState {
current_dir,
total_count: 0,
current_position: 0,
profile_tree: ProfileTreeResponse::default(),
selected_profile: None,
current_mode: AppMode::General,
ui: UiState::default(),
general: GeneralState {
selected_item: 0,
current_option: 0,
},
})
}
// Existing methods remain unchanged
pub fn update_total_count(&mut self, total_count: u64) {
self.total_count = total_count;
}
pub fn update_current_position(&mut self, current_position: u64) {
self.current_position = current_position;
}
pub fn update_mode(&mut self, mode: AppMode) {
self.current_mode = mode;
}
}
impl Default for UiState {
fn default() -> Self {
Self {
show_sidebar: true,
is_saved: false,
show_intro: true,
show_admin: false,
show_form: false,
show_login: false,
intro_state: IntroState::new(),
}
}
}

View File

@@ -1,5 +0,0 @@
// src/tui/controls.rs
pub mod commands;
pub use commands::*;

View File

@@ -1,54 +0,0 @@
// src/tui/controls/commands.rs
use crate::tui::terminal::core::TerminalCore;
pub struct CommandHandler {
pub is_saved: bool,
}
impl CommandHandler {
pub fn new() -> Self {
Self { is_saved: false }
}
pub async fn handle_command(
&mut self,
action: &str,
terminal: &mut TerminalCore,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
match action {
"quit" => self.handle_quit(terminal).await,
"force_quit" => self.handle_force_quit(terminal).await,
"save_and_quit" => self.handle_save_quit(terminal).await,
_ => Ok((false, format!("Unknown command: {}", action))),
}
}
async fn handle_quit(
&self,
terminal: &mut TerminalCore,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
if self.is_saved {
terminal.cleanup()?;
Ok((true, "Exiting.".into()))
} else {
Ok((false, "No changes saved. Use :q! to force quit.".into()))
}
}
async fn handle_force_quit(
&self,
terminal: &mut TerminalCore,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
terminal.cleanup()?;
Ok((true, "Force exiting without saving.".into()))
}
async fn handle_save_quit(
&mut self,
terminal: &mut TerminalCore,
) -> Result<(bool, String), Box<dyn std::error::Error>> {
self.is_saved = true;
terminal.cleanup()?;
Ok((true, "State saved. Exiting.".into()))
}
}

View File

@@ -1,4 +0,0 @@
// src/tui/mod.rs
pub mod terminal;
pub mod controls;

View File

@@ -1,9 +0,0 @@
// src/tui/terminal.rs
pub mod core;
pub mod grpc_client;
pub mod event_reader;
pub use core::TerminalCore;
pub use grpc_client::GrpcClient;
pub use event_reader::EventReader;

View File

@@ -1,73 +0,0 @@
// src/tui/terminal/core.rs
use crossterm::{
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
cursor::{SetCursorStyle, EnableBlinking, Show, MoveTo},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::{self, stdout, Write};
pub struct TerminalCore {
terminal: Terminal<CrosstermBackend<io::Stdout>>,
}
impl TerminalCore {
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(
stdout,
EnterAlternateScreen,
SetCursorStyle::SteadyBlock,
EnableBlinking
)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(Self { terminal })
}
pub fn draw<F>(&mut self, f: F) -> Result<(), Box<dyn std::error::Error>>
where
F: FnOnce(&mut ratatui::Frame),
{
self.terminal.draw(f)?;
Ok(())
}
pub fn cleanup(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let backend = self.terminal.backend_mut();
execute!(
backend,
Show,
SetCursorStyle::DefaultUserShape,
LeaveAlternateScreen
)?;
disable_raw_mode()?;
backend.flush()?;
execute!(
backend,
crossterm::terminal::Clear(crossterm::terminal::ClearType::All),
MoveTo(0, 0)
)?;
Ok(())
}
pub fn set_cursor_style(
&mut self,
style: SetCursorStyle,
) -> Result<(), Box<dyn std::error::Error>> {
execute!(
self.terminal.backend_mut(),
style,
EnableBlinking,
)?;
Ok(())
}
}
impl Drop for TerminalCore {
fn drop(&mut self) {
let _ = self.cleanup();
}
}

View File

@@ -1,15 +0,0 @@
// src/tui/terminal/event_reader.rs
use crossterm::event::{self, Event};
pub struct EventReader;
impl EventReader {
pub fn new() -> Self {
Self
}
pub fn read_event(&self) -> Result<Event, Box<dyn std::error::Error>> {
Ok(event::read()?)
}
}

View File

@@ -1,68 +0,0 @@
// src/tui/terminal/grpc_client.rs
use tonic::transport::Channel;
use common::proto::multieko2::adresar::adresar_client::AdresarClient;
use common::proto::multieko2::adresar::{AdresarResponse, PostAdresarRequest, PutAdresarRequest};
use common::proto::multieko2::common::{CountResponse, PositionRequest, Empty};
use common::proto::multieko2::table_structure::table_structure_service_client::TableStructureServiceClient;
use common::proto::multieko2::table_structure::TableStructureResponse;
use common::proto::multieko2::table_definition::{
table_definition_client::TableDefinitionClient,
ProfileTreeResponse
};
pub struct GrpcClient {
adresar_client: AdresarClient<Channel>,
table_structure_client: TableStructureServiceClient<Channel>,
table_definition_client: TableDefinitionClient<Channel>,
}
impl GrpcClient {
pub async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let adresar_client = AdresarClient::connect("http://[::1]:50051").await?;
let table_structure_client = TableStructureServiceClient::connect("http://[::1]:50051").await?;
let table_definition_client = TableDefinitionClient::connect("http://[::1]:50051").await?;
Ok(Self {
adresar_client,
table_structure_client,
table_definition_client,
})
}
pub async fn get_adresar_count(&mut self) -> Result<u64, Box<dyn std::error::Error>> {
let request = tonic::Request::new(Empty::default());
let response: CountResponse = self.adresar_client.get_adresar_count(request).await?.into_inner();
Ok(response.count as u64)
}
pub async fn get_adresar_by_position(&mut self, position: u64) -> Result<AdresarResponse, Box<dyn std::error::Error>> {
let request = tonic::Request::new(PositionRequest { position: position as i64 });
let response: AdresarResponse = self.adresar_client.get_adresar_by_position(request).await?.into_inner();
Ok(response)
}
pub async fn post_adresar(&mut self, request: PostAdresarRequest) -> Result<tonic::Response<AdresarResponse>, Box<dyn std::error::Error>> {
let request = tonic::Request::new(request);
let response = self.adresar_client.post_adresar(request).await?;
Ok(response)
}
pub async fn put_adresar(&mut self, request: PutAdresarRequest) -> Result<tonic::Response<AdresarResponse>, Box<dyn std::error::Error>> {
let request = tonic::Request::new(request);
let response = self.adresar_client.put_adresar(request).await?;
Ok(response)
}
pub async fn get_table_structure(&mut self) -> Result<TableStructureResponse, Box<dyn std::error::Error>> {
let request = tonic::Request::new(Empty::default());
let response = self.table_structure_client.get_adresar_table_structure(request).await?;
Ok(response.into_inner())
}
pub async fn get_profile_tree(&mut self) -> Result<ProfileTreeResponse, Box<dyn std::error::Error>> {
let request = tonic::Request::new(Empty::default());
let response = self.table_definition_client.get_profile_tree(request).await?;
Ok(response.into_inner())
}
}

View File

@@ -1,8 +0,0 @@
// src/client/ui/handlers.rs
pub mod ui;
pub mod render;
pub mod rat_state;
pub use ui::run_ui;
pub use rat_state::*;

View File

@@ -1,24 +0,0 @@
// src/ui/handlers/rat_state.rs
use crossterm::event::{KeyCode, KeyModifiers};
use crate::config::binds::config::Config;
use crate::state::state::UiState;
pub struct UiStateHandler;
impl UiStateHandler {
pub fn toggle_sidebar(
ui_state: &mut UiState,
config: &Config,
key: KeyCode,
modifiers: KeyModifiers,
) -> bool {
if let Some(action) = config.get_action_for_key_in_mode(&config.keybindings.common, key, modifiers) {
if action == "toggle_sidebar" {
ui_state.show_sidebar = !ui_state.show_sidebar;
return true;
}
}
false
}
}

View File

@@ -1,139 +0,0 @@
// src/ui/handlers/render.rs
use crate::components::{
render_background,
render_command_line,
render_status_line,
handlers::sidebar::{self, calculate_sidebar_layout},
form::form::render_form,
intro::{intro},
admin::{admin_panel::AdminPanelState},
auth::login::render_login,
};
use crate::config::colors::themes::Theme;
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::Frame;
use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::state::state::AppState;
pub fn render_ui(
f: &mut Frame,
form_state: &mut FormState,
auth_state: &mut AuthState,
theme: &Theme,
is_edit_mode: bool,
total_count: u64,
current_position: u64,
current_dir: &str,
command_input: &str,
command_mode: bool,
command_message: &str,
app_state: &AppState,
// intro_state parameter removed
) {
render_background(f, f.area(), theme);
let root = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1),
Constraint::Length(1),
Constraint::Length(1),
])
.split(f.area());
let main_content_area = root[0];
if app_state.ui.show_intro {
// Use app_state's intro_state directly
app_state.ui.intro_state.render(f, main_content_area, theme);
}else if app_state.ui.show_login {
render_login(f, main_content_area, theme, auth_state);
} else if app_state.ui.show_admin {
// Create temporary AdminPanelState for rendering
let mut admin_state = AdminPanelState::new(
app_state.profile_tree.profiles
.iter()
.map(|p| p.name.clone())
.collect()
);
// Set the selected item - FIXED
if !admin_state.profiles.is_empty() {
let selected_index = std::cmp::min(
app_state.general.selected_item,
admin_state.profiles.len() - 1
);
admin_state.list_state.select(Some(selected_index));
}
admin_state.render(
f,
main_content_area,
theme,
&app_state.profile_tree,
&app_state.selected_profile,
);
} else if app_state.ui.show_form {
let (sidebar_area, form_area) = calculate_sidebar_layout(
app_state.ui.show_sidebar,
main_content_area
);
if let Some(sidebar_rect) = sidebar_area {
sidebar::render_sidebar(
f,
sidebar_rect,
theme,
&app_state.profile_tree,
&app_state.selected_profile
);
}
// This change makes the form stay stationary when toggling sidebar
let available_width = form_area.width;
let form_constraint = if available_width >= 80 {
// Use main_content_area for centering when enough space
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(0),
Constraint::Length(80),
Constraint::Min(0),
])
.split(main_content_area)[1]
} else {
// Use form_area (post sidebar) when limited space
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(0),
Constraint::Length(80.min(available_width)),
Constraint::Min(0),
])
.split(form_area)[1]
};
// Convert fields to &[&str] and values to &[&String]
let fields: Vec<&str> = form_state.fields.iter().map(|s| s.as_str()).collect();
let values: Vec<&String> = form_state.values.iter().collect();
render_form(
f,
form_constraint,
form_state,
&fields,
&form_state.current_field,
&values,
theme,
is_edit_mode,
total_count,
current_position,
);
} else{
}
render_status_line(f, root[1], current_dir, theme, is_edit_mode);
render_command_line(f, root[2], command_input, command_mode, theme, command_message);
}

View File

@@ -1,164 +0,0 @@
// src/ui/handlers/ui.rs
use crate::tui::terminal::TerminalCore;
use crate::tui::terminal::GrpcClient;
use crate::tui::controls::CommandHandler;
use crate::tui::terminal::EventReader;
use crate::config::colors::themes::Theme;
use crate::config::binds::config::Config;
use crate::ui::handlers::render::render_ui;
use crate::state::pages::form::FormState;
use crate::state::pages::auth::AuthState;
use crate::modes::handlers::event::EventHandler;
use crate::state::state::AppState;
use crate::components::admin::{admin_panel::AdminPanelState};
use crate::components::intro::{intro::IntroState};
pub async fn run_ui() -> Result<(), Box<dyn std::error::Error>> {
let config = Config::load()?;
let mut terminal = TerminalCore::new()?;
let mut grpc_client = GrpcClient::new().await?;
let mut command_handler = CommandHandler::new();
let theme = Theme::from_str(&config.colors.theme);
let mut intro_state = IntroState::new();
let mut auth_state = AuthState::default();
// Initialize app_state first
let mut app_state = AppState::new()?;
// Fetch profile tree and table structure
let profile_tree = grpc_client.get_profile_tree().await?;
app_state.profile_tree = profile_tree;
// Fetch table structure at startup (one-time)
let table_structure = grpc_client.get_table_structure().await?;
// Extract the column names from the response
let column_names: Vec<String> = table_structure
.columns
.iter()
.map(|col| col.name.clone())
.collect();
// Initialize FormState with dynamic fields
let mut form_state = FormState::new(column_names);
// The rest of your UI initialization remains the same
let mut event_handler = EventHandler::new();
let event_reader = EventReader::new();
// Fetch the total count of Adresar entries
let total_count = grpc_client.get_adresar_count().await?;
app_state.update_total_count(total_count);
app_state.update_current_position(total_count.saturating_add(1)); // Start in new entry mode
form_state.reset_to_empty();
loop {
let total_count = grpc_client.get_adresar_count().await?;
app_state.update_total_count(total_count);
terminal.draw(|f| {
render_ui(
f,
&mut form_state,
&mut auth_state,
&theme,
event_handler.is_edit_mode,
app_state.total_count,
app_state.current_position,
&app_state.current_dir,
&event_handler.command_input,
event_handler.command_mode,
&event_handler.command_message,
&app_state,
);
})?;
let total_count = app_state.total_count;
let mut current_position = app_state.current_position;
let event = event_reader.read_event()?;
let (should_exit, message) = event_handler.handle_event(
event,
&config,
&mut terminal,
&mut grpc_client,
&mut command_handler,
&mut form_state,
&mut app_state,
total_count,
&mut current_position,
).await?;
app_state.current_position = current_position;
// Handle position changes and update form state
if !event_handler.is_edit_mode {
let current_input = form_state.get_current_input();
let max_cursor_pos = if !current_input.is_empty() {
current_input.len() - 1 // Limit to last character in readonly mode
} else {
0
};
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
// Ensure position never exceeds total_count + 1
if app_state.current_position > total_count + 1 {
app_state.current_position = total_count + 1;
}
if app_state.current_position > total_count {
// New entry - reset form
form_state.reset_to_empty();
form_state.current_field = 0;
} else if app_state.current_position >= 1 && app_state.current_position <= total_count {
// Existing entry - load data
match grpc_client.get_adresar_by_position(app_state.current_position).await {
Ok(response) => {
// Set the ID properly
form_state.id = response.id;
// Update form values dynamically
form_state.values = vec![
response.firma,
response.kz,
response.drc,
response.ulica,
response.psc,
response.mesto,
response.stat,
response.banka,
response.ucet,
response.skladm,
response.ico,
response.kontakt,
response.telefon,
response.skladu,
response.fax,
];
let current_input = form_state.get_current_input();
let max_cursor_pos = if !event_handler.is_edit_mode && !current_input.is_empty() {
current_input.len() - 1 // In readonly mode, limit to last character
} else {
current_input.len()
};
form_state.current_cursor_pos = event_handler.ideal_cursor_column.min(max_cursor_pos);
form_state.has_unsaved_changes = false;
event_handler.command_message = format!("Loaded entry {}", app_state.current_position);
}
Err(e) => {
event_handler.command_message = format!("Error loading entry: {}", e);
}
}
} else {
// Invalid position - reset to first entry
app_state.current_position = 1;
}
}
event_handler.command_message = message;
if should_exit {
return Ok(());
}
}
}

View File

@@ -1,5 +0,0 @@
// src/client/ui/mod.rs
pub mod models;
pub mod handlers;
pub use handlers::*;

View File

@@ -5,9 +5,16 @@ edition.workspace = true
license.workspace = true
[dependencies]
tonic = "0.12.3"
prost-types = { workspace = true }
tonic = "0.13.0"
prost = "0.13.5"
serde = { version = "1.0.218", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
# Search
tantivy = { workspace = true }
serde_json.workspace = true
[build-dependencies]
tonic-build = "0.12.3"
tonic-build = { version = "0.13.0", features = ["prost-build"] }
prost-build = "0.14.1"

View File

@@ -1,19 +1,75 @@
// common/build.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.build_server(true)
.out_dir("src/proto")
.file_descriptor_set_path("src/proto/descriptor.bin")
.compile_protos( // Changed from .compile()
.out_dir("src/proto")
// Derive serde for the messages
.type_attribute(
".komp_ac.table_validation.FieldValidation",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.CharacterLimits",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.DisplayMask",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.TableValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpdateFieldValidationRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_validation.UpdateFieldValidationResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
// Enum -> readable strings in JSON ("BYTES", "DISPLAY_WIDTH")
.type_attribute(
".komp_ac.table_validation.CountMode",
"#[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = \"SCREAMING_SNAKE_CASE\")]",
)
.type_attribute(
".komp_ac.table_definition.ColumnDefinition",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_definition.TableLink",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_definition.PostTableDefinitionRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_definition.TableDefinitionResponse",
"#[derive(serde::Serialize, serde::Deserialize)]"
)
.type_attribute(
".komp_ac.table_script.PostTableScriptRequest",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
".komp_ac.table_script.TableScriptResponse",
"#[derive(serde::Serialize, serde::Deserialize)]",
)
.compile_protos(
&[
"proto/common.proto",
"proto/adresar.proto",
"proto/auth.proto",
"proto/uctovnictvo.proto",
"proto/table_structure.proto",
"proto/search.proto",
"proto/search2.proto",
"proto/table_definition.proto",
"proto/tables_data.proto",
"proto/table_script.proto",
"proto/table_structure.proto",
"proto/table_validation.proto",
"proto/tables_data.proto",
"proto/uctovnictvo.proto",
],
&["proto"],
)?;

View File

@@ -1,6 +1,6 @@
// proto/adresar.proto
syntax = "proto3";
package multieko2.adresar;
package komp_ac.adresar;
import "common.proto";
// import "table_structure.proto";

View File

@@ -1,6 +1,6 @@
// proto/auth.proto
syntax = "proto3";
package multieko2.auth;
package komp_ac.auth;
import "common.proto";
@@ -14,6 +14,7 @@ message RegisterRequest {
string email = 2;
string password = 3;
string password_confirmation = 4;
string role = 5;
}
message AuthResponse {
@@ -34,4 +35,5 @@ message LoginResponse {
int32 expires_in = 3; // Expiration in seconds (86400 for 24 hours)
string user_id = 4; // User's UUID in string format
string role = 5; // User's role
string username = 6;
}

View File

@@ -1,6 +1,6 @@
// proto/common.proto
syntax = "proto3";
package multieko2.common;
package komp_ac.common;
message Empty {}
message CountResponse { int64 count = 1; }

20
common/proto/search.proto Normal file
View File

@@ -0,0 +1,20 @@
// In common/proto/search.proto
syntax = "proto3";
package komp_ac.search;
service Searcher {
rpc SearchTable(SearchRequest) returns (SearchResponse);
}
message SearchRequest {
string table_name = 1;
string query = 2;
}
message SearchResponse {
message Hit {
int64 id = 1; // PostgreSQL row ID
float score = 2;
string content_json = 3;
}
repeated Hit hits = 1;
}

View File

@@ -0,0 +1,46 @@
// In common/proto/search2.proto
syntax = "proto3";
package komp_ac.search2;
service Search2 {
rpc SearchTable(Search2Request) returns (Search2Response);
}
message Search2Request {
string profile_name = 1;
string table_name = 2;
repeated ColumnFilter column_filters = 3;
optional string text_query = 4; // Optional fallback text search
optional int32 limit = 5;
optional string order_by = 6;
optional bool order_desc = 7;
}
message ColumnFilter {
string column_name = 1;
FilterType filter_type = 2;
string value = 3;
optional string value2 = 4; // For range queries
}
enum FilterType {
EQUALS = 0;
CONTAINS = 1;
STARTS_WITH = 2;
ENDS_WITH = 3;
RANGE = 4;
GREATER_THAN = 5;
LESS_THAN = 6;
IS_NULL = 7;
IS_NOT_NULL = 8;
}
message Search2Response {
message Hit {
int64 id = 1;
string content_json = 2; // No score - this is SQL-based
optional string match_info = 3; // Info about which columns matched
}
repeated Hit hits = 1;
int32 total_count = 2; // Total matching records (for pagination)
}

View File

@@ -1,12 +1,12 @@
// common/proto/table_definition.proto
syntax = "proto3";
package multieko2.table_definition;
package komp_ac.table_definition;
import "common.proto";
service TableDefinition {
rpc PostTableDefinition (PostTableDefinitionRequest) returns (TableDefinitionResponse);
rpc GetProfileTree (multieko2.common.Empty) returns (ProfileTreeResponse);
rpc GetProfileTree (komp_ac.common.Empty) returns (ProfileTreeResponse);
rpc DeleteTable (DeleteTableRequest) returns (DeleteTableResponse);
}
@@ -21,7 +21,6 @@ message PostTableDefinitionRequest {
repeated ColumnDefinition columns = 3;
repeated string indexes = 4;
string profile_name = 5;
optional string linked_table_name = 6;
}
message ColumnDefinition {
@@ -36,8 +35,9 @@ message TableDefinitionResponse {
message ProfileTreeResponse {
message Table {
string name = 1;
repeated string depends_on = 2;
int64 id = 1;
string name = 2;
repeated string depends_on = 3;
}
message Profile {

View File

@@ -1,5 +1,5 @@
syntax = "proto3";
package multieko2.table_script;
package komp_ac.table_script;
service TableScript {
rpc PostTableScript(PostTableScriptRequest) returns (TableScriptResponse);

View File

@@ -1,21 +1,25 @@
// proto/table_structure.proto
syntax = "proto3";
package multieko2.table_structure;
package komp_ac.table_structure;
import "common.proto";
message GetTableStructureRequest {
string profile_name = 1; // e.g., "default"
string table_name = 2; // e.g., "2025_adresar6"
}
message TableStructureResponse {
repeated TableColumn columns = 1;
}
message TableColumn {
string name = 1;
string data_type = 2;
string data_type = 2; // e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ"
bool is_nullable = 3;
bool is_primary_key = 4;
}
service TableStructureService {
rpc GetAdresarTableStructure (common.Empty) returns (TableStructureResponse);
rpc GetUctovnictvoTableStructure (common.Empty) returns (TableStructureResponse);
rpc GetTableStructure (GetTableStructureRequest) returns (TableStructureResponse);
}

View File

@@ -0,0 +1,79 @@
// common/proto/table_validation.proto
syntax = "proto3";
package komp_ac.table_validation;
// Request validation rules for a table
message GetTableValidationRequest {
string profileName = 1;
string tableName = 2;
}
// Response with field-level validations; if a field is omitted,
// no validation is applied (default unspecified).
message TableValidationResponse {
repeated FieldValidation fields = 1;
}
// Field-level validation (extensible for future kinds)
message FieldValidation {
// MUST match your frontend FormState.dataKey for the column
string dataKey = 1;
// Current: only CharacterLimits. More rules can be added later.
CharacterLimits limits = 10;
// Future expansion:
// PatternRules pattern = 11;
DisplayMask mask = 3;
// ExternalValidation external = 13;
// CustomFormatter formatter = 14;
bool required = 4;
}
// Character length counting mode
enum CountMode {
COUNT_MODE_UNSPECIFIED = 0; // default: same as CHARS
CHARS = 1;
BYTES = 2;
DISPLAY_WIDTH = 3;
}
// Character limit validation (Validation 1)
message CharacterLimits {
// When zero, the field is considered "not set". If both min/max are zero,
// the server should avoid sending this FieldValidation (no validation).
uint32 min = 1;
uint32 max = 2;
// Optional warning threshold; when unset, no warning threshold is applied.
optional uint32 warnAt = 3;
CountMode countMode = 4; // defaults to CHARS if unspecified
}
// Mask for pretty display
message DisplayMask {
string pattern = 1; // e.g., "(###) ###-####" or "####-##-##"
string input_char = 2; // e.g., "#"
optional string template_char = 3; // e.g., "_"
}
// Service to fetch validations for a table
service TableValidationService {
rpc GetTableValidation(GetTableValidationRequest)
returns (TableValidationResponse);
rpc UpdateFieldValidation(UpdateFieldValidationRequest)
returns (UpdateFieldValidationResponse);
}
message UpdateFieldValidationRequest {
string profileName = 1;
string tableName = 2;
string dataKey = 3;
FieldValidation validation = 4;
}
message UpdateFieldValidationResponse {
bool success = 1;
string message = 2;
}

View File

@@ -1,22 +1,23 @@
// common/proto/tables_data.proto
syntax = "proto3";
package multieko2.tables_data;
package komp_ac.tables_data;
import "common.proto";
import "google/protobuf/struct.proto";
service TablesData {
rpc PostTableData (PostTableDataRequest) returns (PostTableDataResponse);
rpc PutTableData (PutTableDataRequest) returns (PutTableDataResponse);
rpc DeleteTableData (DeleteTableDataRequest) returns (DeleteTableDataResponse);
rpc GetTableData(GetTableDataRequest) returns (GetTableDataResponse);
rpc GetTableDataCount(GetTableDataCountRequest) returns (multieko2.common.CountResponse);
rpc GetTableDataCount(GetTableDataCountRequest) returns (komp_ac.common.CountResponse);
rpc GetTableDataByPosition(GetTableDataByPositionRequest) returns (GetTableDataResponse);
}
message PostTableDataRequest {
string profile_name = 1;
string table_name = 2;
map<string, string> data = 3;
map<string, google.protobuf.Value> data = 3;
}
message PostTableDataResponse {
@@ -29,7 +30,7 @@ message PutTableDataRequest {
string profile_name = 1;
string table_name = 2;
int64 id = 3;
map<string, string> data = 4;
map<string, google.protobuf.Value> data = 4;
}
message PutTableDataResponse {

View File

@@ -1,6 +1,6 @@
// proto/uctovnictvo.proto
syntax = "proto3";
package multieko2.uctovnictvo;
package komp_ac.uctovnictvo;
import "common.proto";

View File

@@ -1,29 +1,41 @@
// common/src/lib.rs
pub mod search;
pub mod proto {
pub mod multieko2 {
pub mod komp_ac {
pub mod adresar {
include!("proto/multieko2.adresar.rs");
include!("proto/komp_ac.adresar.rs");
}
pub mod auth {
include!("proto/multieko2.auth.rs");
include!("proto/komp_ac.auth.rs");
}
pub mod common {
include!("proto/multieko2.common.rs");
include!("proto/komp_ac.common.rs");
}
pub mod table_structure {
include!("proto/multieko2.table_structure.rs");
include!("proto/komp_ac.table_structure.rs");
}
pub mod uctovnictvo {
include!("proto/multieko2.uctovnictvo.rs");
include!("proto/komp_ac.uctovnictvo.rs");
}
pub mod table_definition {
include!("proto/multieko2.table_definition.rs");
include!("proto/komp_ac.table_definition.rs");
}
pub mod tables_data {
include!("proto/multieko2.tables_data.rs");
include!("proto/komp_ac.tables_data.rs");
}
pub mod table_script {
include!("proto/multieko2.table_script.rs");
include!("proto/komp_ac.table_script.rs");
}
pub mod search {
include!("proto/komp_ac.search.rs");
}
pub mod search2 {
include!("proto/komp_ac.search2.rs");
}
pub mod table_validation {
include!("proto/komp_ac.table_validation.rs");
}
pub const FILE_DESCRIPTOR_SET: &[u8] =
include_bytes!("proto/descriptor.bin");

Binary file not shown.

View File

@@ -145,7 +145,7 @@ pub mod adresar_client {
}
impl<T> AdresarClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -166,13 +166,13 @@ pub mod adresar_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
AdresarClient::new(InterceptedService::new(inner, interceptor))
@@ -225,11 +225,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/PostAdresar",
"/komp_ac.adresar.Adresar/PostAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "PostAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PostAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar(
@@ -249,11 +249,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/GetAdresar",
"/komp_ac.adresar.Adresar/GetAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn put_adresar(
@@ -273,11 +273,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/PutAdresar",
"/komp_ac.adresar.Adresar/PutAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "PutAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "PutAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn delete_adresar(
@@ -297,11 +297,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/DeleteAdresar",
"/komp_ac.adresar.Adresar/DeleteAdresar",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "DeleteAdresar"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "DeleteAdresar"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar_count(
@@ -321,11 +321,11 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/GetAdresarCount",
"/komp_ac.adresar.Adresar/GetAdresarCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresarCount"));
.insert(GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarCount"));
self.inner.unary(req, path, codec).await
}
pub async fn get_adresar_by_position(
@@ -345,12 +345,12 @@ pub mod adresar_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.adresar.Adresar/GetAdresarByPosition",
"/komp_ac.adresar.Adresar/GetAdresarByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.adresar.Adresar", "GetAdresarByPosition"),
GrpcMethod::new("komp_ac.adresar.Adresar", "GetAdresarByPosition"),
);
self.inner.unary(req, path, codec).await
}
@@ -465,7 +465,7 @@ pub mod adresar_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -476,7 +476,7 @@ pub mod adresar_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.adresar.Adresar/PostAdresar" => {
"/komp_ac.adresar.Adresar/PostAdresar" => {
#[allow(non_camel_case_types)]
struct PostAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -521,7 +521,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/GetAdresar" => {
"/komp_ac.adresar.Adresar/GetAdresar" => {
#[allow(non_camel_case_types)]
struct GetAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -566,7 +566,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/PutAdresar" => {
"/komp_ac.adresar.Adresar/PutAdresar" => {
#[allow(non_camel_case_types)]
struct PutAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -611,7 +611,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/DeleteAdresar" => {
"/komp_ac.adresar.Adresar/DeleteAdresar" => {
#[allow(non_camel_case_types)]
struct DeleteAdresarSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -656,7 +656,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/GetAdresarCount" => {
"/komp_ac.adresar.Adresar/GetAdresarCount" => {
#[allow(non_camel_case_types)]
struct GetAdresarCountSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -701,7 +701,7 @@ pub mod adresar_server {
};
Box::pin(fut)
}
"/multieko2.adresar.Adresar/GetAdresarByPosition" => {
"/komp_ac.adresar.Adresar/GetAdresarByPosition" => {
#[allow(non_camel_case_types)]
struct GetAdresarByPositionSvc<T: Adresar>(pub Arc<T>);
impl<
@@ -751,7 +751,9 @@ pub mod adresar_server {
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
@@ -782,7 +784,7 @@ pub mod adresar_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.adresar.Adresar";
pub const SERVICE_NAME: &str = "komp_ac.adresar.Adresar";
impl<T> tonic::server::NamedService for AdresarServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

View File

@@ -9,6 +9,8 @@ pub struct RegisterRequest {
pub password: ::prost::alloc::string::String,
#[prost(string, tag = "4")]
pub password_confirmation: ::prost::alloc::string::String,
#[prost(string, tag = "5")]
pub role: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AuthResponse {
@@ -50,6 +52,8 @@ pub struct LoginResponse {
/// User's role
#[prost(string, tag = "5")]
pub role: ::prost::alloc::string::String,
#[prost(string, tag = "6")]
pub username: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod auth_service_client {
@@ -79,7 +83,7 @@ pub mod auth_service_client {
}
impl<T> AuthServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -100,13 +104,13 @@ pub mod auth_service_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
AuthServiceClient::new(InterceptedService::new(inner, interceptor))
@@ -156,11 +160,11 @@ pub mod auth_service_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.auth.AuthService/Register",
"/komp_ac.auth.AuthService/Register",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Register"));
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Register"));
self.inner.unary(req, path, codec).await
}
pub async fn login(
@@ -177,11 +181,11 @@ pub mod auth_service_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.auth.AuthService/Login",
"/komp_ac.auth.AuthService/Login",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("multieko2.auth.AuthService", "Login"));
.insert(GrpcMethod::new("komp_ac.auth.AuthService", "Login"));
self.inner.unary(req, path, codec).await
}
}
@@ -273,7 +277,7 @@ pub mod auth_service_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -284,7 +288,7 @@ pub mod auth_service_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.auth.AuthService/Register" => {
"/komp_ac.auth.AuthService/Register" => {
#[allow(non_camel_case_types)]
struct RegisterSvc<T: AuthService>(pub Arc<T>);
impl<
@@ -329,7 +333,7 @@ pub mod auth_service_server {
};
Box::pin(fut)
}
"/multieko2.auth.AuthService/Login" => {
"/komp_ac.auth.AuthService/Login" => {
#[allow(non_camel_case_types)]
struct LoginSvc<T: AuthService>(pub Arc<T>);
impl<T: AuthService> tonic::server::UnaryService<super::LoginRequest>
@@ -374,7 +378,9 @@ pub mod auth_service_server {
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
@@ -405,7 +411,7 @@ pub mod auth_service_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.auth.AuthService";
pub const SERVICE_NAME: &str = "komp_ac.auth.AuthService";
impl<T> tonic::server::NamedService for AuthServiceServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

View File

@@ -0,0 +1,317 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SearchRequest {
#[prost(string, tag = "1")]
pub table_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub query: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct SearchResponse {
#[prost(message, repeated, tag = "1")]
pub hits: ::prost::alloc::vec::Vec<search_response::Hit>,
}
/// Nested message and enum types in `SearchResponse`.
pub mod search_response {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Hit {
/// PostgreSQL row ID
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(float, tag = "2")]
pub score: f32,
#[prost(string, tag = "3")]
pub content_json: ::prost::alloc::string::String,
}
}
/// Generated client implementations.
pub mod searcher_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct SearcherClient<T> {
inner: tonic::client::Grpc<T>,
}
impl SearcherClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> SearcherClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> SearcherClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
SearcherClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn search_table(
&mut self,
request: impl tonic::IntoRequest<super::SearchRequest>,
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.search.Searcher/SearchTable",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.search.Searcher", "SearchTable"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod searcher_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with SearcherServer.
#[async_trait]
pub trait Searcher: std::marker::Send + std::marker::Sync + 'static {
async fn search_table(
&self,
request: tonic::Request<super::SearchRequest>,
) -> std::result::Result<tonic::Response<super::SearchResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct SearcherServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> SearcherServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for SearcherServer<T>
where
T: Searcher,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.search.Searcher/SearchTable" => {
#[allow(non_camel_case_types)]
struct SearchTableSvc<T: Searcher>(pub Arc<T>);
impl<T: Searcher> tonic::server::UnaryService<super::SearchRequest>
for SearchTableSvc<T> {
type Response = super::SearchResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::SearchRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Searcher>::search_table(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = SearchTableSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for SearcherServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.search.Searcher";
impl<T> tonic::server::NamedService for SearcherServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -0,0 +1,394 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Search2Request {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "3")]
pub column_filters: ::prost::alloc::vec::Vec<ColumnFilter>,
/// Optional fallback text search
#[prost(string, optional, tag = "4")]
pub text_query: ::core::option::Option<::prost::alloc::string::String>,
#[prost(int32, optional, tag = "5")]
pub limit: ::core::option::Option<i32>,
#[prost(string, optional, tag = "6")]
pub order_by: ::core::option::Option<::prost::alloc::string::String>,
#[prost(bool, optional, tag = "7")]
pub order_desc: ::core::option::Option<bool>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ColumnFilter {
#[prost(string, tag = "1")]
pub column_name: ::prost::alloc::string::String,
#[prost(enumeration = "FilterType", tag = "2")]
pub filter_type: i32,
#[prost(string, tag = "3")]
pub value: ::prost::alloc::string::String,
/// For range queries
#[prost(string, optional, tag = "4")]
pub value2: ::core::option::Option<::prost::alloc::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Search2Response {
#[prost(message, repeated, tag = "1")]
pub hits: ::prost::alloc::vec::Vec<search2_response::Hit>,
/// Total matching records (for pagination)
#[prost(int32, tag = "2")]
pub total_count: i32,
}
/// Nested message and enum types in `Search2Response`.
pub mod search2_response {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Hit {
#[prost(int64, tag = "1")]
pub id: i64,
/// No score - this is SQL-based
#[prost(string, tag = "2")]
pub content_json: ::prost::alloc::string::String,
/// Info about which columns matched
#[prost(string, optional, tag = "3")]
pub match_info: ::core::option::Option<::prost::alloc::string::String>,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum FilterType {
Equals = 0,
Contains = 1,
StartsWith = 2,
EndsWith = 3,
Range = 4,
GreaterThan = 5,
LessThan = 6,
IsNull = 7,
IsNotNull = 8,
}
impl FilterType {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Equals => "EQUALS",
Self::Contains => "CONTAINS",
Self::StartsWith => "STARTS_WITH",
Self::EndsWith => "ENDS_WITH",
Self::Range => "RANGE",
Self::GreaterThan => "GREATER_THAN",
Self::LessThan => "LESS_THAN",
Self::IsNull => "IS_NULL",
Self::IsNotNull => "IS_NOT_NULL",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"EQUALS" => Some(Self::Equals),
"CONTAINS" => Some(Self::Contains),
"STARTS_WITH" => Some(Self::StartsWith),
"ENDS_WITH" => Some(Self::EndsWith),
"RANGE" => Some(Self::Range),
"GREATER_THAN" => Some(Self::GreaterThan),
"LESS_THAN" => Some(Self::LessThan),
"IS_NULL" => Some(Self::IsNull),
"IS_NOT_NULL" => Some(Self::IsNotNull),
_ => None,
}
}
}
/// Generated client implementations.
pub mod search2_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct Search2Client<T> {
inner: tonic::client::Grpc<T>,
}
impl Search2Client<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> Search2Client<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> Search2Client<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
Search2Client::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn search_table(
&mut self,
request: impl tonic::IntoRequest<super::Search2Request>,
) -> std::result::Result<
tonic::Response<super::Search2Response>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.search2.Search2/SearchTable",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("komp_ac.search2.Search2", "SearchTable"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod search2_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with Search2Server.
#[async_trait]
pub trait Search2: std::marker::Send + std::marker::Sync + 'static {
async fn search_table(
&self,
request: tonic::Request<super::Search2Request>,
) -> std::result::Result<tonic::Response<super::Search2Response>, tonic::Status>;
}
#[derive(Debug)]
pub struct Search2Server<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> Search2Server<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for Search2Server<T>
where
T: Search2,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.search2.Search2/SearchTable" => {
#[allow(non_camel_case_types)]
struct SearchTableSvc<T: Search2>(pub Arc<T>);
impl<T: Search2> tonic::server::UnaryService<super::Search2Request>
for SearchTableSvc<T> {
type Response = super::Search2Response;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::Search2Request>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Search2>::search_table(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = SearchTableSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for Search2Server<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.search2.Search2";
impl<T> tonic::server::NamedService for Search2Server<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -1,4 +1,5 @@
// This file is @generated by prost-build.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableLink {
#[prost(string, tag = "1")]
@@ -6,6 +7,7 @@ pub struct TableLink {
#[prost(bool, tag = "2")]
pub required: bool,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableDefinitionRequest {
#[prost(string, tag = "1")]
@@ -18,9 +20,8 @@ pub struct PostTableDefinitionRequest {
pub indexes: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(string, tag = "5")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, optional, tag = "6")]
pub linked_table_name: ::core::option::Option<::prost::alloc::string::String>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ColumnDefinition {
#[prost(string, tag = "1")]
@@ -28,6 +29,7 @@ pub struct ColumnDefinition {
#[prost(string, tag = "2")]
pub field_type: ::prost::alloc::string::String,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableDefinitionResponse {
#[prost(bool, tag = "1")]
@@ -44,9 +46,11 @@ pub struct ProfileTreeResponse {
pub mod profile_tree_response {
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Table {
#[prost(string, tag = "1")]
#[prost(int64, tag = "1")]
pub id: i64,
#[prost(string, tag = "2")]
pub name: ::prost::alloc::string::String,
#[prost(string, repeated, tag = "2")]
#[prost(string, repeated, tag = "3")]
pub depends_on: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -99,7 +103,7 @@ pub mod table_definition_client {
}
impl<T> TableDefinitionClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -120,13 +124,13 @@ pub mod table_definition_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TableDefinitionClient::new(InterceptedService::new(inner, interceptor))
@@ -179,13 +183,13 @@ pub mod table_definition_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_definition.TableDefinition/PostTableDefinition",
"/komp_ac.table_definition.TableDefinition/PostTableDefinition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_definition.TableDefinition",
"komp_ac.table_definition.TableDefinition",
"PostTableDefinition",
),
);
@@ -208,13 +212,13 @@ pub mod table_definition_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_definition.TableDefinition/GetProfileTree",
"/komp_ac.table_definition.TableDefinition/GetProfileTree",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_definition.TableDefinition",
"komp_ac.table_definition.TableDefinition",
"GetProfileTree",
),
);
@@ -237,13 +241,13 @@ pub mod table_definition_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_definition.TableDefinition/DeleteTable",
"/komp_ac.table_definition.TableDefinition/DeleteTable",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_definition.TableDefinition",
"komp_ac.table_definition.TableDefinition",
"DeleteTable",
),
);
@@ -351,7 +355,7 @@ pub mod table_definition_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -362,7 +366,7 @@ pub mod table_definition_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.table_definition.TableDefinition/PostTableDefinition" => {
"/komp_ac.table_definition.TableDefinition/PostTableDefinition" => {
#[allow(non_camel_case_types)]
struct PostTableDefinitionSvc<T: TableDefinition>(pub Arc<T>);
impl<
@@ -411,7 +415,7 @@ pub mod table_definition_server {
};
Box::pin(fut)
}
"/multieko2.table_definition.TableDefinition/GetProfileTree" => {
"/komp_ac.table_definition.TableDefinition/GetProfileTree" => {
#[allow(non_camel_case_types)]
struct GetProfileTreeSvc<T: TableDefinition>(pub Arc<T>);
impl<
@@ -457,7 +461,7 @@ pub mod table_definition_server {
};
Box::pin(fut)
}
"/multieko2.table_definition.TableDefinition/DeleteTable" => {
"/komp_ac.table_definition.TableDefinition/DeleteTable" => {
#[allow(non_camel_case_types)]
struct DeleteTableSvc<T: TableDefinition>(pub Arc<T>);
impl<
@@ -504,7 +508,9 @@ pub mod table_definition_server {
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
@@ -535,7 +541,7 @@ pub mod table_definition_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.table_definition.TableDefinition";
pub const SERVICE_NAME: &str = "komp_ac.table_definition.TableDefinition";
impl<T> tonic::server::NamedService for TableDefinitionServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

View File

@@ -1,4 +1,5 @@
// This file is @generated by prost-build.
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PostTableScriptRequest {
#[prost(int64, tag = "1")]
@@ -10,6 +11,7 @@ pub struct PostTableScriptRequest {
#[prost(string, tag = "4")]
pub description: ::prost::alloc::string::String,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableScriptResponse {
#[prost(int64, tag = "1")]
@@ -45,7 +47,7 @@ pub mod table_script_client {
}
impl<T> TableScriptClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -66,13 +68,13 @@ pub mod table_script_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TableScriptClient::new(InterceptedService::new(inner, interceptor))
@@ -125,13 +127,13 @@ pub mod table_script_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_script.TableScript/PostTableScript",
"/komp_ac.table_script.TableScript/PostTableScript",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_script.TableScript",
"komp_ac.table_script.TableScript",
"PostTableScript",
),
);
@@ -225,7 +227,7 @@ pub mod table_script_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -236,7 +238,7 @@ pub mod table_script_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.table_script.TableScript/PostTableScript" => {
"/komp_ac.table_script.TableScript/PostTableScript" => {
#[allow(non_camel_case_types)]
struct PostTableScriptSvc<T: TableScript>(pub Arc<T>);
impl<
@@ -283,7 +285,9 @@ pub mod table_script_server {
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
@@ -314,7 +318,7 @@ pub mod table_script_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.table_script.TableScript";
pub const SERVICE_NAME: &str = "komp_ac.table_script.TableScript";
impl<T> tonic::server::NamedService for TableScriptServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

View File

@@ -1,5 +1,14 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableStructureRequest {
/// e.g., "default"
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
/// e.g., "2025_adresar6"
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableStructureResponse {
#[prost(message, repeated, tag = "1")]
pub columns: ::prost::alloc::vec::Vec<TableColumn>,
@@ -8,6 +17,7 @@ pub struct TableStructureResponse {
pub struct TableColumn {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
/// e.g., "TEXT", "BIGINT", "VARCHAR(255)", "TIMESTAMPTZ"
#[prost(string, tag = "2")]
pub data_type: ::prost::alloc::string::String,
#[prost(bool, tag = "3")]
@@ -43,7 +53,7 @@ pub mod table_structure_service_client {
}
impl<T> TableStructureServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -64,13 +74,13 @@ pub mod table_structure_service_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TableStructureServiceClient::new(InterceptedService::new(inner, interceptor))
@@ -106,9 +116,9 @@ pub mod table_structure_service_client {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn get_adresar_table_structure(
pub async fn get_table_structure(
&mut self,
request: impl tonic::IntoRequest<super::super::common::Empty>,
request: impl tonic::IntoRequest<super::GetTableStructureRequest>,
) -> std::result::Result<
tonic::Response<super::TableStructureResponse>,
tonic::Status,
@@ -123,43 +133,14 @@ pub mod table_structure_service_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_structure.TableStructureService/GetAdresarTableStructure",
"/komp_ac.table_structure.TableStructureService/GetTableStructure",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_structure.TableStructureService",
"GetAdresarTableStructure",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn get_uctovnictvo_table_structure(
&mut self,
request: impl tonic::IntoRequest<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::TableStructureResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.table_structure.TableStructureService/GetUctovnictvoTableStructure",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.table_structure.TableStructureService",
"GetUctovnictvoTableStructure",
"komp_ac.table_structure.TableStructureService",
"GetTableStructure",
),
);
self.inner.unary(req, path, codec).await
@@ -179,16 +160,9 @@ pub mod table_structure_service_server {
/// Generated trait containing gRPC methods that should be implemented for use with TableStructureServiceServer.
#[async_trait]
pub trait TableStructureService: std::marker::Send + std::marker::Sync + 'static {
async fn get_adresar_table_structure(
async fn get_table_structure(
&self,
request: tonic::Request<super::super::common::Empty>,
) -> std::result::Result<
tonic::Response<super::TableStructureResponse>,
tonic::Status,
>;
async fn get_uctovnictvo_table_structure(
&self,
request: tonic::Request<super::super::common::Empty>,
request: tonic::Request<super::GetTableStructureRequest>,
) -> std::result::Result<
tonic::Response<super::TableStructureResponse>,
tonic::Status,
@@ -260,7 +234,7 @@ pub mod table_structure_service_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -271,15 +245,13 @@ pub mod table_structure_service_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.table_structure.TableStructureService/GetAdresarTableStructure" => {
"/komp_ac.table_structure.TableStructureService/GetTableStructure" => {
#[allow(non_camel_case_types)]
struct GetAdresarTableStructureSvc<T: TableStructureService>(
pub Arc<T>,
);
struct GetTableStructureSvc<T: TableStructureService>(pub Arc<T>);
impl<
T: TableStructureService,
> tonic::server::UnaryService<super::super::common::Empty>
for GetAdresarTableStructureSvc<T> {
> tonic::server::UnaryService<super::GetTableStructureRequest>
for GetTableStructureSvc<T> {
type Response = super::TableStructureResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
@@ -287,11 +259,11 @@ pub mod table_structure_service_server {
>;
fn call(
&mut self,
request: tonic::Request<super::super::common::Empty>,
request: tonic::Request<super::GetTableStructureRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableStructureService>::get_adresar_table_structure(
<T as TableStructureService>::get_table_structure(
&inner,
request,
)
@@ -306,58 +278,7 @@ pub mod table_structure_service_server {
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetAdresarTableStructureSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/multieko2.table_structure.TableStructureService/GetUctovnictvoTableStructure" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoTableStructureSvc<T: TableStructureService>(
pub Arc<T>,
);
impl<
T: TableStructureService,
> tonic::server::UnaryService<super::super::common::Empty>
for GetUctovnictvoTableStructureSvc<T> {
type Response = super::TableStructureResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::super::common::Empty>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableStructureService>::get_uctovnictvo_table_structure(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetUctovnictvoTableStructureSvc(inner);
let method = GetTableStructureSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
@@ -375,7 +296,9 @@ pub mod table_structure_service_server {
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
@@ -406,7 +329,7 @@ pub mod table_structure_service_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.table_structure.TableStructureService";
pub const SERVICE_NAME: &str = "komp_ac.table_structure.TableStructureService";
impl<T> tonic::server::NamedService for TableStructureServiceServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

View File

@@ -0,0 +1,523 @@
// This file is @generated by prost-build.
/// Request validation rules for a table
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GetTableValidationRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
}
/// Response with field-level validations; if a field is omitted,
/// no validation is applied (default unspecified).
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct TableValidationResponse {
#[prost(message, repeated, tag = "1")]
pub fields: ::prost::alloc::vec::Vec<FieldValidation>,
}
/// Field-level validation (extensible for future kinds)
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FieldValidation {
/// MUST match your frontend FormState.dataKey for the column
#[prost(string, tag = "1")]
pub data_key: ::prost::alloc::string::String,
/// Current: only CharacterLimits. More rules can be added later.
#[prost(message, optional, tag = "10")]
pub limits: ::core::option::Option<CharacterLimits>,
/// Future expansion:
/// PatternRules pattern = 11;
#[prost(message, optional, tag = "3")]
pub mask: ::core::option::Option<DisplayMask>,
/// ExternalValidation external = 13;
/// CustomFormatter formatter = 14;
#[prost(bool, tag = "4")]
pub required: bool,
}
/// Character limit validation (Validation 1)
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct CharacterLimits {
/// When zero, the field is considered "not set". If both min/max are zero,
/// the server should avoid sending this FieldValidation (no validation).
#[prost(uint32, tag = "1")]
pub min: u32,
#[prost(uint32, tag = "2")]
pub max: u32,
/// Optional warning threshold; when unset, no warning threshold is applied.
#[prost(uint32, optional, tag = "3")]
pub warn_at: ::core::option::Option<u32>,
/// defaults to CHARS if unspecified
#[prost(enumeration = "CountMode", tag = "4")]
pub count_mode: i32,
}
/// Mask for pretty display
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct DisplayMask {
/// e.g., "(###) ###-####" or "####-##-##"
#[prost(string, tag = "1")]
pub pattern: ::prost::alloc::string::String,
/// e.g., "#"
#[prost(string, tag = "2")]
pub input_char: ::prost::alloc::string::String,
/// e.g., "_"
#[prost(string, optional, tag = "3")]
pub template_char: ::core::option::Option<::prost::alloc::string::String>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct UpdateFieldValidationRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(string, tag = "3")]
pub data_key: ::prost::alloc::string::String,
#[prost(message, optional, tag = "4")]
pub validation: ::core::option::Option<FieldValidation>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct UpdateFieldValidationResponse {
#[prost(bool, tag = "1")]
pub success: bool,
#[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String,
}
/// Character length counting mode
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum CountMode {
/// default: same as CHARS
Unspecified = 0,
Chars = 1,
Bytes = 2,
DisplayWidth = 3,
}
impl CountMode {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Self::Unspecified => "COUNT_MODE_UNSPECIFIED",
Self::Chars => "CHARS",
Self::Bytes => "BYTES",
Self::DisplayWidth => "DISPLAY_WIDTH",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"COUNT_MODE_UNSPECIFIED" => Some(Self::Unspecified),
"CHARS" => Some(Self::Chars),
"BYTES" => Some(Self::Bytes),
"DISPLAY_WIDTH" => Some(Self::DisplayWidth),
_ => None,
}
}
}
/// Generated client implementations.
pub mod table_validation_service_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
/// Service to fetch validations for a table
#[derive(Debug, Clone)]
pub struct TableValidationServiceClient<T> {
inner: tonic::client::Grpc<T>,
}
impl TableValidationServiceClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> TableValidationServiceClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> TableValidationServiceClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TableValidationServiceClient::new(
InterceptedService::new(inner, interceptor),
)
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn get_table_validation(
&mut self,
request: impl tonic::IntoRequest<super::GetTableValidationRequest>,
) -> std::result::Result<
tonic::Response<super::TableValidationResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_validation.TableValidationService/GetTableValidation",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_validation.TableValidationService",
"GetTableValidation",
),
);
self.inner.unary(req, path, codec).await
}
pub async fn update_field_validation(
&mut self,
request: impl tonic::IntoRequest<super::UpdateFieldValidationRequest>,
) -> std::result::Result<
tonic::Response<super::UpdateFieldValidationResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/komp_ac.table_validation.TableValidationService/UpdateFieldValidation",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"komp_ac.table_validation.TableValidationService",
"UpdateFieldValidation",
),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod table_validation_service_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with TableValidationServiceServer.
#[async_trait]
pub trait TableValidationService: std::marker::Send + std::marker::Sync + 'static {
async fn get_table_validation(
&self,
request: tonic::Request<super::GetTableValidationRequest>,
) -> std::result::Result<
tonic::Response<super::TableValidationResponse>,
tonic::Status,
>;
async fn update_field_validation(
&self,
request: tonic::Request<super::UpdateFieldValidationRequest>,
) -> std::result::Result<
tonic::Response<super::UpdateFieldValidationResponse>,
tonic::Status,
>;
}
/// Service to fetch validations for a table
#[derive(Debug)]
pub struct TableValidationServiceServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> TableValidationServiceServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>>
for TableValidationServiceServer<T>
where
T: TableValidationService,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/komp_ac.table_validation.TableValidationService/GetTableValidation" => {
#[allow(non_camel_case_types)]
struct GetTableValidationSvc<T: TableValidationService>(pub Arc<T>);
impl<
T: TableValidationService,
> tonic::server::UnaryService<super::GetTableValidationRequest>
for GetTableValidationSvc<T> {
type Response = super::TableValidationResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::GetTableValidationRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableValidationService>::get_table_validation(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetTableValidationSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/komp_ac.table_validation.TableValidationService/UpdateFieldValidation" => {
#[allow(non_camel_case_types)]
struct UpdateFieldValidationSvc<T: TableValidationService>(
pub Arc<T>,
);
impl<
T: TableValidationService,
> tonic::server::UnaryService<super::UpdateFieldValidationRequest>
for UpdateFieldValidationSvc<T> {
type Response = super::UpdateFieldValidationResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::UpdateFieldValidationRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TableValidationService>::update_field_validation(
&inner,
request,
)
.await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = UpdateFieldValidationSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for TableValidationServiceServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "komp_ac.table_validation.TableValidationService";
impl<T> tonic::server::NamedService for TableValidationServiceServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}

View File

@@ -5,10 +5,10 @@ pub struct PostTableDataRequest {
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(map = "string, string", tag = "3")]
#[prost(map = "string, message", tag = "3")]
pub data: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost::alloc::string::String,
::prost_types::Value,
>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -28,10 +28,10 @@ pub struct PutTableDataRequest {
pub table_name: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub id: i64,
#[prost(map = "string, string", tag = "4")]
#[prost(map = "string, message", tag = "4")]
pub data: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost::alloc::string::String,
::prost_types::Value,
>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -118,7 +118,7 @@ pub mod tables_data_client {
}
impl<T> TablesDataClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -139,13 +139,13 @@ pub mod tables_data_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
TablesDataClient::new(InterceptedService::new(inner, interceptor))
@@ -198,12 +198,12 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/PostTableData",
"/komp_ac.tables_data.TablesData/PostTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.tables_data.TablesData", "PostTableData"),
GrpcMethod::new("komp_ac.tables_data.TablesData", "PostTableData"),
);
self.inner.unary(req, path, codec).await
}
@@ -224,12 +224,12 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/PutTableData",
"/komp_ac.tables_data.TablesData/PutTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.tables_data.TablesData", "PutTableData"),
GrpcMethod::new("komp_ac.tables_data.TablesData", "PutTableData"),
);
self.inner.unary(req, path, codec).await
}
@@ -250,15 +250,12 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/DeleteTableData",
"/komp_ac.tables_data.TablesData/DeleteTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.tables_data.TablesData",
"DeleteTableData",
),
GrpcMethod::new("komp_ac.tables_data.TablesData", "DeleteTableData"),
);
self.inner.unary(req, path, codec).await
}
@@ -279,12 +276,12 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/GetTableData",
"/komp_ac.tables_data.TablesData/GetTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.tables_data.TablesData", "GetTableData"),
GrpcMethod::new("komp_ac.tables_data.TablesData", "GetTableData"),
);
self.inner.unary(req, path, codec).await
}
@@ -305,13 +302,13 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/GetTableDataCount",
"/komp_ac.tables_data.TablesData/GetTableDataCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.tables_data.TablesData",
"komp_ac.tables_data.TablesData",
"GetTableDataCount",
),
);
@@ -334,13 +331,13 @@ pub mod tables_data_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/GetTableDataByPosition",
"/komp_ac.tables_data.TablesData/GetTableDataByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.tables_data.TablesData",
"komp_ac.tables_data.TablesData",
"GetTableDataByPosition",
),
);
@@ -469,7 +466,7 @@ pub mod tables_data_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -480,7 +477,7 @@ pub mod tables_data_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.tables_data.TablesData/PostTableData" => {
"/komp_ac.tables_data.TablesData/PostTableData" => {
#[allow(non_camel_case_types)]
struct PostTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -525,7 +522,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/PutTableData" => {
"/komp_ac.tables_data.TablesData/PutTableData" => {
#[allow(non_camel_case_types)]
struct PutTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -570,7 +567,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/DeleteTableData" => {
"/komp_ac.tables_data.TablesData/DeleteTableData" => {
#[allow(non_camel_case_types)]
struct DeleteTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -615,7 +612,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/GetTableData" => {
"/komp_ac.tables_data.TablesData/GetTableData" => {
#[allow(non_camel_case_types)]
struct GetTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -660,7 +657,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/GetTableDataCount" => {
"/komp_ac.tables_data.TablesData/GetTableDataCount" => {
#[allow(non_camel_case_types)]
struct GetTableDataCountSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -706,7 +703,7 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/GetTableDataByPosition" => {
"/komp_ac.tables_data.TablesData/GetTableDataByPosition" => {
#[allow(non_camel_case_types)]
struct GetTableDataByPositionSvc<T: TablesData>(pub Arc<T>);
impl<
@@ -757,7 +754,9 @@ pub mod tables_data_server {
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
@@ -788,7 +787,7 @@ pub mod tables_data_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.tables_data.TablesData";
pub const SERVICE_NAME: &str = "komp_ac.tables_data.TablesData";
impl<T> tonic::server::NamedService for TablesDataServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

View File

@@ -112,7 +112,7 @@ pub mod uctovnictvo_client {
}
impl<T> UctovnictvoClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -133,13 +133,13 @@ pub mod uctovnictvo_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
UctovnictvoClient::new(InterceptedService::new(inner, interceptor))
@@ -192,15 +192,12 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/PostUctovnictvo",
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"PostUctovnictvo",
),
GrpcMethod::new("komp_ac.uctovnictvo.Uctovnictvo", "PostUctovnictvo"),
);
self.inner.unary(req, path, codec).await
}
@@ -221,15 +218,12 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvo",
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"GetUctovnictvo",
),
GrpcMethod::new("komp_ac.uctovnictvo.Uctovnictvo", "GetUctovnictvo"),
);
self.inner.unary(req, path, codec).await
}
@@ -250,13 +244,13 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvoCount",
),
);
@@ -279,13 +273,13 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"komp_ac.uctovnictvo.Uctovnictvo",
"GetUctovnictvoByPosition",
),
);
@@ -308,15 +302,12 @@ pub mod uctovnictvo_client {
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.uctovnictvo.Uctovnictvo/PutUctovnictvo",
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new(
"multieko2.uctovnictvo.Uctovnictvo",
"PutUctovnictvo",
),
GrpcMethod::new("komp_ac.uctovnictvo.Uctovnictvo", "PutUctovnictvo"),
);
self.inner.unary(req, path, codec).await
}
@@ -436,7 +427,7 @@ pub mod uctovnictvo_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -447,7 +438,7 @@ pub mod uctovnictvo_server {
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/multieko2.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
"/komp_ac.uctovnictvo.Uctovnictvo/PostUctovnictvo" => {
#[allow(non_camel_case_types)]
struct PostUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -492,7 +483,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvo" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -537,7 +528,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoCount" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoCountSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -583,7 +574,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
"/komp_ac.uctovnictvo.Uctovnictvo/GetUctovnictvoByPosition" => {
#[allow(non_camel_case_types)]
struct GetUctovnictvoByPositionSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -634,7 +625,7 @@ pub mod uctovnictvo_server {
};
Box::pin(fut)
}
"/multieko2.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
"/komp_ac.uctovnictvo.Uctovnictvo/PutUctovnictvo" => {
#[allow(non_camel_case_types)]
struct PutUctovnictvoSvc<T: Uctovnictvo>(pub Arc<T>);
impl<
@@ -681,7 +672,9 @@ pub mod uctovnictvo_server {
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
@@ -712,7 +705,7 @@ pub mod uctovnictvo_server {
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "multieko2.uctovnictvo.Uctovnictvo";
pub const SERVICE_NAME: &str = "komp_ac.uctovnictvo.Uctovnictvo";
impl<T> tonic::server::NamedService for UctovnictvoServer<T> {
const NAME: &'static str = SERVICE_NAME;
}

78
common/src/search.rs Normal file
View File

@@ -0,0 +1,78 @@
// common/src/search.rs
use tantivy::schema::*;
use tantivy::tokenizer::*;
use tantivy::Index;
/// Creates a hybrid Slovak search schema with optimized prefix fields.
pub fn create_search_schema() -> Schema {
let mut schema_builder = Schema::builder();
schema_builder.add_u64_field("pg_id", INDEXED | STORED);
// FIELD 1: For prefixes (1-4 chars).
let short_prefix_indexing = TextFieldIndexing::default()
.set_tokenizer("slovak_prefix_edge")
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
let short_prefix_options = TextOptions::default()
.set_indexing_options(short_prefix_indexing)
.set_stored();
schema_builder.add_text_field("prefix_edge", short_prefix_options);
// FIELD 2: For the full word.
let full_word_indexing = TextFieldIndexing::default()
.set_tokenizer("slovak_prefix_full")
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
let full_word_options = TextOptions::default()
.set_indexing_options(full_word_indexing)
.set_stored();
schema_builder.add_text_field("prefix_full", full_word_options);
// NGRAM FIELD: For substring matching.
let ngram_field_indexing = TextFieldIndexing::default()
.set_tokenizer("slovak_ngram")
.set_index_option(IndexRecordOption::WithFreqsAndPositions);
let ngram_options = TextOptions::default()
.set_indexing_options(ngram_field_indexing)
.set_stored();
schema_builder.add_text_field("text_ngram", ngram_options);
schema_builder.build()
}
/// Registers all necessary Slovak tokenizers with the index.
///
/// This must be called by ANY process that opens the index
/// to ensure the tokenizers are loaded into memory.
pub fn register_slovak_tokenizers(index: &Index) -> tantivy::Result<()> {
let tokenizer_manager = index.tokenizers();
// TOKENIZER for `prefix_edge`: Edge N-gram (1-4 chars)
let edge_tokenizer =
TextAnalyzer::builder(NgramTokenizer::new(1, 4, true)?)
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build();
tokenizer_manager.register("slovak_prefix_edge", edge_tokenizer);
// TOKENIZER for `prefix_full`: Simple word tokenizer
let full_tokenizer =
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build();
tokenizer_manager.register("slovak_prefix_full", full_tokenizer);
// NGRAM TOKENIZER: For substring matching.
let ngram_tokenizer =
TextAnalyzer::builder(NgramTokenizer::new(3, 3, false)?)
.filter(RemoveLongFilter::limit(40))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build();
tokenizer_manager.register("slovak_ngram", ngram_tokenizer);
Ok(())
}

View File

@@ -71,6 +71,8 @@ feature-depth = 1
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
{ id = "RUSTSEC-2024-0014", reason = "generational-arena is archived but no safe upgrade path exists; accepted for now" },
{ id = "RUSTSEC-2024-0436", reason = "paste is archived; no immediate alternative in dependency tree; accepted for now" },
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
@@ -100,6 +102,7 @@ allow = [
"HPND",
"ISC",
"LGPL-3.0",
"AGPL-3.0",
"MIT",
"MPL-2.0",
"NCSA",

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1753549186,
"narHash": "sha256-Znl7rzuxKg/Mdm6AhimcKynM7V3YeNDIcLjBuoBcmNs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "17f6bd177404d6d43017595c5264756764444ab8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

52
flake.nix Normal file
View File

@@ -0,0 +1,52 @@
{
description = "Komp AC - Kompress Accounting";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Rust toolchain
rustc
cargo
rustfmt
clippy
cargo-watch
# C build tools (for your linker issue)
gcc
binutils
pkg-config
# OpenSSL for crypto dependencies
openssl
openssl.dev
# PostgreSQL for sqlx
postgresql
sqlx-cli
# Protocol Buffers compiler for gRPC
protobuf
];
shellHook = ''
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig:$PKG_CONFIG_PATH"
export OPENSSL_DIR="${pkgs.openssl.dev}"
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include"
echo "🦀 Rust development environment loaded"
echo "OpenSSL and PostgreSQL available"
'';
};
}
);
}

19
search/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "search"
version.workspace = true
edition.workspace = true
license = "AGPL-3.0-or-later"
[dependencies]
anyhow = { workspace = true }
prost = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tonic = { workspace = true }
tracing = { workspace = true }
tantivy = { workspace = true }
common = { path = "../common" }
tonic-reflection = "0.13.1"
sqlx = { version = "0.8.6", features = ["postgres"] }

302
search/src/lib.rs Normal file
View File

@@ -0,0 +1,302 @@
// src/lib.rs
use std::collections::HashMap;
use std::path::Path;
use tantivy::collector::TopDocs;
use tantivy::query::{
BooleanQuery, BoostQuery, FuzzyTermQuery, Occur, Query, QueryParser,
TermQuery,
};
use tantivy::schema::{IndexRecordOption, Value};
use tantivy::{Index, TantivyDocument, Term};
use tonic::{Request, Response, Status};
use common::proto::komp_ac::search::{
search_response::Hit, SearchRequest, SearchResponse,
};
pub use common::proto::komp_ac::search::searcher_server::SearcherServer;
use common::proto::komp_ac::search::searcher_server::Searcher;
use common::search::register_slovak_tokenizers;
use sqlx::{PgPool, Row};
use tracing::info;
// We need to hold the database pool in our service struct.
pub struct SearcherService {
pub pool: PgPool,
}
// normalize_slovak_text function remains unchanged...
fn normalize_slovak_text(text: &str) -> String {
// ... function content is unchanged ...
text.chars()
.map(|c| match c {
'á' | 'à' | 'â' | 'ä' | 'ă' | 'ā' => 'a',
'Á' | 'À' | 'Â' | 'Ä' | 'Ă' | 'Ā' => 'A',
'é' | 'è' | 'ê' | 'ë' | 'ě' | 'ē' => 'e',
'É' | 'È' | 'Ê' | 'Ë' | 'Ě' | 'Ē' => 'E',
'í' | 'ì' | 'î' | 'ï' | 'ī' => 'i',
'Í' | 'Ì' | 'Î' | 'Ï' | 'Ī' => 'I',
'ó' | 'ò' | 'ô' | 'ö' | 'ō' | 'ő' => 'o',
'Ó' | 'Ò' | 'Ô' | 'Ö' | 'Ō' | 'Ő' => 'O',
'ú' | 'ù' | 'û' | 'ü' | 'ū' | 'ű' => 'u',
'Ú' | 'Ù' | 'Û' | 'Ü' | 'Ū' | 'Ű' => 'U',
'ý' | 'ỳ' | 'ŷ' | 'ÿ' => 'y',
'Ý' | 'Ỳ' | 'Ŷ' | 'Ÿ' => 'Y',
'č' => 'c',
'Č' => 'C',
'ď' => 'd',
'Ď' => 'D',
'ľ' => 'l',
'Ľ' => 'L',
'ň' => 'n',
'Ň' => 'N',
'ř' => 'r',
'Ř' => 'R',
'š' => 's',
'Š' => 'S',
'ť' => 't',
'Ť' => 'T',
'ž' => 'z',
'Ž' => 'Z',
_ => c,
})
.collect()
}
#[tonic::async_trait]
impl Searcher for SearcherService {
async fn search_table(
&self,
request: Request<SearchRequest>,
) -> Result<Response<SearchResponse>, Status> {
let req = request.into_inner();
let table_name = req.table_name;
let query_str = req.query;
// --- MODIFIED LOGIC ---
// If the query is empty, fetch the 5 most recent records.
if query_str.trim().is_empty() {
info!(
"Empty query for table '{}'. Fetching default results.",
table_name
);
let qualified_table = format!("gen.\"{}\"", table_name);
let sql = format!(
"SELECT id, to_jsonb(t) AS data FROM {} t ORDER BY id DESC LIMIT 5",
qualified_table
);
let rows = sqlx::query(&sql)
.fetch_all(&self.pool)
.await
.map_err(|e| {
Status::internal(format!(
"DB query for default results failed: {}",
e
))
})?;
let hits: Vec<Hit> = rows
.into_iter()
.map(|row| {
let id: i64 = row.try_get("id").unwrap_or_default();
let json_data: serde_json::Value =
row.try_get("data").unwrap_or_default();
Hit {
id,
// Score is 0.0 as this is not a relevance-ranked search
score: 0.0,
content_json: json_data.to_string(),
}
})
.collect();
info!("--- SERVER: Successfully processed empty query. Returning {} default hits. ---", hits.len());
return Ok(Response::new(SearchResponse { hits }));
}
// --- END OF MODIFIED LOGIC ---
let index_path = Path::new("./tantivy_indexes").join(&table_name);
if !index_path.exists() {
return Err(Status::not_found(format!(
"No search index found for table '{}'",
table_name
)));
}
let index = Index::open_in_dir(&index_path)
.map_err(|e| Status::internal(format!("Failed to open index: {}", e)))?;
register_slovak_tokenizers(&index).map_err(|e| {
Status::internal(format!("Failed to register Slovak tokenizers: {}", e))
})?;
let reader = index.reader().map_err(|e| {
Status::internal(format!("Failed to create index reader: {}", e))
})?;
let searcher = reader.searcher();
let schema = index.schema();
let pg_id_field = schema.get_field("pg_id").map_err(|_| {
Status::internal("Schema is missing the 'pg_id' field.")
})?;
// --- Query Building Logic (no changes here) ---
let prefix_edge_field = schema.get_field("prefix_edge").unwrap();
let prefix_full_field = schema.get_field("prefix_full").unwrap();
let text_ngram_field = schema.get_field("text_ngram").unwrap();
let normalized_query = normalize_slovak_text(&query_str);
let words: Vec<&str> = normalized_query.split_whitespace().collect();
if words.is_empty() {
return Ok(Response::new(SearchResponse { hits: vec![] }));
}
let mut query_layers: Vec<(Occur, Box<dyn Query>)> = Vec::new();
// ... all your query building layers remain exactly the same ...
// ===============================
// LAYER 1: PREFIX MATCHING (HIGHEST PRIORITY, Boost: 4.0)
// ===============================
{
let mut must_clauses: Vec<(Occur, Box<dyn Query>)> = Vec::new();
for word in &words {
let edge_term =
Term::from_field_text(prefix_edge_field, word);
let full_term =
Term::from_field_text(prefix_full_field, word);
let per_word_query = BooleanQuery::new(vec![
(
Occur::Should,
Box::new(TermQuery::new(
edge_term,
IndexRecordOption::Basic,
)),
),
(
Occur::Should,
Box::new(TermQuery::new(
full_term,
IndexRecordOption::Basic,
)),
),
]);
must_clauses.push((Occur::Must, Box::new(per_word_query) as Box<dyn Query>));
}
if !must_clauses.is_empty() {
let prefix_query = BooleanQuery::new(must_clauses);
let boosted_query =
BoostQuery::new(Box::new(prefix_query), 4.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
}
// ===============================
// LAYER 2: FUZZY MATCHING (HIGH PRIORITY, Boost: 3.0)
// ===============================
{
let last_word = words.last().unwrap();
let fuzzy_term =
Term::from_field_text(prefix_full_field, last_word);
let fuzzy_query = FuzzyTermQuery::new(fuzzy_term, 2, true);
let boosted_query = BoostQuery::new(Box::new(fuzzy_query), 3.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
// ===============================
// LAYER 3: PHRASE MATCHING WITH SLOP (MEDIUM PRIORITY, Boost: 2.0)
// ===============================
if words.len() > 1 {
let slop_parser =
QueryParser::for_index(&index, vec![prefix_full_field]);
let slop_query_str = format!("\"{}\"~3", normalized_query);
if let Ok(slop_query) = slop_parser.parse_query(&slop_query_str) {
let boosted_query = BoostQuery::new(slop_query, 2.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
}
// ===============================
// LAYER 4: NGRAM SUBSTRING MATCHING (LOWEST PRIORITY, Boost: 1.0)
// ===============================
{
let ngram_parser =
QueryParser::for_index(&index, vec![text_ngram_field]);
if let Ok(ngram_query) =
ngram_parser.parse_query(&normalized_query)
{
let boosted_query = BoostQuery::new(ngram_query, 1.0);
query_layers.push((Occur::Should, Box::new(boosted_query)));
}
}
let master_query = BooleanQuery::new(query_layers);
// --- End of Query Building Logic ---
let top_docs = searcher
.search(&master_query, &TopDocs::with_limit(100))
.map_err(|e| Status::internal(format!("Search failed: {}", e)))?;
if top_docs.is_empty() {
return Ok(Response::new(SearchResponse { hits: vec![] }));
}
// --- NEW LOGIC: Fetch from DB and combine results ---
// Step 1: Extract (score, pg_id) from Tantivy results.
let mut scored_ids: Vec<(f32, u64)> = Vec::new();
for (score, doc_address) in top_docs {
let doc: TantivyDocument = searcher.doc(doc_address).map_err(|e| {
Status::internal(format!("Failed to retrieve document: {}", e))
})?;
if let Some(pg_id_value) = doc.get_first(pg_id_field) {
if let Some(pg_id) = pg_id_value.as_u64() {
scored_ids.push((score, pg_id));
}
}
}
// Step 2: Fetch all corresponding rows from Postgres in a single query.
let pg_ids: Vec<i64> =
scored_ids.iter().map(|(_, id)| *id as i64).collect();
let qualified_table = format!("gen.\"{}\"", table_name);
let query_str = format!(
"SELECT id, to_jsonb(t) AS data FROM {} t WHERE id = ANY($1)",
qualified_table
);
let rows = sqlx::query(&query_str)
.bind(&pg_ids)
.fetch_all(&self.pool)
.await
.map_err(|e| {
Status::internal(format!("Database query failed: {}", e))
})?;
// Step 3: Map the database results by ID for quick lookup.
let mut content_map: HashMap<i64, String> = HashMap::new();
for row in rows {
let id: i64 = row.try_get("id").unwrap_or(0);
let json_data: serde_json::Value =
row.try_get("data").unwrap_or(serde_json::Value::Null);
content_map.insert(id, json_data.to_string());
}
// Step 4: Build the final response, combining Tantivy scores with PG content.
let hits: Vec<Hit> = scored_ids
.into_iter()
.filter_map(|(score, pg_id)| {
content_map
.get(&(pg_id as i64))
.map(|content_json| Hit {
id: pg_id as i64,
score,
content_json: content_json.clone(),
})
})
.collect();
info!("--- SERVER: Successfully processed search. Returning {} hits. ---", hits.len());
let response = SearchResponse { hits };
Ok(Response::new(response))
}
}

Some files were not shown because too many files have changed in this diff Show More