├── src ├── notes.v ├── assets │ ├── fmt.png │ ├── merge.png │ ├── run.png │ ├── vicon.png │ ├── word.png │ ├── word0.png │ ├── explore.png │ ├── search.png │ ├── v-logo.png │ ├── file-icon.png │ ├── help-icon.png │ ├── theme │ │ ├── btn.png │ │ ├── menu.png │ │ └── btn-light.png │ ├── Anomaly-Mono.ttf │ ├── JetBrainsMono.ttf │ ├── icons8-edit-24.png │ ├── icons8-save-24.png │ ├── FiraCode-Regular.ttf │ ├── JetBrainsMono-Regular.ttf │ ├── ezgif.com-gif-maker(3).png │ ├── ezgif.com-gif-maker(5).png │ ├── icons8-change-theme-24.png │ └── credit.txt ├── image_view.v ├── tree-list.v ├── code_interaction.v ├── vide-theme.v ├── events.v ├── verminal_commands.v ├── new_proj.v ├── verminal.v ├── code_suggestions.v ├── tabs.v ├── menus.v ├── config.v ├── main.v └── vcreate.v ├── v.mod ├── .gitattributes ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md └── .github └── workflows ├── blank.yml └── rele.yml /src/notes.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | struct Note { 4 | text string 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/fmt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/fmt.png -------------------------------------------------------------------------------- /src/assets/merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/merge.png -------------------------------------------------------------------------------- /src/assets/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/run.png -------------------------------------------------------------------------------- /src/assets/vicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/vicon.png -------------------------------------------------------------------------------- /src/assets/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/word.png -------------------------------------------------------------------------------- /src/assets/word0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/word0.png -------------------------------------------------------------------------------- /src/assets/explore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/explore.png -------------------------------------------------------------------------------- /src/assets/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/search.png -------------------------------------------------------------------------------- /src/assets/v-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/v-logo.png -------------------------------------------------------------------------------- /src/assets/file-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/file-icon.png -------------------------------------------------------------------------------- /src/assets/help-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/help-icon.png -------------------------------------------------------------------------------- /src/assets/theme/btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/theme/btn.png -------------------------------------------------------------------------------- /src/assets/theme/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/theme/menu.png -------------------------------------------------------------------------------- /src/assets/Anomaly-Mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/Anomaly-Mono.ttf -------------------------------------------------------------------------------- /src/assets/JetBrainsMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/JetBrainsMono.ttf -------------------------------------------------------------------------------- /src/assets/icons8-edit-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/icons8-edit-24.png -------------------------------------------------------------------------------- /src/assets/icons8-save-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/icons8-save-24.png -------------------------------------------------------------------------------- /src/assets/theme/btn-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/theme/btn-light.png -------------------------------------------------------------------------------- /src/assets/FiraCode-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/FiraCode-Regular.ttf -------------------------------------------------------------------------------- /src/assets/JetBrainsMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/JetBrainsMono-Regular.ttf -------------------------------------------------------------------------------- /src/assets/ezgif.com-gif-maker(3).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/ezgif.com-gif-maker(3).png -------------------------------------------------------------------------------- /src/assets/ezgif.com-gif-maker(5).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/ezgif.com-gif-maker(5).png -------------------------------------------------------------------------------- /src/assets/icons8-change-theme-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pisaiah/Vide/HEAD/src/assets/icons8-change-theme-24.png -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | Module { 2 | name: 'vide2' 3 | description: 'Testing' 4 | version: '1.0' 5 | license: 'MIT' 6 | dependencies: [] 7 | } 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.bat eol=crlf 3 | 4 | **/*.v linguist-language=V 5 | **/*.vv linguist-language=V 6 | **/*.vsh linguist-language=V 7 | **/v.mod linguist-language=V 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [*.v] 8 | indent_style = tab 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /src/assets/credit.txt: -------------------------------------------------------------------------------- 1 | Save icon by Icons8 2 | Edit icon by Icons8 3 | 4 | Anomaly Mono: 5 | https://github.com/benbusby/anomaly-mono/ -------------------------------------------------------------------------------- /src/image_view.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | 5 | fn image_view(path string) &ui.ScrollView { 6 | mut p := ui.Panel.new() 7 | 8 | mut im := ui.Image.new(file: path) 9 | p.add_child(im) 10 | 11 | mut sv := ui.ScrollView.new( 12 | view: p 13 | ) 14 | sv.set_border_painted(false) 15 | 16 | return sv 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | main 3 | vide2 4 | *.exe 5 | *.exe~ 6 | *.so 7 | *.dylib 8 | *.dll 9 | *.def 10 | 11 | # Ignore binary output folders 12 | bin/ 13 | 14 | # Ignore common editor/system specific metadata 15 | .DS_Store 16 | .idea/ 17 | .vscode/ 18 | *.iml 19 | 20 | # ENV 21 | .env 22 | 23 | # vweb and database 24 | *.db 25 | *.js 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Isaiah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tree-list.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | 6 | // Tree v2: 7 | 8 | // Make an Tree list from files from dir 9 | fn make_tree2(fold string) &ui.TreeNode { 10 | files := os.ls(fold) or { [] } 11 | 12 | mut nodes := []&ui.TreeNode{} 13 | 14 | for fi in files { 15 | if fi.starts_with('.git') || fi.contains('.exe') || fi.contains('.dll') { 16 | continue 17 | } 18 | mut sub := &ui.TreeNode{ 19 | text: fold + '/' + fi 20 | } 21 | 22 | if !fi.starts_with('.') { 23 | join := os.join_path(fold, fi) 24 | subfiles := os.ls(join) or { [] } 25 | for f in subfiles { 26 | node := make_tree2(os.join_path(join, f)) 27 | sub.nodes << node 28 | } 29 | } 30 | nodes << sub 31 | } 32 | 33 | mut node := &ui.TreeNode{ 34 | text: fold 35 | nodes: nodes 36 | } 37 | 38 | return node 39 | } 40 | 41 | fn tree2_click(mut ctx ui.GraphicsContext, tree &ui.Tree2, node &ui.TreeNode) { 42 | txt := node.text 43 | dump(txt) 44 | path := os.real_path(txt) 45 | dump(path) 46 | if !os.is_dir(path) { 47 | new_tab(ctx.win, txt) 48 | } 49 | } 50 | 51 | // Refresh Tree list 52 | fn refresh_tree(mut window ui.Window, fold string, mut tree ui.Tree2) { 53 | // TODO 54 | dump('REFRESH') 55 | tree.children.clear() 56 | 57 | dump(fold) 58 | files := os.ls(fold) or { [] } 59 | tree.click_event_fn = tree2_click 60 | 61 | for fi in files { 62 | mut node := make_tree2(os.join_path(fold, fi)) 63 | tree.add_child(node) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/code_interaction.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | 5 | struct MyPopup { 6 | ui.Popup 7 | mut: 8 | texts []string 9 | p &ui.Panel 10 | sv &ui.ScrollView 11 | } 12 | 13 | fn code_popup() &MyPopup { 14 | mut p := ui.Panel.new( 15 | layout: ui.BoxLayout.new( 16 | ori: 1 17 | vgap: 1 18 | hgap: 1 19 | ) 20 | ) 21 | p.set_bounds(0, 0, 300, 150) 22 | 23 | mut sv := ui.ScrollView.new( 24 | view: p 25 | bounds: ui.Bounds{0, 0, 300, 150} 26 | ) 27 | 28 | mut pop := &MyPopup{ 29 | p: p 30 | sv: sv 31 | } 32 | 33 | pop.add_child(sv) 34 | pop.set_bounds(0, 0, 300, 150) 35 | 36 | return pop 37 | } 38 | 39 | fn (mut this MyPopup) set_texts(mut tb ui.Textbox, lines []string, aft int) { 40 | this.p.children.clear() 41 | for line in lines { 42 | mut o := ui.Label.new( 43 | text: line 44 | ) 45 | o.subscribe_event('draw', fn (mut e ui.MouseEvent) { 46 | e.target.height = e.ctx.line_height 47 | e.target.width = e.target.parent.width - 2 48 | 49 | is_in := ui.is_in(e.target, e.ctx.win.mouse_x, e.ctx.win.mouse_y) 50 | 51 | if is_in { 52 | e.ctx.gg.draw_rect_filled(e.target.x, e.target.y, e.target.width, e.target.height, 53 | e.ctx.theme.button_bg_hover) 54 | } 55 | }) 56 | o.subscribe_event('mouse_up', fn [mut tb, mut this, aft] (mut e ui.MouseEvent) { 57 | bef := tb.lines[tb.caret_y][0..(tb.caret_x - aft)] 58 | ln := bef + e.target.text 59 | 60 | tb.lines[tb.caret_y] = ln 61 | tb.caret_x = ln.len 62 | 63 | this.hide(e.ctx) 64 | }) 65 | o.set_bounds(0, 0, 100, 30) 66 | this.p.add_child(o) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | A simple IDE designed for the [V Programming Language](https://vlang.io/) made in V. 4 | 5 | ![licence](http://img.shields.io/badge/licence-MIT-blue?style=flat) 6 | [![GitHub all releases](http://img.shields.io/github/downloads/pisaiah/Vide/total?style=flat)](https://github.com/pisaiah/Vide/releases) 7 | ![vlang](http://img.shields.io/badge/V-0.4.3+-%236d8fc5?style=flat) 8 | 9 | ## Features: 10 | - **UI for VPM**: Vide integrates with VPM to manage your project dependencies. 11 | - **Multiple Projects**: Work on multiple projects simultaneously within the same IDE. 12 | - **Autocomplete** (Beta): Enjoy code suggestions as you type (still in beta, but improving!). 13 | - **GUI Builder** (Planned) 14 | - **Online Playground** (Planned) 15 | 16 | ## Screenshots: 17 | 18 |
UI for VPM
19 |
Multiple Projects
20 |
Autocomplete (Beta)
21 | 22 | 23 | ## Resource Requirements 24 | 25 | | IDE | Disk | RAM | Render | 26 | |---------|---------|---------|----------| 27 | | | | | 28 | | Vɪᴅᴇ | < 5MB | ~ 70MB | GG/Sokol | 29 | | Vɪᴅᴇ | < 5MB | ~ 4MB | [Win32](https://github.com/pisaiah/v/tree/win32-native-rendering) | 30 | | VS Code | 300MB | ~ 300MB | Electron | 31 | | | | | | 32 | 33 | ## Contributing 34 | Feel free to contribute to the development of Vɪᴅᴇ by creating an account on GitHub. Your feedback, bug reports, and pull requests are highly appreciated! 35 | 36 | ## See Also 37 | 38 | - [iUI](https://github.com/pisaiah/ui), UI Library for V. 39 | - [vPaint](https://github.com/pisaiah/vpaint), MS-Paint alternative written in V. 40 | - [Vizard](https://github.com/pisaiah/vizard), Utility for creating wizard guis in V. 41 | 42 | - [V](https://github.com/vlang/v) 43 | 44 | -------------------------------------------------------------------------------- /src/vide-theme.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import gx 5 | 6 | // Vide Light Theme 7 | pub fn vide_light_theme() &ui.Theme { 8 | mut theme := ui.theme_default() 9 | theme.name = 'Vide Light' 10 | theme.button_fill_fn = vide_theme_button_fill_fn 11 | theme.bar_fill_fn = vide_theme_bar_fill_fn 12 | theme.setup_fn = vide_theme_setup 13 | theme.menu_bar_fill_fn = vide_theme_menubar_fill_fn 14 | return theme 15 | } 16 | 17 | // Vide Default Dark Theme 18 | pub fn vide_dark_theme() &ui.Theme { 19 | mut theme := ui.theme_dark() 20 | theme.name = 'Vide Default Dark' 21 | // theme.button_fill_fn = vide_theme_button_fill_fn 22 | theme.bar_fill_fn = vide_theme_bar_fill_fn 23 | theme.setup_fn = vide_theme_setup 24 | theme.menu_bar_fill_fn = vide_theme_menubar_fill_fn 25 | 26 | // V colors 27 | theme.accent_fill = gx.rgb(93, 135, 191) 28 | theme.accent_fill_second = gx.rgb(88, 121, 165) 29 | theme.accent_fill_third = gx.rgb(83, 107, 138) 30 | 31 | // theme.button_bg_normal = theme.accent_fill_third 32 | 33 | return theme 34 | } 35 | 36 | pub fn vide_theme_setup(mut win ui.Window) { 37 | mut ctx := win.graphics_context 38 | // mut o_file := $embed_file('assets/theme/btn.png') 39 | // mut o_icons := win.create_gg_image(o_file.data(), o_file.len) 40 | // ctx.icon_cache['vide_theme-btn'] = win.gg.cache_image(o_icons) 41 | 42 | // mut o_file1 := $embed_file('assets/theme/bar.png') 43 | // o_icons = win.create_gg_image(o_file1.data(), o_file1.len) 44 | // ctx.icon_cache['vide_theme-bar'] = ctx.gg.cache_image(o_icons) 45 | 46 | mut o_file2 := $embed_file('assets/theme/menu.png') 47 | mut o_icons := win.create_gg_image(o_file2.data(), o_file2.len) 48 | ctx.icon_cache['vide_theme-menu'] = ctx.gg.cache_image(o_icons) 49 | 50 | // mut o_file3 := $embed_file('assets/theme/barw.png') 51 | // o_icons = win.create_gg_image(o_file3.data(), o_file3.len) 52 | // ctx.icon_cache['vide_theme-bar-w'] = ctx.gg.cache_image(o_icons) 53 | } 54 | 55 | pub fn vide_theme_button_fill_fn(x int, y int, w int, h int, r int, bg gx.Color, ctx &ui.GraphicsContext) { 56 | if bg == ctx.theme.button_bg_normal { 57 | ctx.gg.draw_image_by_id(x - 1, y - 1, w + 2, h + 2, ctx.icon_cache['vide_theme-btn']) 58 | } else { 59 | ctx.gg.draw_rounded_rect_filled(x, y, w, h, r, bg) 60 | } 61 | } 62 | 63 | pub fn vide_theme_bar_fill_fn(x int, y f32, w int, h f32, hor bool, ctx &ui.GraphicsContext) { 64 | if hor { 65 | hh := h / 2 66 | ctx.gg.draw_rect_filled(x, y, w, hh, gx.rgb(88, 128, 181)) 67 | ctx.gg.draw_rect_filled(x, y + hh, w, hh, gx.rgb(68, 100, 140)) 68 | ctx.gg.draw_rect_empty(x, y, w, h, ctx.theme.scroll_bar_color) 69 | } else { 70 | xx := x - 1 71 | ww := (w + 2) / 2 72 | 73 | ctx.gg.draw_rect_filled(xx, y, w + 3, h, gx.rgb(88, 128, 181)) 74 | ctx.gg.draw_rect_filled(xx + ww, y, ww + 1, h, gx.rgb(68, 100, 140)) 75 | } 76 | } 77 | 78 | pub fn vide_theme_menubar_fill_fn(x int, y int, w int, h int, ctx &ui.GraphicsContext) { 79 | ctx.gg.draw_image_by_id(x, y, w, h + 1, ctx.icon_cache['vide_theme-menu']) 80 | } 81 | -------------------------------------------------------------------------------- /src/events.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import gg 5 | 6 | // Change the width of the project tree to correspond with the collapse state. 7 | fn (mut app App) proj_tree_draw(mut e ui.DrawEvent) { 8 | if app.shown_activity != 0 { 9 | e.target.width = -1 10 | return 11 | } 12 | 13 | if app.collapse_tree { 14 | mx := app.win.mouse_x 15 | if mx < e.target.width || mx < e.target.x { 16 | if e.target.width < 250 { 17 | e.target.width += app.activty_speed 18 | } 19 | return 20 | } 21 | 22 | if e.target.width > app.activty_speed { 23 | e.target.width -= app.activty_speed 24 | } 25 | if e.target.width <= app.activty_speed { 26 | e.target.width = 0 27 | } 28 | } else { 29 | if e.target.width < 250 { 30 | e.target.width += app.activty_speed 31 | } 32 | } 33 | 34 | height := gg.window_size().height - 32 35 | 36 | if height > 0 { 37 | e.target.height = height 38 | } 39 | } 40 | 41 | // Change the width of the project tree to correspond with the collapse state. 42 | fn (mut app App) search_pane_draw(mut e ui.DrawEvent) { 43 | if app.shown_activity != 1 { 44 | e.target.width = -1 45 | return 46 | } 47 | 48 | if app.collapse_search { 49 | mx := app.win.mouse_x 50 | if mx < e.target.width || mx < e.target.x { 51 | if e.target.width < 250 { 52 | e.target.width += app.activty_speed 53 | } 54 | return 55 | } 56 | 57 | if e.target.width > app.activty_speed { 58 | e.target.width -= app.activty_speed 59 | } 60 | if e.target.width <= app.activty_speed { 61 | e.target.width = 0 62 | } 63 | } else { 64 | if e.target.width < 250 { 65 | e.target.width += app.activty_speed 66 | } 67 | } 68 | 69 | height := gg.window_size().height - 32 70 | 71 | if height > 0 { 72 | e.target.height = height 73 | } 74 | } 75 | 76 | // Change the collapse state when the button is clicked 77 | fn (mut app App) calb_click(mut e ui.MouseEvent) { 78 | if app.shown_activity != 0 { 79 | app.shown_activity = 0 80 | app.collapse_tree = false 81 | } else { 82 | app.collapse_tree = !app.collapse_tree 83 | } 84 | } 85 | 86 | // Change the collapse state when the button is clicked 87 | fn (mut app App) serb_click(mut e ui.MouseEvent) { 88 | if app.shown_activity != 1 { 89 | app.shown_activity = 1 90 | app.collapse_search = false 91 | } else { 92 | app.collapse_search = !app.collapse_search 93 | } 94 | } 95 | 96 | // Set the width of the verminal's ScrollView to 97 | // the width of the SplitView (aka the parent) 98 | fn terminal_scrollview_fill(mut e ui.DrawEvent) { 99 | e.target.width = e.target.parent.width 100 | } 101 | 102 | // Set the width and height of the SplitView to fill the content area. 103 | fn splitview_fill(mut e ui.DrawEvent) { 104 | size := e.ctx.gg.window_size() 105 | 106 | w := size.width - e.target.rx - 1 107 | h := size.height - 30 108 | 109 | if w < 0 || h < 0 { 110 | return 111 | } 112 | 113 | e.target.width = w 114 | e.target.height = h 115 | } 116 | 117 | // Have the main HBox's size be set to the window size 118 | @[deprecated: 'Not needed with latest ui, as we use Panel now'] 119 | fn content_pane_fill_window(mut e ui.DrawEvent) { 120 | } 121 | 122 | // Have Tabbox take up the full width of the SplitView 123 | fn tabbox_fill_width(mut e ui.DrawEvent) { 124 | size := e.ctx.gg.window_size() 125 | wid := size.width - e.target.x - 1 126 | if wid < 0 { 127 | return 128 | } 129 | e.target.width = wid 130 | 131 | // TODO: add open/close tab event. 132 | 133 | mut app := e.ctx.win.get[&App]('app') 134 | mut tb := e.ctx.win.get[&ui.Tabbox]('main-tabs') 135 | 136 | if app.confg.open_paths.len != tb.kids.len { 137 | // dump('need update') 138 | 139 | for name in tb.kids.keys() { 140 | if name !in app.confg.open_paths { 141 | app.confg.open_paths << name 142 | } 143 | } 144 | 145 | for i, name in app.confg.open_paths { 146 | if name !in tb.kids.keys() { 147 | app.confg.open_paths.delete(i) 148 | // app.confg.open_paths.delete(name) 149 | } 150 | } 151 | app.confg.save() 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/verminal_commands.v: -------------------------------------------------------------------------------- 1 | // Verminal 2 | module main 3 | 4 | import iui as ui 5 | import os 6 | 7 | fn cmd_cd(mut win ui.Window, mut tbox ui.Textbox, args []string) { 8 | mut path := win.extra_map['path'] 9 | if args.len == 1 { 10 | tbox.text = tbox.text + path 11 | return 12 | } 13 | 14 | if args[1] == '..' { 15 | path = path.substr(0, path.replace('\\', '/').last_index('/') or { 0 }) // ' 16 | } else { 17 | if os.is_abs_path(args[1]) { 18 | path = os.real_path(args[1]) 19 | } else { 20 | path = os.real_path(path + '/' + args[1]) 21 | } 22 | } 23 | if os.exists(path) { 24 | win.extra_map['path'] = path 25 | } else { 26 | tbox.text = tbox.text + 'Cannot find the path specified: ' + path 27 | } 28 | } 29 | 30 | fn cmd_dir(mut tbox ui.Textbox, path string, args []string) { 31 | mut ls := os.ls(os.real_path(path)) or { [''] } 32 | tbox.lines << ' Directory of "' + path + '".' 33 | for file in ls { 34 | tbox.lines << '\t' + file 35 | } 36 | } 37 | 38 | fn cmd_v(mut tbox ui.Textbox, args []string) { 39 | mut pro := os.execute('cmd /min /c ' + args.join(' ')) 40 | tbox.text = tbox.text + pro.output.trim_space() 41 | } 42 | 43 | fn verminal_cmd_exec(mut win ui.Window, mut tbox ui.Textbox, args []string) { 44 | // Make sure we are in the correct directory 45 | os.chdir(win.extra_map['path']) or { tbox.lines << err.str() } 46 | 47 | if os.user_os() == 'windows' { 48 | cmd_exec_win(mut win, mut tbox, args) 49 | } else { 50 | cmd_exec_unix(mut win, mut tbox, args) 51 | } 52 | 53 | win.extra_map['update_scroll'] = 'true' 54 | } 55 | 56 | // Linux 57 | fn cmd_exec_unix(mut win ui.Window, mut tbox ui.Textbox, args []string) { 58 | mut cmd := os.Command{ 59 | path: args.join(' ') 60 | } 61 | 62 | cmd.start() or { tbox.lines << err.str() } 63 | for !cmd.eof { 64 | out := cmd.read_line() 65 | if out.len > 0 { 66 | for line in out.split_into_lines() { 67 | tbox.lines << line.trim_space() 68 | } 69 | } 70 | } 71 | add_new_input_line(mut tbox, win) 72 | 73 | cmd.close() or { tbox.lines << err.str() } 74 | } 75 | 76 | // Windows 77 | fn cmd_exec_win(mut win ui.Window, mut tbox ui.Textbox, args []string) { 78 | mut pro := os.new_process('C:\\Windows\\System32\\cmd.exe') 79 | 80 | mut argsa := ['/min', '/c', args.join(' ')] 81 | pro.set_args(argsa) 82 | 83 | pro.set_redirect_stdio() 84 | pro.run() 85 | 86 | for pro.is_alive() { 87 | mut out := pro.stdout_read() 88 | mut oute := pro.stderr_read() 89 | 90 | if oute.len > 0 { 91 | for line in oute.split_into_lines() { 92 | tbox.lines << line.trim_space() 93 | } 94 | } 95 | 96 | if out.len > 0 { 97 | for line in out.split_into_lines() { 98 | tbox.lines << line.trim_space() 99 | } 100 | } 101 | } 102 | add_new_input_line(mut tbox, win) 103 | 104 | pro.close() 105 | } 106 | 107 | // Run command without updating a text box 108 | fn run_exec(args []string) []string { 109 | if os.user_os() == 'windows' { 110 | return run_exec_win(args) 111 | } else { 112 | return run_exec_unix(args) 113 | } 114 | } 115 | 116 | // Linux 117 | fn run_exec_unix(args []string) []string { 118 | mut cmd := os.Command{ 119 | path: args.join(' ') 120 | } 121 | 122 | mut content := []string{} 123 | cmd.start() or { content << err.str() } 124 | for !cmd.eof { 125 | out := cmd.read_line() 126 | if out.len > 0 { 127 | for line in out.split_into_lines() { 128 | content << line.trim_space() 129 | } 130 | } 131 | } 132 | 133 | cmd.close() or { content << err.str() } 134 | return content 135 | } 136 | 137 | // Windows; 138 | fn run_exec_win(args []string) []string { 139 | mut pro := os.new_process('cmd') 140 | 141 | mut argsa := ['/min', '/c', args.join(' ')] 142 | pro.set_args(argsa) 143 | 144 | pro.set_redirect_stdio() 145 | pro.run() 146 | 147 | mut content := []string{} 148 | for pro.is_alive() { 149 | mut out := pro.stdout_read() 150 | if out.len > 0 { 151 | // println(out) 152 | for line in out.split_into_lines() { 153 | content << line.trim_space() 154 | } 155 | } 156 | } 157 | 158 | pro.close() 159 | return content 160 | } 161 | -------------------------------------------------------------------------------- /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | name: Build binary artifacts 2 | 3 | #on: 4 | # push: 5 | # tags: 6 | # - weekly.** 7 | # - 0.** 8 | 9 | on: 10 | push: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | 16 | build-linux: 17 | runs-on: ubuntu-20.04 18 | env: 19 | CC: gcc 20 | ZIPNAME: vide_linux.zip 21 | steps: 22 | - name: Setup V 23 | uses: vlang/setup-v@v1 24 | with: 25 | # Default: ${{ github.token }} 26 | token: ${{ github.token }} 27 | version: 'weekly.2023.02' 28 | version-file: '' 29 | check-latest: true 30 | stable: false 31 | architecture: '' 32 | - uses: actions/checkout@v1 33 | - name: Compile 34 | run: | 35 | sudo apt-get -qq update 36 | sudo apt-get -qq install libgc-dev 37 | sudo apt install build-essential 38 | sudo apt-get --yes --force-yes install libxi-dev libxcursor-dev mesa-common-dev 39 | sudo apt-get --yes --force-yes install libgl1-mesa-glx 40 | v install https://github.com/isaiahpatton/ui 41 | git clone https://github.com/isaiahpatton/vide 42 | v -cc $CC -skip-unused -gc boehm vide 43 | - name: Remove excluded 44 | run: | 45 | rm -rf .git 46 | - name: Create ZIP archive 47 | run: | 48 | zip -r9 --symlinks $ZIPNAME vide/ 49 | - name: Create artifact 50 | uses: actions/upload-artifact@v2 51 | with: 52 | name: linux 53 | path: vide_linux.zip 54 | 55 | build-macos: 56 | runs-on: macos-latest 57 | env: 58 | CC: clang 59 | ZIPNAME: vide_macos.zip 60 | steps: 61 | - name: Setup V 62 | uses: vlang/setup-v@v1 63 | with: 64 | # Default: ${{ github.token }} 65 | token: ${{ github.token }} 66 | version: 'weekly.2023.02' 67 | version-file: '' 68 | check-latest: true 69 | stable: false 70 | architecture: '' 71 | - uses: actions/checkout@v1 72 | - name: Compile 73 | run: | 74 | v install https://github.com/isaiahpatton/ui 75 | git clone https://github.com/isaiahpatton/vide 76 | v -cc $CC -skip-unused -gc boehm vide 77 | - name: Remove excluded 78 | run: | 79 | rm -rf .git 80 | - name: Create ZIP archive 81 | run: | 82 | zip -r9 --symlinks $ZIPNAME vide/ 83 | - name: Create artifact 84 | uses: actions/upload-artifact@v2 85 | with: 86 | name: macos 87 | path: vide_macos.zip 88 | 89 | build-windows: 90 | runs-on: windows-latest 91 | env: 92 | CC: msvc 93 | ZIPNAME: vide_windows.zip 94 | steps: 95 | - name: Setup V 96 | uses: vlang/setup-v@v1 97 | with: 98 | # Default: ${{ github.token }} 99 | token: ${{ github.token }} 100 | version: 'weekly.2023.02' 101 | version-file: '' 102 | check-latest: true 103 | stable: false 104 | architecture: '' 105 | - uses: actions/checkout@v1 106 | - uses: msys2/setup-msys2@v2 107 | - name: Compile 108 | run: | 109 | git clone https://github.com/vlang/v 110 | cd v 111 | .\make.bat 112 | .\v.exe install https://github.com/isaiahpatton/ui 113 | .\v.exe symlink 114 | git clone https://github.com/isaiahpatton/vide 115 | v -cc gcc -skip-unused -gc boehm -cflags -static vide 116 | - name: Remove excluded 117 | shell: msys2 {0} 118 | run: | 119 | rm -rf .git 120 | cd v 121 | cd vide 122 | rm -rf *.v 123 | rm -rf .git 124 | cd .. 125 | cd .. 126 | - name: Create archive 127 | shell: msys2 {0} 128 | run: | 129 | cd v 130 | cd vide 131 | cd .. 132 | powershell Compress-Archive vide $ZIPNAME 133 | mv $ZIPNAME ../ 134 | cd .. 135 | # NB: the powershell Compress-Archive line is from: 136 | # https://superuser.com/a/1336434/194881 137 | # It is needed, because `zip` is not installed by default :-| 138 | - name: Create artifact 139 | uses: actions/upload-artifact@v2 140 | with: 141 | name: windows 142 | path: vide_windows.zip -------------------------------------------------------------------------------- /src/new_proj.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | 5 | fn (mut app App) new_project_click(mut win ui.Window, com ui.MenuItem) { 6 | app.new_project(mut win) 7 | } 8 | 9 | fn (mut app App) new_project(mut win ui.Window) { 10 | mut modal := ui.Modal.new(title: 'New Project') 11 | 12 | modal.top_off = 10 13 | modal.in_height = 370 14 | 15 | mut ip := ui.Panel.new( 16 | layout: ui.BoxLayout.new(ori: 1) 17 | ) 18 | 19 | ip.add_child(create_input(mut win, 'Name', 'my project')) 20 | ip.add_child(create_input(mut win, 'Description', 'Hello world!')) 21 | ip.add_child(create_input(mut win, 'Version', '0.0.0')) 22 | 23 | ip.set_pos(10, 5) 24 | modal.add_child(ip) 25 | 26 | mut lic := make_license_section() 27 | 28 | mut lic_tb := ui.Titlebox.new(text: 'License', children: [lic]) 29 | lic_tb.set_bounds(25, 125, 5, 25) 30 | modal.add_child(lic_tb) 31 | 32 | mut templ := make_templ_section() 33 | mut templ_tb := ui.Titlebox.new(text: 'Template', children: [templ]) 34 | templ_tb.set_bounds(260, 125, 5, 25) 35 | modal.add_child(templ_tb) 36 | 37 | modal.needs_init = false 38 | 39 | mut close := ui.Button.new( 40 | text: 'Create' 41 | bounds: ui.Bounds{114, 334, 160, 28} 42 | ) 43 | 44 | mut can := ui.Button.new( 45 | text: 'Cancel' 46 | bounds: ui.Bounds{10, 334, 100, 28} 47 | ) 48 | can.set_area_filled(false) 49 | can.subscribe_event('mouse_up', fn (mut e ui.MouseEvent) { 50 | e.ctx.win.components = e.ctx.win.components.filter(mut it !is ui.Modal) 51 | }) 52 | modal.add_child(can) 53 | 54 | win.extra_map['np-lic'] = 'MIT' 55 | win.extra_map['np-templ'] = 'hello_world' 56 | 57 | close.subscribe_event('mouse_up', app.new_project_click_close) 58 | 59 | modal.add_child(close) 60 | win.add_child(modal) 61 | } 62 | 63 | fn (mut app App) new_project_click_close(mut e ui.MouseEvent) { 64 | mut win := e.ctx.win 65 | name := win.get[&ui.TextField]('NewProj-Name').text 66 | des := win.get[&ui.TextField]('NewProj-Description').text 67 | ver := win.get[&ui.TextField]('NewProj-Version').text 68 | 69 | lic := win.extra_map['np-lic'] 70 | dir := app.confg.workspace_dir 71 | templ := win.extra_map['np-templ'] 72 | 73 | new_project( 74 | name: name 75 | description: des 76 | version: ver 77 | license: lic 78 | template: templ 79 | app: app 80 | ) 81 | 82 | win.components = win.components.filter(mut it !is ui.Modal) 83 | 84 | mut com := win.get[&ui.Tree2]('proj-tree') 85 | refresh_tree(mut win, dir, mut com) 86 | } 87 | 88 | fn make_license_section() &ui.Panel { 89 | mut p := ui.Panel.new( 90 | layout: ui.BoxLayout.new(ori: 1) 91 | ) 92 | 93 | choices := [ 94 | 'MIT', 95 | 'Unlicense/CC0', 96 | 'GPL', 97 | 'Apache', 98 | 'Mozilla Public', 99 | 'All Rights Reserved', 100 | 'Other', 101 | ] 102 | 103 | mut box := ui.Selectbox.new( 104 | text: 'MIT' 105 | items: choices 106 | ) 107 | box.set_bounds(-5, 0, 200, 0) 108 | 109 | box.subscribe_event('item_change', fn (mut e ui.ItemChangeEvent) { 110 | e.ctx.win.extra_map['np-lic'] = e.target.text 111 | }) 112 | 113 | p.add_child(box) 114 | return p 115 | } 116 | 117 | fn make_templ_section() &ui.Panel { 118 | mut p := ui.Panel.new( 119 | layout: ui.BoxLayout.new(ori: 1) 120 | ) 121 | 122 | choices := ['hello_world', 'web', 'basic_window', 'border_layout'] 123 | 124 | mut group := ui.buttongroup[ui.Checkbox]() 125 | for choice in choices { 126 | mut box := ui.Checkbox.new(text: choice) 127 | box.set_bounds(0, 4, 190, 30) 128 | box.subscribe_event('draw', checkbox_pack_height) 129 | 130 | group.add(box) 131 | p.add_child(box) 132 | } 133 | 134 | group.subscribe_event('mouse_up', fn (mut e ui.MouseEvent) { 135 | e.ctx.win.extra_map['np-templ'] = e.target.text 136 | }) 137 | 138 | group.setup() 139 | return p 140 | } 141 | 142 | fn checkbox_pack_height(mut e ui.DrawEvent) { 143 | e.target.height = e.ctx.line_height + 5 144 | } 145 | 146 | fn create_input(mut win ui.Window, title string, val string) &ui.Panel { 147 | mut box := ui.Panel.new( 148 | layout: ui.BoxLayout.new(ori: 0, vgap: 1) 149 | ) 150 | 151 | mut work_lbl := ui.Label.new(text: title) 152 | 153 | work_lbl.set_bounds(0, 4, 125, 30) 154 | box.add_child(work_lbl) 155 | 156 | mut work := ui.TextField.new(text: val) 157 | 158 | work.subscribe_event('draw', fn (mut e ui.DrawEvent) { 159 | txt := e.target.text 160 | e.target.width = int(f64_max(300, e.ctx.text_width(txt) + txt.len)) 161 | e.target.height = e.ctx.line_height + 8 162 | }) 163 | 164 | work.set_id(mut win, 'NewProj-' + title) 165 | box.add_child(work) 166 | 167 | return box 168 | } 169 | -------------------------------------------------------------------------------- /src/verminal.v: -------------------------------------------------------------------------------- 1 | // 2 | // Verminal - Terminal Emulator in V 3 | // 4 | module main 5 | 6 | import iui as ui 7 | import os 8 | 9 | pub fn create_box(mut win ui.Window) &ui.Textbox { 10 | path := os.real_path(os.home_dir()) 11 | win.extra_map['path'] = path 12 | 13 | mut box := ui.Textbox.new(lines: [path + '>']) 14 | box.set_id(mut win, 'vermbox') 15 | 16 | box.subscribe_event('draw', vermbox_draw) 17 | 18 | box.before_txtc_event_fn = before_txt_change 19 | box.set_bounds(0, 0, 300, 80) 20 | 21 | return box 22 | } 23 | 24 | fn vermbox_draw(mut e ui.DrawEvent) { 25 | mut this := e.ctx.win.get[&ui.Textbox]('vermbox') 26 | 27 | this.caret_y = this.lines.len - 1 28 | line := this.lines[this.caret_y] 29 | cp := e.ctx.win.extra_map['path'] 30 | 31 | if line.contains(cp + '>') { 32 | if this.caret_x < cp.len + 1 { 33 | this.caret_x = cp.len + 1 34 | } 35 | } 36 | 37 | hei := (this.lines.len + 1) * ui.get_line_height(e.ctx) 38 | pw := this.parent.height 39 | if hei < pw { 40 | this.height = pw 41 | } else { 42 | this.height = hei 43 | } 44 | 45 | this.width = this.parent.width 46 | 47 | if 'update_scroll' in e.ctx.win.extra_map { 48 | jump_sv(mut e.ctx.win, this.height, this.lines.len) 49 | e.ctx.win.extra_map.delete('update_scroll') 50 | } 51 | } 52 | 53 | fn before_txt_change(mut win ui.Window, tb ui.Textbox) bool { 54 | is_backsp := tb.last_letter == 'backspace' 55 | 56 | if is_backsp { 57 | txt := tb.lines[tb.caret_y] 58 | path := win.extra_map['path'] 59 | if txt.ends_with(path + '>') { 60 | return true 61 | } 62 | } 63 | 64 | is_enter := tb.last_letter == 'enter' 65 | jump_sv(mut win, tb.height, tb.lines.len) 66 | 67 | if is_enter { 68 | mut tbox := win.get[&ui.Textbox]('vermbox') 69 | tbox.last_letter = '' 70 | 71 | mut txt := tb.lines[tb.caret_y] 72 | mut cline := txt // txt[txt.len - 1] 73 | mut path := win.extra_map['path'] 74 | 75 | if cline.contains(path + '>') { 76 | mut cmd := cline.split(path + '>')[1] 77 | on_cmd(mut win, tb, cmd) 78 | } 79 | return true 80 | } 81 | return false 82 | } 83 | 84 | fn jump_sv(mut win ui.Window, tbh int, lines int) { 85 | mut sv := win.get[&ui.ScrollView]('vermsv') 86 | val := tbh - sv.height 87 | if lines <= 1 { 88 | sv.scroll_i = 0 89 | return 90 | } 91 | sv.scroll_i = val / sv.increment 92 | } 93 | 94 | fn on_cmd(mut win ui.Window, box ui.Textbox, cmd string) { 95 | args := cmd.split(' ') 96 | 97 | mut tbox := win.get[&ui.Textbox]('vermbox') 98 | if args[0] == 'cd' { 99 | cmd_cd(mut win, mut tbox, args) 100 | add_new_input_line(mut tbox, win) 101 | } else if args[0] == 'help' { 102 | tbox.lines << win.extra_map['verm-help'] 103 | add_new_input_line(mut tbox, win) 104 | } else if args[0] == 'version' || args[0] == 'ver' { 105 | tbox.lines << 'Verminal: 0.5' 106 | add_new_input_line(mut tbox, win) 107 | } else if args[0] == 'cls' || args[0] == 'clear' { 108 | tbox.lines.clear() 109 | tbox.scroll_i = 0 110 | add_new_input_line(mut tbox, win) 111 | } else if args[0] == 'font-size' { 112 | win.font_size = args[1].int() 113 | add_new_input_line(mut tbox, win) 114 | } else if args[0] == 'dira' { 115 | mut path := win.extra_map['path'] 116 | cmd_dir(mut tbox, path, args) 117 | add_new_input_line(mut tbox, win) 118 | } else if args[0] == 'loadfiles' { 119 | $if emscripten ? { 120 | C.emscripten_run_script(c'iui.trigger = "lloadfiles"') 121 | } 122 | mut com := win.get[&ui.Tree2]('proj-tree') 123 | if args.len == 1 { 124 | refresh_tree(mut win, '/home/web_user/.vide/workspace', mut com) 125 | } else { 126 | refresh_tree(mut win, args[1], mut com) 127 | } 128 | } else if args[0] == 'v' || args[0] == 'dir' || args[0] == 'git' { 129 | spawn verminal_cmd_exec(mut win, mut tbox, args) 130 | } else if args[0].len == 2 && args[0].ends_with(':') { 131 | win.extra_map['path'] = os.real_path(args[0]) 132 | add_new_input_line(mut tbox, win) 133 | tbox.caret_y += 1 134 | } else { 135 | verminal_cmd_exec(mut win, mut tbox, args) 136 | } 137 | 138 | jump_sv(mut win, box.height, tbox.lines.len) 139 | 140 | win.extra_map['update_scroll'] = 'true' 141 | win.extra_map['lastcmd'] = cmd 142 | } 143 | 144 | fn wasm_save_files() { 145 | $if emscripten ? { 146 | C.emscripten_run_script(c'iui.trigger = "savefiles"') 147 | } 148 | } 149 | 150 | fn write_file(path string, text string) ! { 151 | println('Writing content to ${path}') 152 | os.write_file(path, text)! 153 | if path.ends_with('.v') || path.ends_with('.mod') { 154 | wasm_save_files() 155 | } 156 | } 157 | 158 | fn add_new_input_line(mut tbox ui.Textbox, win &ui.Window) { 159 | tbox.lines << win.extra_map['path'] + '>' 160 | } 161 | -------------------------------------------------------------------------------- /.github/workflows/rele.yml: -------------------------------------------------------------------------------- 1 | name: Build binary artifacts 2 | 3 | on: 4 | push: 5 | tags: 6 | - weekly.** 7 | - 0.** 8 | 9 | jobs: 10 | 11 | build-linux: 12 | runs-on: ubuntu-20.04 13 | env: 14 | CC: gcc 15 | ZIPNAME: vide_linux.zip 16 | steps: 17 | - name: Setup V 18 | uses: vlang/setup-v@v1 19 | with: 20 | # Default: ${{ github.token }} 21 | token: ${{ github.token }} 22 | version: 'weekly.2023.02' 23 | version-file: '' 24 | check-latest: true 25 | stable: false 26 | architecture: '' 27 | - uses: actions/checkout@v1 28 | - name: Compile 29 | run: | 30 | sudo apt-get -qq update 31 | sudo apt-get -qq install libgc-dev 32 | sudo apt install build-essential 33 | sudo apt-get --yes --force-yes install libxi-dev libxcursor-dev mesa-common-dev 34 | sudo apt-get --yes --force-yes install libgl1-mesa-glx 35 | v install https://github.com/isaiahpatton/ui 36 | git clone https://github.com/isaiahpatton/vide 37 | v -cc $CC -skip-unused -gc boehm vide 38 | - name: Remove excluded 39 | run: | 40 | rm -rf .git 41 | - name: Create ZIP archive 42 | run: | 43 | zip -r9 --symlinks $ZIPNAME vide/ 44 | - name: Create artifact 45 | uses: actions/upload-artifact@v2 46 | with: 47 | name: linux 48 | path: vide_linux.zip 49 | 50 | build-macos: 51 | runs-on: macos-latest 52 | env: 53 | CC: clang 54 | ZIPNAME: vide_macos.zip 55 | steps: 56 | - name: Setup V 57 | uses: vlang/setup-v@v1 58 | with: 59 | # Default: ${{ github.token }} 60 | token: ${{ github.token }} 61 | version: 'weekly.2023.02' 62 | version-file: '' 63 | check-latest: true 64 | stable: false 65 | architecture: '' 66 | - uses: actions/checkout@v1 67 | - name: Compile 68 | run: | 69 | v install https://github.com/isaiahpatton/ui 70 | git clone https://github.com/isaiahpatton/vide 71 | v -cc $CC -skip-unused -gc boehm vide 72 | - name: Remove excluded 73 | run: | 74 | rm -rf .git 75 | - name: Create ZIP archive 76 | run: | 77 | zip -r9 --symlinks $ZIPNAME vide/ 78 | - name: Create artifact 79 | uses: actions/upload-artifact@v2 80 | with: 81 | name: macos 82 | path: vide_macos.zip 83 | 84 | build-windows: 85 | runs-on: windows-latest 86 | env: 87 | CC: msvc 88 | ZIPNAME: vide_windows.zip 89 | steps: 90 | - name: Setup V 91 | uses: vlang/setup-v@v1 92 | with: 93 | # Default: ${{ github.token }} 94 | token: ${{ github.token }} 95 | version: 'weekly.2023.02' 96 | version-file: '' 97 | check-latest: true 98 | stable: false 99 | architecture: '' 100 | - uses: actions/checkout@v1 101 | - uses: msys2/setup-msys2@v2 102 | - name: Compile 103 | run: | 104 | git clone https://github.com/vlang/v 105 | cd v 106 | .\make.bat 107 | .\v.exe install https://github.com/isaiahpatton/ui 108 | .\v.exe symlink 109 | git clone https://github.com/isaiahpatton/vide 110 | v -cc gcc -skip-unused -gc boehm -cflags -static vide 111 | - name: Remove excluded 112 | shell: msys2 {0} 113 | run: | 114 | rm -rf .git 115 | cd v 116 | cd vide 117 | rm -rf *.v 118 | rm -rf .git 119 | cd .. 120 | cd .. 121 | - name: Create archive 122 | shell: msys2 {0} 123 | run: | 124 | cd v 125 | cd vide 126 | cd .. 127 | powershell Compress-Archive vide $ZIPNAME 128 | mv $ZIPNAME ../ 129 | cd .. 130 | # NB: the powershell Compress-Archive line is from: 131 | # https://superuser.com/a/1336434/194881 132 | # It is needed, because `zip` is not installed by default :-| 133 | - name: Create artifact 134 | uses: actions/upload-artifact@v2 135 | with: 136 | name: windows 137 | path: vide_windows.zip 138 | 139 | release: 140 | name: Create Github Release 141 | needs: [build-linux, build-windows, build-macos] 142 | runs-on: ubuntu-20.04 143 | steps: 144 | - name: Get short tag name 145 | uses: jungwinter/split@v1 146 | id: split 147 | with: 148 | msg: ${{ github.ref }} 149 | seperator: / 150 | - name: Create Release 151 | id: create_release 152 | uses: ncipollo/release-action@v1 153 | with: 154 | token: ${{ secrets.GITHUB_TOKEN }} 155 | tag: ${{ steps.split.outputs._2 }} 156 | name: ${{ steps.split.outputs._2 }} 157 | commit: ${{ github.sha }} 158 | draft: false 159 | prerelease: false 160 | 161 | publish: 162 | needs: [release] 163 | runs-on: ubuntu-20.04 164 | strategy: 165 | matrix: 166 | version: [linux, macos, windows] 167 | steps: 168 | - uses: actions/checkout@v1 169 | - name: Fetch artifacts 170 | uses: actions/download-artifact@v1 171 | with: 172 | name: ${{ matrix.version }} 173 | path: ./${{ matrix.version }} 174 | - name: Get short tag name 175 | uses: jungwinter/split@v1 176 | id: split 177 | with: 178 | msg: ${{ github.ref }} 179 | seperator: / 180 | - name: Get release 181 | id: get_release_info 182 | uses: bruceadams/get-release@v1.3.2 183 | env: 184 | GITHUB_TOKEN: ${{ github.token }} 185 | - name: Upload Release Asset 186 | id: upload-release-asset 187 | uses: actions/upload-release-asset@v1.0.1 188 | env: 189 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 190 | with: 191 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} 192 | asset_path: ${{ matrix.version }}/vide_${{ matrix.version }}.zip 193 | asset_name: vide_${{ matrix.version }}.zip 194 | asset_content_type: application/zip 195 | -------------------------------------------------------------------------------- /src/code_suggestions.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | 6 | const modules = ['arrays', 'benchmark', 'bitfield', 'cli', 'clipboard', 'clipboard.dummy', 7 | 'clipboard.x11', 'compress', 'compress.deflate', 'compress.gzip', 'compress.zlib', 'context', 8 | 'context.onecontext', 'crypto', 'crypto.aes', 'crypto.bcrypt', 'crypto.blowfish', 'crypto.cipher', 9 | 'crypto.des', 'crypto.ed25519', 'crypto.ed25519.internal.edwards25519', 'crypto.hmac', 10 | 'crypto.internal.subtle', 'crypto.md5', 'crypto.pem', 'crypto.rand', 'crypto.rc4', 'crypto.sha1', 11 | 'crypto.sha256', 'crypto.sha512', 'datatypes', 'datatypes.fsm', 'main', 'db', 'db.mssql', 12 | 'db.mysql', 'db.pg', 'db.sqlite', 'dl', 'dl.loader', 'dlmalloc', 'encoding', 'encoding.base32', 13 | 'encoding.base58', 'encoding.base64', 'encoding.binary', 'encoding.csv', 'encoding.hex', 14 | 'encoding.html', 'encoding.leb128', 'encoding.utf8', 'encoding.utf8.east_asian', 'eventbus', 15 | 'flag', 'fontstash', 'gg', 'gg.m4', 'gx', 'hash', 'hash.crc32', 'hash.fnv1a', 'io', 'io.util', 16 | 'json', 'json.cjson', 'log', 'maps', 'math', 'math.big', 'math.bits', 'math.complex', 17 | 'math.fractions', 'math.internal', 'math.stats', 'math.unsigned', 'math.vec', 'mssql', 'mysql', 18 | 'net', 'net.conv', 'net.ftp', 'net.html', 'net.http', 'net.http.chunked', 'net.http.mime', 19 | 'net.mbedtls', 'net.openssl', 'net.smtp', 'net.ssl', 'net.unix', 'net.urllib', 'net.websocket', 20 | 'orm', 'os', 'os.cmdline', 'os.filelock', 'os.font', 'os.notify', 'pg', 'picoev', 21 | 'picohttpparser', 'rand', 'rand.buffer', 'rand.config', 'rand.constants', 'rand.mt19937', 22 | 'rand.musl', 'rand.pcg32', 'rand.seed', 'rand.splitmix64', 'rand.sys', 'rand.wyrand', 23 | 'rand.xoroshiro128pp', 'readline', 'regex', 'runtime', 'semver', 'sokol', 'sokol.audio', 24 | 'sokol.c', 'sokol.f', 'sokol.gfx', 'sokol.memory', 'sokol.sapp', 'sokol.sfons', 'sokol.sgl', 25 | 'sqlite', 'stbi', 'strconv', 'strings', 'strings.textscanner', 'sync', 'sync.pool', 26 | 'sync.stdatomic', 'szip', 'term', 'term.termios', 'term.ui', 'time', 'time.misc', 'toml', 27 | 'toml.ast', 'toml.ast.walker', 'toml.checker', 'toml.decoder', 'toml.input', 'toml.parser', 28 | 'toml.scanner', 'toml.to', 'toml.token', 'toml.util', 'vweb', 'vweb.assets', 'vweb.csrf', 29 | 'vweb.sse', 'wasm', 'x', 'x.json2', 'x.ttf'] 30 | 31 | fn find_all_dot_match(sub string, mut e ui.DrawTextlineEvent) ([]string, int, int) { 32 | doti := sub.index('.') or { return [''], 0, 0 } 33 | dot := sub[0..(doti + 1)] 34 | aft := sub[(doti + 1)..] 35 | 36 | dw := e.ctx.gg.text_width(dot) 37 | 38 | trim := dot.trim_space() 39 | 40 | mut mats := find_all_matches(mut e.ctx.win, trim, aft) 41 | mats.sort(a.len > b.len) 42 | return mats, dw, aft.len 43 | } 44 | 45 | fn text_box_active_line_draw(mut e ui.DrawTextlineEvent) { 46 | mut box := e.target 47 | if mut box is ui.Textbox { 48 | txt := box.lines[e.line] 49 | 50 | sub := txt[0..box.caret_x].replace('\t', ' '.repeat(8)) 51 | 52 | line_height := ui.get_line_height(e.ctx) + 5 53 | 54 | mut mats, mut dw, mut aft := []string{}, 0, 0 55 | 56 | if sub.index('.') or { -1 } != -1 { 57 | mats, dw, aft = find_all_dot_match(sub, mut e) 58 | } 59 | 60 | mut app := e.ctx.win.get[&App]('app') 61 | 62 | if app.popup.shown { 63 | app.popup.hide(e.ctx) 64 | } 65 | 66 | if sub.starts_with('import ') && sub.len > 'import '.len { 67 | spl := sub.split('import ')[1] 68 | for s in modules { 69 | if s.starts_with(spl) { 70 | mats << s 71 | } 72 | } 73 | if mats.len == 1 && sub.contains(mats[0]) { 74 | return 75 | } 76 | dw = e.ctx.text_width('import ') 77 | aft = spl.len 78 | } 79 | 80 | if mats.len == 0 { 81 | if app.popup.shown { 82 | app.popup.hide(e.ctx) 83 | } 84 | return 85 | } 86 | 87 | if mats.len == 1 { 88 | if sub.ends_with(mats[0]) { 89 | return 90 | } 91 | } 92 | 93 | mut max_wid := e.ctx.gg.text_width(mats[0] + ' ') 94 | if max_wid < 100 { 95 | max_wid = 100 96 | } 97 | 98 | for mat in mats { 99 | mw := e.ctx.gg.text_width(mat) 100 | if mw > max_wid { 101 | max_wid = mw 102 | } 103 | } 104 | 105 | x := e.x + dw - 4 106 | 107 | e.ctx.gg.draw_rect_empty(x, e.y, max_wid, line_height, e.ctx.theme.button_border_normal) 108 | 109 | px := e.x + dw - e.target.x - 4 110 | py := e.y + line_height - e.target.y 111 | 112 | if app.popup.shown { 113 | app.popup.hide(e.ctx) 114 | } 115 | 116 | app.popup.width = max_wid 117 | app.popup.sv.width = max_wid 118 | app.popup.p.width = max_wid 119 | ph := line_height * mats.len 120 | app.popup.p.height = ph 121 | if ph < 150 { 122 | app.popup.height = ph 123 | app.popup.sv.height = ph 124 | } else { 125 | app.popup.height = 150 126 | app.popup.sv.height = 150 127 | } 128 | 129 | app.popup.set_texts(mut box, mats, aft) 130 | app.popup.show(box, px, py, e.ctx) 131 | } 132 | } 133 | 134 | fn find_all_matches(mut win ui.Window, mod string, str string) []string { 135 | if str.len <= 0 { 136 | return [] 137 | } 138 | strs := find_all_fn_in_vlib(mut win, mod) 139 | 140 | for st in strs { 141 | if st == str { 142 | return [st] 143 | } 144 | } 145 | 146 | mut matches := []string{} 147 | for st in strs { 148 | if st.contains(str) && !matches.contains(st) { 149 | matches << st 150 | } 151 | } 152 | return matches 153 | } 154 | 155 | fn all_vlib_mod(mut win ui.Window) []string { 156 | id := 'vlib' 157 | if id in win.extra_map { 158 | return win.extra_map[id].split(' ') 159 | } 160 | 161 | mut arr := []string{} 162 | mut vlib := os.dir(get_v_exe()).replace('\\', '/') + '/vlib' 163 | for file in os.ls(vlib) or { [''] } { 164 | arr << file 165 | } 166 | win.extra_map[id] = arr.join(' ') 167 | return arr 168 | } 169 | 170 | fn find_all_fn_in_vlib(mut win ui.Window, mod string) []string { 171 | id := 'sug-' + mod 172 | if id in win.extra_map { 173 | return win.extra_map[id].split(' ') 174 | } 175 | 176 | mut arr := []string{} 177 | mut vlib := os.dir(get_v_exe()).replace('\\', '/') + '/vlib' 178 | mut mod_dir := vlib + '/' + mod 179 | for file in os.ls(mod_dir) or { [''] } { 180 | lines := os.read_lines(mod_dir + '/' + file) or { [''] } 181 | for line in lines { 182 | if line.starts_with('pub fn') && !line.starts_with('pub fn (') { 183 | name := line.split('pub fn ')[1].split('(')[0] 184 | arr << name 185 | } 186 | } 187 | } 188 | win.extra_map[id] = arr.join(' ') 189 | return arr 190 | } 191 | -------------------------------------------------------------------------------- /src/tabs.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | import clipboard 6 | 7 | fn (mut app App) welcome_tab(folder string) { 8 | mut logo := ui.image_from_bytes(mut app.win, vide_png1.to_bytes(), 229, 90) 9 | logo.set_bounds(0, 0, 229, 90) 10 | 11 | mut info_lbl := ui.Label.new( 12 | text: 'Simple IDE for V made in V.' 13 | bold: true 14 | ) 15 | 16 | padding_top := 5 17 | 18 | mut vbox := ui.Panel.new( 19 | layout: ui.BoxLayout.new(ori: 1, vgap: 15) 20 | ) 21 | 22 | info_lbl.set_pos(0, 0) 23 | info_lbl.pack() 24 | 25 | mut hbox := ui.Panel.new( 26 | layout: ui.BoxLayout.new(hgap: 0, vgap: 0) 27 | ) 28 | 29 | hbox.add_child(logo) 30 | hbox.set_bounds(0, 0, 230, 51) 31 | 32 | vbox.add_child(hbox) 33 | vbox.add_child(info_lbl) 34 | 35 | mut sw := ui.Titlebox.new(text: 'Start', children: [app.start_with()], padding: 4) 36 | vbox.add_child(sw) 37 | 38 | mut lbox := app.links_box() 39 | lbox.set_pos(1, 0) 40 | 41 | mut box := ui.Panel.new( 42 | layout: ui.BorderLayout.new(hgap: 25) 43 | ) 44 | box.set_bounds(0, padding_top, 550, 350) 45 | box.subscribe_event('draw', center_box) 46 | 47 | mut sbox := app.south_panel() 48 | 49 | box.add_child_with_flag(vbox, ui.borderlayout_center) 50 | box.add_child_with_flag(lbox, ui.borderlayout_east) 51 | box.add_child_with_flag(sbox, ui.borderlayout_south) 52 | 53 | mut sv := ui.ScrollView.new( 54 | view: box 55 | ) 56 | sv.set_border_painted(false) 57 | 58 | app.tb.add_child('Welcome', sv) 59 | } 60 | 61 | fn center_box(mut e ui.DrawEvent) { 62 | pw := e.target.parent.width 63 | x := (pw / 2) - (e.target.width / 2) 64 | if pw > 550 { 65 | e.target.set_x(x / 2) 66 | } 67 | } 68 | 69 | fn (mut app App) start_with() &ui.Panel { 70 | mut p := ui.Panel.new(layout: ui.BoxLayout.new(ori: 1)) 71 | 72 | mut btn := ui.Button.new(text: 'New Project') 73 | btn.set_bounds(0, 0, 150, 30) 74 | btn.subscribe_event('mouse_up', fn [mut app] (mut e ui.MouseEvent) { 75 | app.new_project(mut e.ctx.win) 76 | }) 77 | 78 | mut btn2 := ui.Button.new(text: 'V Documentation') 79 | btn2.set_bounds(0, 0, 150, 30) 80 | btn2.subscribe_event('mouse_up', fn (mut e ui.MouseEvent) { 81 | // app.new_project(mut e.ctx.win) 82 | ui.open_url('https://vlang.io/docs') 83 | // new_tab(e.ctx.win, 'C:\\v\\doc\\docs.md') 84 | }) 85 | 86 | p.add_child(btn) 87 | p.add_child(btn2) 88 | return p 89 | } 90 | 91 | fn (mut app App) south_panel() &ui.Panel { 92 | mut p := ui.Panel.new() 93 | 94 | res := os.execute('${app.confg.vexe} version') 95 | 96 | mut out := res.output 97 | if !out.contains('V ') { 98 | out = 'Error executing "v version"\nPlease see Settings' 99 | } 100 | 101 | mut btn := ui.Label.new( 102 | text: 'Compiler: ${out}' 103 | em_size: 0.85 104 | ) 105 | btn.pack() 106 | 107 | p.add_child(btn) 108 | return p 109 | } 110 | 111 | fn (mut app App) links_box() &ui.Panel { 112 | mut box := ui.Panel.new( 113 | layout: ui.BoxLayout.new(ori: 1, vgap: 6) 114 | ) 115 | 116 | mut title := ui.Label.new( 117 | text: 'Useful Links:' 118 | em_size: 1.2 119 | ) 120 | title.pack() 121 | box.add_child(title) 122 | 123 | links := [ 124 | 'V Documentation|vlang.io/docs', 125 | 'V stdlib docs|modules.vlang.io', 126 | 'V on Github|github.com/vlang/v', 127 | 'Vide on Github|github.com/pisaiah/vide', 128 | 'Vide on Discord|discord.gg/NruVtYBf5g', 129 | 'r/vlang|reddit.com/r/vlang', 130 | ] 131 | 132 | for val in links { 133 | spl := val.split('|') 134 | mut link := ui.link( 135 | text: spl[0] 136 | url: 'https://' + spl[1] 137 | ) 138 | link.set_bounds(4, 0, 150, 25) 139 | box.add_child(link) 140 | } 141 | 142 | mut vv := ui.Label.new( 143 | text: 'Vide™ ${version} - iUI ${ui.version}' 144 | em_size: 0.8 145 | ) 146 | vv.set_pos(0, 10) 147 | vv.set_bounds(5, 8, 150, 40) 148 | 149 | box.add_child(vv) 150 | return box 151 | } 152 | 153 | fn new_tab(window &ui.Window, file string) { 154 | dump('opening ' + file) 155 | mut tb := window.get[&ui.Tabbox]('main-tabs') 156 | 157 | if file in tb.kids { 158 | // Don't remake already open tab 159 | tb.active_tab = file 160 | return 161 | } 162 | 163 | if file.ends_with('.vide_test') { 164 | // TODO 165 | } 166 | 167 | if file.ends_with('.png') { 168 | // Test 169 | p := image_view(file) 170 | tb.add_child(file, p) 171 | tb.active_tab = file 172 | return 173 | } 174 | 175 | lines := os.read_lines(file) or { ['ERROR while reading file contents'] } 176 | 177 | mut code_box := ui.Textbox.new(lines: lines) 178 | code_box.text = file 179 | 180 | code_box.set_bounds(0, 0, 620, 250) 181 | 182 | mut scroll_view := ui.scroll_view( 183 | bounds: ui.Bounds{0, 0, 620, 250} 184 | view: code_box 185 | increment: 16 186 | padding: 0 187 | ) 188 | 189 | scroll_view.set_border_painted(false) 190 | 191 | scroll_view.subscribe_event('draw', fn (mut e ui.DrawEvent) { 192 | mut tb := e.ctx.win.get[&ui.Tabbox]('main-tabs') 193 | e.target.width = tb.width 194 | e.target.height = tb.height - 26 195 | }) 196 | 197 | code_box.subscribe_event('draw', code_box_draw) 198 | 199 | code_box.subscribe_event('current_line_draw', text_box_active_line_draw) 200 | 201 | code_box.before_txtc_event_fn = text_change 202 | 203 | tb.add_child(file, scroll_view) 204 | tb.active_tab = file 205 | } 206 | 207 | fn execute_syntax_check(file string) { 208 | vexe := get_v_exe() 209 | res := os.execute('${vexe} -check-syntax ${file}') 210 | dump(res) 211 | } 212 | 213 | fn code_box_draw(mut e ui.DrawEvent) { 214 | mut tb := e.ctx.win.get[&ui.Tabbox]('main-tabs') 215 | mut cb := e.target 216 | file := e.target.text 217 | 218 | if mut cb is ui.Textbox { 219 | e.target.width = tb.width 220 | hei := ui.get_line_height(e.ctx) * (cb.lines.len + 1) 221 | min := tb.height - 30 222 | if hei > min { 223 | cb.height = hei 224 | } else if cb.height < min { 225 | cb.height = min 226 | } 227 | 228 | // Do save 229 | if cb.ctrl_down && cb.last_letter == 's' { 230 | cb.ctrl_down = false 231 | write_file(file, cb.lines.join('\n')) or {} 232 | execute_syntax_check(file) 233 | } 234 | 235 | // Copy 236 | if cb.ctrl_down && cb.last_letter == 'c' { 237 | cb.ctrl_down = false 238 | // dump(cb.sel) 239 | } 240 | 241 | // Paste 242 | if cb.ctrl_down && cb.last_letter == 'v' { 243 | do_paste(mut cb) 244 | } 245 | } 246 | } 247 | 248 | fn do_paste(mut cb ui.Textbox) { 249 | cb.ctrl_down = false 250 | mut c := clipboard.new() 251 | 252 | cl := cb.lines[cb.caret_y] 253 | be := cl[..cb.caret_x] 254 | af := cl[cb.caret_x..] 255 | 256 | plines := c.get_text().split_into_lines() 257 | 258 | if plines.len == 0 { 259 | c.destroy() 260 | return 261 | } 262 | 263 | if plines.len == 1 { 264 | cb.lines[cb.caret_y] = be + plines[0] + af 265 | } else { 266 | cb.lines[cb.caret_y] = be + plines[0] 267 | for i in 1 .. plines.len - 1 { 268 | cb.lines.insert(cb.caret_y + i, plines[i]) 269 | } 270 | cb.lines.insert(cb.caret_y + (plines.len - 1), plines[plines.len - 1] + af) 271 | cb.caret_y = cb.caret_y + (plines.len - 1) 272 | cb.caret_x = plines[plines.len - 1].len 273 | } 274 | 275 | c.destroy() 276 | } 277 | 278 | fn text_change(mut w ui.Window, cb ui.Textbox) bool { 279 | if cb.last_letter == 'backspace' { 280 | } 281 | 282 | if cb.ctrl_down && cb.last_letter == 'c' { 283 | mut x0 := cb.sel or { ui.Selection{} }.x0 284 | mut y0 := cb.sel or { ui.Selection{} }.y0 285 | 286 | mut x1 := cb.sel or { ui.Selection{} }.x1 287 | mut y1 := cb.sel or { ui.Selection{} }.y1 288 | 289 | if y1 > cb.lines.len - 1 { 290 | y1 = cb.lines.len - 1 291 | } 292 | 293 | if y1 < y0 { 294 | sy := if y1 > y0 { y0 } else { y1 } 295 | ey := if y1 > y0 { y1 } else { y0 } 296 | y0 = sy 297 | y1 = ey 298 | sx := x1 299 | ex := x0 300 | x0 = sx 301 | x1 = ex 302 | } 303 | 304 | mut lines := []string{} 305 | 306 | fl := cb.lines[y0][x0..] 307 | el := cb.lines[y1][..x1] 308 | 309 | lines << fl 310 | for i in (y0 + 1) .. y1 { 311 | lines << cb.lines[i] 312 | } 313 | lines << el 314 | 315 | mut c := clipboard.new() 316 | c.copy(lines.join('\n')) 317 | c.destroy() 318 | 319 | return true 320 | } 321 | return false 322 | } 323 | -------------------------------------------------------------------------------- /src/menus.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | 6 | const vide_png0 = $embed_file('assets/ezgif.com-gif-maker(5).png') 7 | const vide_png1 = $embed_file('assets/word.png') 8 | 9 | const i_w = 24 10 | const i_h = 24 11 | 12 | pub fn (mut app App) iicon(c bool, b []u8) &ui.Image { 13 | if !c { 14 | return unsafe { nil } 15 | } 16 | return ui.image_from_bytes(mut app.win, b, i_w, i_h) 17 | } 18 | 19 | fn (mut app App) make_menubar() { 20 | // Setup Menubar and items 21 | mut window := app.win 22 | window.bar = ui.Menubar.new() 23 | window.bar.set_padding(4) 24 | 25 | // file_img := $embed_file('assets/file-icon.png') 26 | // edit_img := $embed_file('assets/icons8-edit-24.png') 27 | // help_img := $embed_file('assets/help-icon.png') 28 | save_img := $embed_file('assets/icons8-save-24.png') 29 | // theme_img := $embed_file('assets/icons8-change-theme-24.png') 30 | run_img := $embed_file('assets/run.png') 31 | fmt_img := $embed_file('assets/fmt.png') 32 | 33 | colored := true 34 | 35 | // file_icon := app.iicon(colored, file_img.to_bytes()) 36 | // edit_icon := app.iicon(colored, edit_img.to_bytes()) 37 | // help_icon := app.iicon(colored, help_img.to_bytes()) 38 | save_icon := app.iicon(colored, save_img.to_bytes()) 39 | // theme_icon := app.iicon(colored, theme_img.to_bytes()) 40 | run_icon := app.iicon(colored, run_img.to_bytes()) 41 | fmt_icon := app.iicon(colored, fmt_img.to_bytes()) 42 | 43 | file_menu := ui.MenuItem.new( 44 | text: 'File' 45 | // icon: file_icon 46 | uicon: '\uE132' 47 | children: [ 48 | ui.MenuItem.new( 49 | text: 'New Project..' 50 | uicon: '\uE9AF' 51 | click_event_fn: app.new_project_click 52 | ), 53 | ui.MenuItem.new( 54 | text: 'New File...' 55 | uicon: '\uE132' 56 | // click_event_fn: new_file_click 57 | ), 58 | ui.MenuItem.new( 59 | uicon: '\uE105' 60 | text: 'Save' 61 | click_event_fn: save_click 62 | ), 63 | ui.MenuItem.new( 64 | text: 'Run' 65 | uicon: '\uEA16' 66 | click_event_fn: run_click 67 | ), 68 | ui.MenuItem.new( 69 | text: 'Manage Modules..' 70 | uicon: '\uEAE8' 71 | // click_event_fn: vpm_click_ 72 | ), 73 | ui.MenuItem.new( 74 | text: 'Settings' 75 | uicon: '\uF8B0' 76 | click_event_fn: settings_click 77 | ), 78 | ui.MenuItem.new( 79 | text: 'Manage V' 80 | uicon: '\uEC7A' 81 | // click_event_fn: show_install_modal 82 | ), 83 | ] 84 | ) 85 | 86 | edit_menu := ui.MenuItem.new( 87 | text: 'Edit' 88 | // icon: edit_icon 89 | uicon: '\uE104' 90 | ) 91 | 92 | help_menu := ui.MenuItem.new( 93 | text: 'Help' 94 | uicon: '\uEA0D' 95 | // icon: help_icon 96 | children: [ 97 | ui.MenuItem.new( 98 | text: 'About Vide' 99 | click_event_fn: about_click 100 | ), 101 | ui.MenuItem.new( 102 | text: 'Github' 103 | click_event_fn: gh_click 104 | ), 105 | ui.MenuItem.new( 106 | text: 'Discord' 107 | click_event_fn: dis_click 108 | ), 109 | ui.MenuItem.new( 110 | text: 'About iUI' 111 | ), 112 | ] 113 | ) 114 | 115 | mut theme_menu := ui.MenuItem.new( 116 | text: 'Themes' 117 | // icon: theme_icon 118 | uicon: '\uE9D7' 119 | ) 120 | 121 | themes := tmanager.get_themes() 122 | for theme2 in themes { 123 | item := ui.MenuItem.new(text: theme2.name, click_event_fn: on_theme_click) 124 | theme_menu.add_child(item) 125 | } 126 | 127 | item := ui.MenuItem.new(text: 'Vide Default Dark', click_event_fn: on_theme_click) 128 | theme_menu.add_child(item) 129 | 130 | item_ := ui.MenuItem.new(text: 'Vide Light Theme', click_event_fn: on_theme_click) 131 | theme_menu.add_child(item_) 132 | 133 | save_menu := ui.MenuItem.new( 134 | // text: 'Save' 135 | icon: save_icon 136 | uicon: '\uE105' 137 | click_event_fn: save_click 138 | ) 139 | 140 | run_menu := ui.MenuItem.new( 141 | // text: 'Run' 142 | icon: run_icon 143 | uicon: '\uEA16' 144 | click_event_fn: run_click 145 | ) 146 | 147 | fmt_menu := ui.MenuItem.new( 148 | // text: 'v fmt' 149 | icon: fmt_icon 150 | uicon: '\uEA5D' 151 | click_event_fn: fmt_click 152 | ) 153 | 154 | window.bar.add_child(file_menu) 155 | window.bar.add_child(edit_menu) 156 | window.bar.add_child(help_menu) 157 | window.bar.add_child(theme_menu) 158 | 159 | window.bar.add_child(save_menu) 160 | window.bar.add_child(run_menu) 161 | window.bar.add_child(fmt_menu) 162 | } 163 | 164 | fn (mut app App) set_theme_from_save() { 165 | /* 166 | name := app.get_saved_value('theme') 167 | if name.len > 1 { 168 | theme := ui.theme_by_name(name) 169 | app.win.set_theme(theme) 170 | theme.setup_fn(mut app.win) 171 | }*/ 172 | } 173 | 174 | fn settings_click(mut win ui.Window, com ui.MenuItem) { 175 | mut app := win.get[&App]('app') 176 | // file := os.join_path(app.confg.cfg_dir, 'config.yml') 177 | // new_tab(win, file) 178 | app.show_settings() 179 | } 180 | 181 | const tmanager = ui.ThemeManager.new() 182 | 183 | fn on_theme_click(mut win ui.Window, com ui.MenuItem) { 184 | if com.text == 'Vide Default Dark' { 185 | mut vt := vide_dark_theme() 186 | win.set_theme(vt) 187 | return 188 | } 189 | if com.text == 'Vide Light Theme' { 190 | mut vt := vide_light_theme() 191 | win.set_theme(vt) 192 | return 193 | } 194 | 195 | theme := tmanager.get_theme(com.text) 196 | mut app := win.get[&App]('app') 197 | app.confg.theme = com.text 198 | app.confg.save() 199 | win.set_theme(theme) 200 | } 201 | 202 | fn gh_click(mut win ui.Window, com ui.MenuItem) { 203 | ui.open_url('https://github.com/pisaiah/vide') 204 | } 205 | 206 | fn dis_click(mut win ui.Window, com ui.MenuItem) { 207 | ui.open_url('https://discord.gg/NruVtYBf5g') 208 | } 209 | 210 | fn about_click(mut win ui.Window, com ui.MenuItem) { 211 | mut modal := ui.Modal.new( 212 | title: 'About VIDE' 213 | width: 380 214 | height: 250 215 | ) 216 | 217 | mut p := ui.Panel.new( 218 | layout: ui.BoxLayout.new(ori: 1) 219 | ) 220 | p.set_pos(8, 16) 221 | 222 | mut label := ui.Label.new( 223 | text: 'Simple IDE for the V Language made in V.\n\nVersion: ${version}\niUI: ${ui.version}' 224 | ) 225 | 226 | label.pack() 227 | 228 | mut copy := ui.Label.new( 229 | text: 'Copyright © 2021-2025 by Isaiah.' 230 | em_size: 0.8 231 | pack: true 232 | ) 233 | copy.set_pos(16, 175) 234 | 235 | p.add_child(label) 236 | modal.add_child(copy) 237 | modal.add_child(p) 238 | win.add_child(modal) 239 | } 240 | 241 | fn save_click(mut win ui.Window, item ui.MenuItem) { 242 | do_save(mut win) 243 | } 244 | 245 | fn do_save(mut win ui.Window) { 246 | mut com := win.get[&ui.Tabbox]('main-tabs') 247 | 248 | mut tab := com.kids[com.active_tab] 249 | for mut sv in tab { 250 | if mut sv is ui.ScrollView { 251 | for mut child in sv.children { 252 | if mut child is ui.Textbox { 253 | write_file(com.active_tab, child.lines.join('\n')) or { 254 | // set_console_text(mut win, 'Unable to save file!') 255 | } 256 | } 257 | } 258 | } 259 | } 260 | } 261 | 262 | fn refresh_current_tab(mut win ui.Window, file string) { 263 | mut com := win.get[&ui.Tabbox]('main-tabs') 264 | 265 | mut tab := com.kids[com.active_tab] 266 | for mut sv in tab { 267 | if mut sv is ui.ScrollView { 268 | for mut child in sv.children { 269 | if mut child is ui.Textbox { 270 | old_x := child.caret_x 271 | old_y := child.caret_y 272 | 273 | lines := os.read_lines(file) or { child.lines } 274 | child.lines = lines 275 | 276 | child.caret_x = old_x 277 | child.caret_y = old_y 278 | } 279 | } 280 | } 281 | } 282 | } 283 | 284 | fn run_click(mut win ui.Window, item ui.MenuItem) { 285 | com := win.get[&ui.Tabbox]('main-tabs') 286 | 287 | txt := com.active_tab 288 | mut dir := os.dir(txt) 289 | 290 | if dir.ends_with('src') { 291 | dir = os.dir(dir) 292 | } 293 | 294 | args := ['v', '-skip-unused', 'run', dir] 295 | 296 | mut tbox := win.get[&ui.Textbox]('vermbox') 297 | 298 | spawn verminal_cmd_exec(mut win, mut tbox, args) 299 | 300 | jump_sv(mut win, tbox.height, tbox.lines.len) 301 | 302 | win.extra_map['update_scroll'] = 'true' 303 | win.extra_map['lastcmd'] = args.join(' ') 304 | } 305 | 306 | fn fmt_click(mut win ui.Window, item ui.MenuItem) { 307 | com := win.get[&ui.Tabbox]('main-tabs') 308 | 309 | txt := com.active_tab 310 | /* 311 | mut dir := os.dir(txt) 312 | 313 | if dir.ends_with('src') { 314 | dir = os.dir(dir) 315 | }*/ 316 | 317 | do_save(mut win) 318 | 319 | args := ['v', 'fmt', '-w', txt] 320 | 321 | mut tbox := win.get[&ui.Textbox]('vermbox') 322 | 323 | verminal_cmd_exec(mut win, mut tbox, args) 324 | 325 | jump_sv(mut win, tbox.height, tbox.lines.len) 326 | 327 | refresh_current_tab(mut win, txt) 328 | 329 | win.extra_map['update_scroll'] = 'true' 330 | win.extra_map['lastcmd'] = args.join(' ') 331 | } 332 | -------------------------------------------------------------------------------- /src/config.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import iui as ui 4 | import os 5 | 6 | struct Config { 7 | mut: 8 | cfg_dir string = os.join_path(os.home_dir(), '.vide') 9 | workspace_dir string 10 | vexe string 11 | font_path string 12 | font_size int = 18 13 | theme string = 'Vide Default Dark' 14 | open_paths []string 15 | } 16 | 17 | fn make_config() &Config { 18 | mut cfg := &Config{} 19 | 20 | file := os.join_path(cfg.cfg_dir, 'config.yml') 21 | 22 | if !os.exists(file) { 23 | cfg.load_defaults() 24 | } else { 25 | cfg.load_from_file() 26 | } 27 | 28 | cfg.save() 29 | 30 | return cfg 31 | } 32 | 33 | fn (mut this Config) load_from_file() { 34 | file := os.join_path(this.cfg_dir, 'config.yml') 35 | 36 | lines := os.read_lines(file) or { [''] } 37 | for line in lines { 38 | spl := line.split(': ') 39 | if spl[0].starts_with('# ') { 40 | continue 41 | } 42 | match spl[0] { 43 | 'cfg_dir' { this.cfg_dir = spl[1] } 44 | 'workspace_dir' { this.workspace_dir = spl[1] } 45 | 'vexe' { this.vexe = spl[1] } 46 | 'font_path' { this.font_path = spl[1] } 47 | 'font_size' { this.font_size = spl[1].int() } 48 | 'theme' { this.theme = spl[1] } 49 | 'open_paths' { this.open_paths = spl[1].split(',') } 50 | else {} 51 | } 52 | } 53 | } 54 | 55 | fn (mut this Config) save() { 56 | file := os.join_path(this.cfg_dir, 'config.yml') 57 | 58 | data := [ 59 | '# Vide Configuration', 60 | 'cfg_dir: ${this.cfg_dir}', 61 | 'workspace_dir: ${this.workspace_dir}', 62 | 'vexe: ${this.vexe}', 63 | 'font_path: ${this.font_path}', 64 | 'font_size: ${this.font_size}', 65 | 'theme: ${this.theme}', 66 | 'open_paths: ${this.open_paths.join(',')}', 67 | ] 68 | 69 | mut lic := ['\n\n# LICENSE.txt:', '#', '# Copyright (c) 2021-2023 Isaiah\n#', 70 | '# Permission is hereby granted, free of charge, to any person obtaining a copy of this', 71 | '# software and associated documentation files (the “Software”), to deal in the Software', 72 | '# without restriction, including without limitation the rights to use, copy, modify, merge', 73 | '# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons', 74 | '# to whom the Software is furnished to do so, subject to the following conditions:\n#', 75 | '# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n#', 76 | '# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.'] 77 | 78 | write_file(file, data.join('\n') + lic.join('\n')) or {} 79 | } 80 | 81 | fn (mut this Config) load_defaults() { 82 | dot_vide := os.join_path(os.home_dir(), '.vide') 83 | 84 | os.mkdir(dot_vide) or {} 85 | 86 | this.cfg_dir = dot_vide 87 | this.workspace_dir = os.join_path(dot_vide, 'workspace') 88 | 89 | mut font_path := os.join_path(dot_vide, 'FiraCode-Regular.ttf') 90 | if !os.exists(font_path) { 91 | mut font_file := $embed_file('assets/FiraCode-Regular.ttf') 92 | os.write_file_array(font_path, font_file.to_bytes()) or { font_path = ui.default_font() } 93 | } 94 | this.font_path = font_path 95 | this.vexe = 'v' 96 | } 97 | 98 | // Settings Page 99 | 100 | fn (mut app App) show_settings() { 101 | mut page := ui.Page.new(title: 'Vide Settings') 102 | 103 | mut p := ui.Panel.new( 104 | layout: ui.BoxLayout.new( 105 | ori: 1 106 | ) 107 | ) 108 | p.set_pos(1, 1) 109 | 110 | mut ttf := app.s_cfg_dir() 111 | mut swp := app.s_workspace_dir() 112 | mut sexe := app.s_vexe() 113 | mut sfp := app.s_font_path() 114 | mut sfs := app.s_font_size() 115 | 116 | p.add_child(ttf) 117 | p.add_child(swp) 118 | p.add_child(sexe) 119 | p.add_child(sfp) 120 | p.add_child(sfs) 121 | 122 | p.subscribe_event('draw', fn (mut e ui.DrawEvent) { 123 | pw := e.target.parent.width 124 | size := if pw < 990 { pw } else { int(pw * f32(0.65)) } 125 | e.target.width = size - 10 126 | }) 127 | 128 | mut sv := ui.ScrollView.new( 129 | view: p 130 | ) 131 | 132 | page.add_child(sv) 133 | app.win.add_child(page) 134 | } 135 | 136 | fn set_field_width(mut e ui.DrawEvent) { 137 | tw := e.ctx.text_width(e.target.text) + 20 138 | e.target.width = if tw > 100 { tw } else { 100 } 139 | e.target.height = 30 140 | 141 | mut wid := 0 142 | for kid in e.target.parent.children { 143 | wid += kid.width 144 | } 145 | e.target.parent.width = wid + 20 146 | } 147 | 148 | fn (mut app App) s_cfg_dir() &ui.SettingsCard { 149 | mut p := ui.Panel.new(layout: ui.BoxLayout.new(ori: 0, vgap: 0)) 150 | 151 | mut tf := ui.TextField.new(text: app.confg.cfg_dir) 152 | mut sb := ui.Button.new(text: 'Save') 153 | sb.subscribe_event('mouse_up', fn [mut app, tf] (mut e ui.MouseEvent) { 154 | app.confg.cfg_dir = tf.text 155 | app.confg.save() 156 | }) 157 | 158 | tf.subscribe_event('draw', set_field_width) 159 | sb.set_bounds(0, 0, 100, 30) 160 | 161 | p.add_child(tf) 162 | p.add_child(sb) 163 | 164 | mut card := ui.SettingsCard.new( 165 | text: 'Config Directory' 166 | description: 'Where Vide stores files' 167 | stretch: true 168 | ) 169 | card.add_child(p) 170 | 171 | return card 172 | } 173 | 174 | fn (mut app App) s_workspace_dir() &ui.SettingsCard { 175 | mut p := ui.Panel.new(layout: ui.BoxLayout.new(ori: 0, vgap: 0)) 176 | 177 | mut tf := ui.TextField.new(text: app.confg.workspace_dir) 178 | mut sb := ui.Button.new(text: 'Save') 179 | sb.subscribe_event('mouse_up', fn [mut app, tf] (mut e ui.MouseEvent) { 180 | app.confg.workspace_dir = tf.text 181 | app.confg.save() 182 | }) 183 | tf.subscribe_event('draw', set_field_width) 184 | sb.set_bounds(0, 0, 100, 30) 185 | 186 | p.add_child(tf) 187 | p.add_child(sb) 188 | 189 | mut card := ui.SettingsCard.new( 190 | text: 'Workspace Directory' 191 | description: 'The directory that is opened in the file tree' 192 | stretch: true 193 | ) 194 | card.add_child(p) 195 | return card 196 | } 197 | 198 | fn (mut app App) s_vexe() &ui.SettingsCard { 199 | mut p := ui.Panel.new(layout: ui.BoxLayout.new(ori: 0, vgap: 0)) 200 | 201 | mut tf := ui.TextField.new(text: app.confg.vexe) 202 | mut sb := ui.Button.new(text: 'Save') 203 | sb.subscribe_event('mouse_up', fn [mut app, tf] (mut e ui.MouseEvent) { 204 | app.confg.vexe = tf.text 205 | app.confg.save() 206 | }) 207 | 208 | mut card := ui.SettingsCard.new( 209 | text: 'V Executable Path' 210 | description: 'Path to the V executable' 211 | stretch: true 212 | ) 213 | card.add_child(p) 214 | 215 | mut teb := ui.Button.new(text: 'Test') 216 | teb.subscribe_event('mouse_up', fn [mut app, tf, mut card] (mut e ui.MouseEvent) { 217 | app.confg.vexe = tf.text 218 | app.confg.save() 219 | res := os.execute('${app.confg.vexe} version') 220 | app.win.tooltip = res.output 221 | card.desc = card.desc.split('(')[0] + ' (test: ${res.output})' 222 | }) 223 | 224 | tf.subscribe_event('draw', set_field_width) 225 | sb.set_bounds(0, 0, 95, 30) 226 | teb.set_bounds(0, 0, 65, 30) 227 | 228 | p.add_child(tf) 229 | p.add_child(sb) 230 | p.add_child(teb) 231 | 232 | return card 233 | } 234 | 235 | fn (mut app App) s_font_path() &ui.SettingsCard { 236 | mut p := ui.Panel.new(layout: ui.BoxLayout.new(ori: 0, vgap: 0)) 237 | 238 | mut tf := ui.TextField.new(text: app.confg.font_path) 239 | mut sb := ui.Button.new(text: 'Save') 240 | sb.subscribe_event('mouse_up', fn [mut app, tf] (mut e ui.MouseEvent) { 241 | app.confg.font_path = tf.text 242 | app.confg.save() 243 | }) 244 | tf.subscribe_event('draw', set_field_width) 245 | sb.set_bounds(0, 0, 100, 30) 246 | 247 | p.add_child(tf) 248 | p.add_child(sb) 249 | 250 | mut card := ui.SettingsCard.new( 251 | text: 'Font Path' 252 | description: 'Path to the font file used' 253 | stretch: true 254 | ) 255 | card.add_child(p) 256 | return card 257 | } 258 | 259 | fn (mut app App) s_font_size() &ui.SettingsCard { 260 | mut p := ui.Panel.new(layout: ui.BoxLayout.new(ori: 0, vgap: 0)) 261 | 262 | mut tf := ui.numeric_field(app.confg.font_size) 263 | tf.set_bounds(0, 0, 100, 30) 264 | 265 | mut ib := ui.Button.new(text: '+') 266 | ib.subscribe_event('mouse_up', fn [mut app, mut tf] (mut e ui.MouseEvent) { 267 | val := cfs(tf.text.int() + 1) 268 | dump(val) 269 | tf.text = '${val}' 270 | app.set_fs(val) 271 | }) 272 | 273 | mut db := ui.Button.new(text: '-') 274 | db.subscribe_event('mouse_up', fn [mut app, mut tf] (mut e ui.MouseEvent) { 275 | val := cfs(tf.text.int() - 1) 276 | dump(val) 277 | tf.text = '${val}' 278 | app.set_fs(val) 279 | }) 280 | 281 | ib.set_bounds(0, 0, 50, 30) 282 | db.set_bounds(0, 0, 50, 30) 283 | 284 | p.add_child(tf) 285 | p.add_child(ib) 286 | p.add_child(db) 287 | 288 | mut card := ui.SettingsCard.new( 289 | text: 'Font Size' 290 | description: 'The font size used' 291 | stretch: true 292 | ) 293 | card.add_child(p) 294 | return card 295 | } 296 | 297 | fn (mut app App) set_fs(fs int) { 298 | app.win.font_size = fs 299 | app.confg.font_size = fs 300 | app.confg.save() 301 | } 302 | 303 | fn cfs(fs int) int { 304 | if fs < 8 { 305 | return 8 306 | } 307 | if fs > 32 { 308 | return 32 309 | } 310 | return fs 311 | } 312 | -------------------------------------------------------------------------------- /src/main.v: -------------------------------------------------------------------------------- 1 | // VIDE - A simple IDE for V 2 | // (c) 2021-2024 Isaiah. 3 | module main 4 | 5 | import iui as ui 6 | import os 7 | 8 | const version = '0.1-pre' 9 | 10 | @[heap] 11 | pub struct App { 12 | mut: 13 | win &ui.Window 14 | tb &ui.Tabbox 15 | collapse_tree bool 16 | collapse_search bool = true 17 | shown_activity int 18 | activty_speed int = 30 19 | confg &Config 20 | popup &MyPopup 21 | } 22 | 23 | pub fn C.emscripten_run_script(&char) 24 | 25 | fn main() { 26 | vide_home := os.join_path(os.home_dir(), '.vide') 27 | mut folder := os.join_path(vide_home, 'workspace') 28 | 29 | os.mkdir_all(folder) or {} 30 | 31 | confg := make_config() 32 | 33 | mut win := ui.Window.new( 34 | width: 900 35 | height: 550 36 | title: 'Vide' 37 | font_size: confg.font_size 38 | font_path: confg.font_path 39 | ui_mode: true 40 | ) 41 | 42 | $if windows { 43 | ui.set_power_save(true) 44 | } 45 | 46 | if os.exists(confg.workspace_dir) { 47 | folder = confg.workspace_dir 48 | } 49 | 50 | win.set_theme(vide_dark_theme()) 51 | 52 | mut app := &App{ 53 | win: win 54 | tb: ui.Tabbox.new() 55 | confg: confg 56 | popup: code_popup() 57 | } 58 | 59 | win.id_map['app'] = app 60 | 61 | app.make_menubar() 62 | 63 | mut hbox := ui.Panel.new( 64 | layout: ui.BoxLayout.new( 65 | hgap: 0 66 | vgap: 0 67 | ) 68 | ) 69 | 70 | tree := app.setup_tree(mut win, folder) 71 | 72 | activity_bar := app.make_activity_bar() 73 | hbox.add_child(activity_bar) 74 | 75 | hbox.add_child(tree) 76 | 77 | // Search box 78 | search := app.setup_search(mut win, folder) 79 | hbox.add_child(search) 80 | 81 | // end; 82 | app.tb.set_id(mut win, 'main-tabs') 83 | app.tb.set_bounds(0, 0, 400, 200) 84 | app.welcome_tab('') 85 | 86 | for name in app.confg.open_paths { 87 | if os.exists(name) { 88 | new_tab(win, name) 89 | } 90 | } 91 | 92 | mut console_box := create_box(mut win) 93 | console_box.z_index = 2 94 | console_box.set_id(mut win, 'consolebox') 95 | 96 | mut sv := ui.ScrollView.new( 97 | view: console_box 98 | increment: 5 99 | bounds: ui.Bounds{ 100 | width: 300 101 | height: 100 102 | } 103 | padding: 0 104 | ) 105 | sv.noborder = true 106 | sv.set_id(mut win, 'vermsv') 107 | 108 | mut spv := ui.SplitView.new( 109 | first: app.tb 110 | second: sv 111 | min_percent: 20 112 | h1: 70 113 | h2: 20 114 | bounds: ui.Bounds{ 115 | y: 3 116 | x: 2 117 | width: 400 118 | height: 400 119 | } 120 | ) 121 | 122 | app.tb.subscribe_event('draw', tabbox_fill_width) 123 | sv.subscribe_event('draw', terminal_scrollview_fill) 124 | spv.subscribe_event('draw', splitview_fill) 125 | 126 | hbox.add_child(spv) 127 | 128 | win.add_child(hbox) 129 | win.gg.run() 130 | } 131 | 132 | fn (mut app App) make_activity_bar() &ui.NavPane { 133 | mut np := ui.NavPane.new( 134 | pack: true 135 | collapsed: true 136 | ) 137 | 138 | mut item1 := ui.NavPaneItem.new( 139 | icon: '\uED43' 140 | text: 'Workspace' 141 | ) 142 | 143 | mut item2 := ui.NavPaneItem.new( 144 | icon: '\uF002' 145 | text: 'Search' 146 | ) 147 | 148 | mut item3 := ui.NavPaneItem.new( 149 | icon: '\uEAE7' 150 | text: 'Git' 151 | ) 152 | 153 | mut item4 := ui.NavPaneItem.new( 154 | icon: '\uE713' 155 | text: 'Settings' 156 | ) 157 | 158 | item1.subscribe_event('mouse_up', app.calb_click) 159 | item2.subscribe_event('mouse_up', app.serb_click) 160 | item3.subscribe_event('mouse_up', app.calb_click) 161 | item4.subscribe_event('mouse_up', app.settings_btn_click) 162 | 163 | np.add_child(item1) 164 | np.add_child(item2) 165 | np.add_child(item3) 166 | np.add_child(item4) 167 | 168 | return np 169 | } 170 | 171 | fn (mut app App) settings_btn_click(e &ui.MouseEvent) { 172 | mut tar := e.target 173 | if mut tar is ui.NavPaneItem { 174 | tar.unselect() 175 | } 176 | 177 | app.show_settings() 178 | } 179 | 180 | @[deprecated: 'Replaced by ui.NavPane'] 181 | fn (mut app App) make_activity_bar_old() &ui.Panel { 182 | mut activity_bar := ui.Panel.new( 183 | layout: ui.BoxLayout.new( 184 | ori: 1 185 | hgap: 4 186 | ) 187 | ) 188 | activity_bar.set_bounds(0, 0, 40, 200) 189 | 190 | activity_bar.subscribe_event('draw', fn (mut e ui.DrawEvent) { 191 | hei := e.ctx.gg.window_size().height 192 | e.ctx.theme.menu_bar_fill_fn(e.target.x, e.target.y, e.target.width, hei, e.ctx) 193 | }) 194 | 195 | // Explore Button 196 | img_wide_file := $embed_file('assets/explore.png') 197 | mut calb := app.icon_btn(img_wide_file.to_bytes(), app.win) 198 | 199 | activity_bar.add_child(calb) 200 | 201 | calb.subscribe_event('mouse_up', app.calb_click) 202 | 203 | // Search Button 204 | img_search_file := $embed_file('assets/search.png') 205 | mut serb := app.icon_btn(img_search_file.to_bytes(), app.win) 206 | 207 | activity_bar.add_child(serb) 208 | 209 | serb.subscribe_event('mouse_up', app.serb_click) 210 | 211 | // Git Commit satus Button 212 | img_gitm_file := $embed_file('assets/merge.png') 213 | mut gitb := app.icon_btn(img_gitm_file.to_bytes(), app.win) 214 | 215 | activity_bar.add_child(gitb) 216 | 217 | gitb.subscribe_event('mouse_up', app.calb_click) 218 | 219 | return activity_bar 220 | } 221 | 222 | fn (mut app App) icon_btn(data []u8, win &ui.Window) &ui.Button { 223 | mut ggc := win.gg 224 | gg_im := ggc.create_image_from_byte_array(data) or { return ui.Button.new(text: 'NO IMG') } 225 | cim := ggc.cache_image(gg_im) 226 | mut btn := ui.Button.new(icon: cim) 227 | 228 | btn.set_bounds(0, 5, 33, 46) 229 | btn.z_index = 5 230 | 231 | // btn.border_radius = -1 232 | btn.set_area_filled(false) 233 | btn.icon_height = 32 234 | return btn 235 | } 236 | 237 | fn (mut app App) setup_tree(mut window ui.Window, folder string) &ui.ScrollView { 238 | mut tree2 := ui.tree('My Workspace') 239 | tree2.set_bounds(0, 0, 250, 200) 240 | tree2.needs_pack = true 241 | 242 | files := os.ls(folder) or { [] } 243 | tree2.click_event_fn = tree2_click 244 | 245 | for fi in files { 246 | mut node := make_tree2(os.join_path(folder, fi)) 247 | tree2.add_child(node) 248 | } 249 | 250 | mut sv := ui.ScrollView.new( 251 | view: tree2 252 | bounds: ui.Bounds{1, 3, 250, 200} 253 | padding: 0 254 | ) 255 | sv.subscribe_event('draw', app.proj_tree_draw) 256 | tree2.subscribe_event('draw', fn (mut e ui.DrawEvent) { 257 | e.target.width = e.target.parent.width 258 | }) 259 | 260 | tree2.set_id(mut window, 'proj-tree') 261 | return sv // tree2 262 | } 263 | 264 | fn (mut app App) setup_search(mut window ui.Window, folder string) &ui.ScrollView { 265 | mut search_box := ui.Panel.new( 266 | layout: ui.BoxLayout.new( 267 | ori: 1 268 | ) 269 | ) 270 | 271 | search_box.subscribe_event('draw', fn (mut e ui.DrawEvent) { 272 | // e.ctx.gg.draw_rect_empty(e.target.x, e.target.y, e.target.width, e.target.height, 273 | // gx.blue) 274 | e.target.width = 200 275 | e.target.height = 100 + e.target.children[1].height 276 | }) 277 | 278 | mut search_field := ui.text_field( 279 | text: 'Search ...' 280 | bounds: ui.Bounds{1, 1, 190, 25} 281 | ) 282 | search_box.add_child(search_field) 283 | 284 | mut search_out := ui.Panel.new(layout: ui.BoxLayout.new(ori: 1)) 285 | search_box.add_child(search_out) 286 | search_out.set_bounds(0, 0, 200, 0) 287 | 288 | search_field.subscribe_event('before_text_change', fn [mut app, mut search_field, mut search_out] (mut e ui.TextChangeEvent) { 289 | if search_field.last_letter != 'enter' { 290 | return 291 | } 292 | search_out.children.clear() 293 | 294 | txt := e.target.text 295 | dir := app.confg.workspace_dir 296 | read_files(dir, txt, mut search_out, e.ctx) 297 | 298 | dump(e.target.text) 299 | }) 300 | 301 | mut stb := ui.Titlebox.new(text: 'Search', children: [search_box]) 302 | stb.set_bounds(4, 4, 200, 250) 303 | 304 | // hbox.add_child(stb) 305 | mut sv := ui.ScrollView.new( 306 | view: stb 307 | bounds: ui.Bounds{1, 4, 240, 200} 308 | padding: 0 309 | ) 310 | sv.subscribe_event('draw', app.search_pane_draw) 311 | stb.subscribe_event('draw', fn (mut e ui.DrawEvent) { 312 | e.target.width = e.target.parent.width - 7 313 | }) 314 | 315 | stb.set_id(mut window, 'stb') 316 | return sv // tree2 317 | } 318 | 319 | fn read_files(dir string, txt string, mut search_out ui.Panel, ctx &ui.GraphicsContext) { 320 | ls := os.ls(dir) or { [] } 321 | 322 | for file in ls { 323 | jp := os.join_path(dir, file) 324 | if os.is_dir(jp) { 325 | read_files(jp, txt, mut search_out, ctx) 326 | continue 327 | } 328 | 329 | if !(file.ends_with('.v') || file.ends_with('.md') || file.ends_with('.c')) { 330 | continue 331 | } 332 | 333 | lines := os.read_lines(jp) or { [] } 334 | for i, line in lines { 335 | if line.contains(txt) { 336 | mut btn := ui.Label.new(text: '${file}: ${i + 1}:\n${line.trim_space()}') 337 | btn.pack_do(ctx) 338 | search_out.add_child(btn) 339 | } 340 | } 341 | } 342 | if search_out.children.len > 0 { 343 | dump(search_out.children.len) 344 | search_out.set_bounds(0, 0, 200, search_out.children.len * (search_out.children[0].height + 345 | 5)) 346 | } 347 | } 348 | 349 | fn get_v_exe() string { 350 | mut saved := '' // config.get_value('v_exe').replace('\{user_home}', '~') 351 | dump(saved) 352 | saved = saved.replace('~', os.home_dir().replace('\\', '/')) 353 | 354 | if saved.len <= 0 { 355 | mut vexe := 'v' 356 | $if windows { 357 | vexe = 'v.exe' 358 | } 359 | if 'VEXE' in os.environ() { 360 | vexe = os.environ()['VEXE'].replace('\\', '/') 361 | } 362 | vexe = vexe.replace(os.home_dir().replace('\\', '/'), '~') 363 | 364 | // config.set('v_exe', vexe) 365 | // config.save() 366 | return vexe 367 | } else { 368 | return saved 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /src/vcreate.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved. 2 | // Use of this source code is governed by an MIT license that can be found in the LICENSE file. 3 | module main 4 | 5 | import os 6 | 7 | // Note: this program follows a similar convention to Rust: `init` makes the 8 | // structure of the program in the _current_ directory, while `new` 9 | // makes the program structure in a _sub_ directory. Besides that, the 10 | // functionality is essentially the same. 11 | 12 | // Note: here are the currently supported invokations so far: 13 | // 1) `v init` -> create a new project in the current folder 14 | // 2) `v new abc` -> create a new project in the new folder `abc`, by default a "hello world" project. 15 | // 3) `v new abcd web` -> create a new project in the new folder `abcd`, using the vweb template. 16 | // 4) `v new abcde hello_world` -> create a new project in the new folder `abcde`, using the hello_world template. 17 | 18 | // Note: run `v cmd/tools/vcreate_test.v` after changes to this program, to avoid regressions. 19 | struct Create { 20 | mut: 21 | name string 22 | description string 23 | version string 24 | license string 25 | files []ProjectFiles 26 | app &App 27 | } 28 | 29 | struct ProjectFiles { 30 | path string 31 | content string 32 | } 33 | 34 | @[params] 35 | struct CreateConfig { 36 | name string 37 | description string 38 | version string 39 | license string 40 | template string 41 | app &App 42 | } 43 | 44 | // fn new_project(args []string) { 45 | fn new_project(cfg CreateConfig) { 46 | mut c := Create{ 47 | app: cfg.app 48 | } 49 | 50 | // project name 51 | c.name = check_name(cfg.name) 52 | 53 | if c.name == '' { 54 | cerror('project name cannot be empty') 55 | exit(1) 56 | } 57 | 58 | if c.name.contains('-') { 59 | cerror('"${c.name}" should not contain hyphens') 60 | exit(1) 61 | } 62 | 63 | if os.is_dir(c.name) { 64 | cerror('${c.name} folder already exists') 65 | exit(3) 66 | } 67 | 68 | c.description = cfg.description 69 | 70 | default_version := '0.0.0' 71 | 72 | c.version = cfg.version 73 | if c.version == '' { 74 | c.version = default_version 75 | } 76 | 77 | default_license := os.getenv_opt('VLICENSE') or { 'MIT' } 78 | 79 | c.license = cfg.license 80 | if c.license == '' { 81 | c.license = default_license 82 | } 83 | 84 | println('Initialising ...') 85 | 86 | // `v new abcde hello_world` 87 | templ := cfg.template 88 | 89 | // if args.len == 2 { 90 | if templ.len > 0 { 91 | // match os.args.last() { 92 | match templ { 93 | 'web' { 94 | c.set_web_project_files() 95 | } 96 | 'hello_world' { 97 | c.set_hello_world_project_files() 98 | } 99 | 'basic_window' { 100 | c.set_basic_window_files() 101 | } 102 | 'border_layout' { 103 | c.set_border_layout_files() 104 | } 105 | else { 106 | eprintln('${templ} model not exist') 107 | exit(1) 108 | } 109 | } 110 | } else { 111 | // `v new abc` 112 | c.set_hello_world_project_files() 113 | } 114 | 115 | // gen project based in the `Create.files` info 116 | c.create_files_and_directories() 117 | 118 | c.write_vmod(true) 119 | c.write_gitattributes(true) 120 | c.write_editorconfig(true) 121 | 122 | mut gdir := os.join_path(c.app.confg.workspace_dir, c.name) 123 | 124 | c.create_git_repo(gdir) 125 | } 126 | 127 | @[deprecated: 'Not used in Vide'] 128 | fn init_project() { 129 | } 130 | 131 | fn cerror(e string) { 132 | eprintln('\nerror: ${e}') 133 | } 134 | 135 | fn check_name(name string) string { 136 | if name.trim_space().len == 0 { 137 | cerror('project name cannot be empty') 138 | exit(1) 139 | } 140 | if name.is_title() { 141 | mut cname := name.to_lower() 142 | if cname.contains(' ') { 143 | cname = cname.replace(' ', '_') 144 | } 145 | eprintln('warning: the project name cannot be capitalized, the name will be changed to `${cname}`') 146 | return cname 147 | } 148 | if name.contains(' ') { 149 | cname := name.replace(' ', '_') 150 | eprintln('warning: the project name cannot contain spaces, the name will be changed to `${cname}`') 151 | return cname 152 | } 153 | return name 154 | } 155 | 156 | fn vmod_content(c Create) string { 157 | return "Module { 158 | name: '${c.name}' 159 | description: '${c.description}' 160 | version: '${c.version}' 161 | license: '${c.license}' 162 | dependencies: [] 163 | } 164 | " 165 | } 166 | 167 | fn hello_world_content() string { 168 | return "module main 169 | 170 | fn main() { 171 | println('Hello World!') 172 | } 173 | " 174 | } 175 | 176 | fn basic_window_content() string { 177 | return "module main 178 | 179 | import iui as ui // // v install https://github.com/pisaiah/ui 180 | 181 | fn main() { 182 | // Create Window 183 | mut window := ui.Window.new( 184 | title: 'My Window' 185 | width: 520 186 | height: 400 187 | theme: ui.theme_default() 188 | ) 189 | 190 | // A content pane 191 | mut p := ui.Panel.new() 192 | 193 | window.add_child(p) 194 | 195 | // Run window 196 | window.run() 197 | } 198 | " 199 | } 200 | 201 | fn border_layout_content() string { 202 | return "module main 203 | 204 | import iui as ui // v install https://github.com/pisaiah/ui 205 | 206 | struct App { 207 | mut: 208 | p &ui.Panel 209 | } 210 | 211 | fn main() { 212 | mut win := ui.Window.new( 213 | title: 'BorderLayoutDemo' 214 | width: 450 215 | height: 295 216 | ) 217 | 218 | mut pan := ui.Panel.new(layout: ui.BorderLayout.new()) 219 | 220 | mut app := &App{ 221 | p: pan 222 | } 223 | 224 | app.make_button('1 (NORTH)', ui.borderlayout_north) 225 | app.make_button('2 (WEST)', ui.borderlayout_west) 226 | app.make_button('3 (EAST)', ui.borderlayout_east) 227 | app.make_button('4 (SOUTH)', ui.borderlayout_south) 228 | app.make_button('5 (CENTER)', ui.borderlayout_center) 229 | 230 | win.add_child(pan) 231 | win.gg.run() 232 | } 233 | 234 | fn (mut app App) make_button(id string, constrain int) &ui.Button { 235 | mut btn := ui.Button.new( 236 | text: 'Button ' + id 237 | ) 238 | app.p.add_child_with_flag(btn, constrain) 239 | return btn 240 | } 241 | " 242 | } 243 | 244 | fn gen_gitignore(name string) string { 245 | return '# Binaries for programs and plugins 246 | main 247 | ${name} 248 | *.exe 249 | *.exe~ 250 | *.so 251 | *.dylib 252 | *.dll 253 | 254 | # Ignore binary output folders 255 | bin/ 256 | 257 | # Ignore common editor/system specific metadata 258 | .DS_Store 259 | .idea/ 260 | .vscode/ 261 | *.iml 262 | 263 | # ENV 264 | .env 265 | 266 | # vweb and database 267 | *.db 268 | *.js 269 | ' 270 | } 271 | 272 | fn gitattributes_content() string { 273 | return '* text=auto eol=lf 274 | *.bat eol=crlf 275 | 276 | **/*.v linguist-language=V 277 | **/*.vv linguist-language=V 278 | **/*.vsh linguist-language=V 279 | **/v.mod linguist-language=V 280 | ' 281 | } 282 | 283 | fn editorconfig_content() string { 284 | return '[*] 285 | charset = utf-8 286 | end_of_line = lf 287 | insert_final_newline = true 288 | trim_trailing_whitespace = true 289 | 290 | [*.v] 291 | indent_style = tab 292 | indent_size = 4 293 | ' 294 | } 295 | 296 | fn (c &Create) write_vmod(new bool) { 297 | mut vmod_path := if new { '${c.name}/v.mod' } else { 'v.mod' } 298 | vmod_path = os.join_path(c.app.confg.workspace_dir, vmod_path) 299 | write_file(vmod_path, vmod_content(c)) or { panic(err) } 300 | } 301 | 302 | fn (c &Create) write_gitattributes(new bool) { 303 | mut gitattributes_path := if new { '${c.name}/.gitattributes' } else { '.gitattributes' } 304 | gitattributes_path = os.join_path(c.app.confg.workspace_dir, gitattributes_path) 305 | if !new && os.exists(gitattributes_path) { 306 | return 307 | } 308 | write_file(gitattributes_path, gitattributes_content()) or { panic(err) } 309 | } 310 | 311 | fn (c &Create) write_editorconfig(new bool) { 312 | mut editorconfig_path := if new { '${c.name}/.editorconfig' } else { '.editorconfig' } 313 | editorconfig_path = os.join_path(c.app.confg.workspace_dir, editorconfig_path) 314 | if !new && os.exists(editorconfig_path) { 315 | return 316 | } 317 | write_file(editorconfig_path, editorconfig_content()) or { panic(err) } 318 | } 319 | 320 | fn (c &Create) create_git_repo(dir string) { 321 | // Create Git Repo and .gitignore file 322 | if !os.is_dir('${dir}/.git') { 323 | res := os.execute('git init ${dir}') 324 | if res.exit_code != 0 { 325 | $if emscripten ? { 326 | println('Unable to run "git init"') 327 | } $else { 328 | cerror('Unable to create git repo') 329 | exit(4) 330 | } 331 | } 332 | } 333 | gitignore_path := '${dir}/.gitignore' 334 | if !os.exists(gitignore_path) { 335 | write_file(gitignore_path, gen_gitignore(c.name)) or {} 336 | } 337 | } 338 | 339 | fn (mut c Create) create_files_and_directories() { 340 | for file in c.files { 341 | // get dir and convert path separator 342 | mut dir := file.path.split('/')#[..-1].join(os.path_separator) 343 | 344 | dir = os.join_path(c.app.confg.workspace_dir, dir) 345 | 346 | // create all directories, if not exist 347 | os.mkdir_all(dir) or { panic(err) } 348 | 349 | fp := os.join_path(c.app.confg.workspace_dir, file.path) 350 | 351 | write_file(fp, file.content) or { panic(err) } 352 | } 353 | } 354 | 355 | // ####################################### PROJECTS CONTENT AND PATH ####################################### 356 | fn (mut c Create) set_hello_world_project_files() { 357 | c.files << ProjectFiles{ 358 | path: '${c.name}/src/main.v' 359 | content: hello_world_content() 360 | } 361 | } 362 | 363 | fn (mut c Create) set_basic_window_files() { 364 | c.files << ProjectFiles{ 365 | path: '${c.name}/src/main.v' 366 | content: basic_window_content() 367 | } 368 | } 369 | 370 | fn (mut c Create) set_border_layout_files() { 371 | c.files << ProjectFiles{ 372 | path: '${c.name}/src/main.v' 373 | content: border_layout_content() 374 | } 375 | } 376 | 377 | fn (mut c Create) set_web_project_files() { 378 | c.files << ProjectFiles{ 379 | path: '${c.name}/src/databases/config_databases_sqlite.v' 380 | content: "module databases 381 | 382 | import db.sqlite // can change to 'db.mysql', 'db.pg' 383 | 384 | pub fn create_db_connection() !sqlite.DB { 385 | mut db := sqlite.connect('app.db')! 386 | return db 387 | } 388 | " 389 | } 390 | c.files << ProjectFiles{ 391 | path: '${c.name}/src/templates/header_component.html' 392 | content: " 407 | " 408 | } 409 | c.files << ProjectFiles{ 410 | path: '${c.name}/src/templates/products.css' 411 | content: 'h1.title { 412 | font-family: Arial, Helvetica, sans-serif; 413 | color: #3b7bbf; 414 | } 415 | 416 | div.products-table { 417 | border: 1px solid; 418 | max-width: 720px; 419 | padding: 10px; 420 | margin: 10px; 421 | }' 422 | } 423 | c.files << ProjectFiles{ 424 | path: '${c.name}/src/templates/products.html' 425 | content: " 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | Login 440 | @css 'src/templates/products.css' 441 | 442 | 443 |
@include 'header_component.html'
444 |

Hi, \${user.username}! you are online

445 | 446 |
447 |
448 |
449 | 450 | 451 |
452 |
453 | 454 |
455 |
456 | 459 |
460 | 495 |
496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | @for product in user.products 507 | 508 | 509 | 510 | 511 | 512 | @end 513 | 514 |
IDNameCreated date
\${product.id}\${product.name}\${product.created_at}
515 |
516 | 517 | " 518 | } 519 | c.files << ProjectFiles{ 520 | path: '${c.name}/src/auth_controllers.v' 521 | content: "module main 522 | 523 | import vweb 524 | 525 | ['/controller/auth'; post] 526 | pub fn (mut app App) controller_auth(username string, password string) vweb.Result { 527 | response := app.service_auth(username, password) or { 528 | app.set_status(400, '') 529 | return app.text('error: \${err}') 530 | } 531 | 532 | return app.json(response) 533 | } 534 | " 535 | } 536 | c.files << ProjectFiles{ 537 | path: '${c.name}/src/auth_dto.v' 538 | content: 'module main 539 | 540 | struct AuthRequestDto { 541 | username string [required] 542 | password string [required] 543 | } 544 | ' 545 | } 546 | c.files << ProjectFiles{ 547 | path: '${c.name}/src/auth_services.v' 548 | content: "module main 549 | 550 | import crypto.hmac 551 | import crypto.sha256 552 | import crypto.bcrypt 553 | import encoding.base64 554 | import json 555 | import databases 556 | import time 557 | 558 | struct JwtHeader { 559 | alg string 560 | typ string 561 | } 562 | 563 | struct JwtPayload { 564 | sub string // (subject) = Entity to whom the token belongs, usually the user ID; 565 | iss string // (issuer) = Token issuer; 566 | exp string // (expiration) = Timestamp of when the token will expire; 567 | iat time.Time // (issued at) = Timestamp of when the token was created; 568 | aud string // (audience) = Token recipient, represents the application that will use it. 569 | name string 570 | roles string 571 | permissions string 572 | } 573 | 574 | fn (mut app App) service_auth(username string, password string) !string { 575 | mut db := databases.create_db_connection() or { 576 | eprintln(err) 577 | panic(err) 578 | } 579 | 580 | defer { 581 | db.close() or { panic(err) } 582 | } 583 | 584 | user := sql db { 585 | select from User where username == username limit 1 586 | } 587 | if user.username != username { 588 | return error('user not found') 589 | } 590 | 591 | if !user.active { 592 | return error('user is not active') 593 | } 594 | 595 | bcrypt.compare_hash_and_password(password.bytes(), user.password.bytes()) or { 596 | return error('Failed to auth user, \${err}') 597 | } 598 | 599 | token := make_token(user) 600 | return token 601 | } 602 | 603 | fn make_token(user User) string { 604 | secret := 'SECRET_KEY' // os.getenv('SECRET_KEY') 605 | 606 | jwt_header := JwtHeader{'HS256', 'JWT'} 607 | jwt_payload := JwtPayload{ 608 | sub: '\${user.id}' 609 | name: '\${user.username}' 610 | iat: time.now() 611 | } 612 | 613 | header := base64.url_encode(json.encode(jwt_header).bytes()) 614 | payload := base64.url_encode(json.encode(jwt_payload).bytes()) 615 | signature := base64.url_encode(hmac.new(secret.bytes(), '\${header}.\${payload}'.bytes(), 616 | sha256.sum, sha256.block_size).bytestr().bytes()) 617 | 618 | jwt := '\${header}.\${payload}.\${signature}' 619 | 620 | return jwt 621 | } 622 | 623 | fn auth_verify(token string) bool { 624 | if token == '' { 625 | return false 626 | } 627 | secret := 'SECRET_KEY' // os.getenv('SECRET_KEY') 628 | token_split := token.split('.') 629 | 630 | signature_mirror := hmac.new(secret.bytes(), '\${token_split[0]}.\${token_split[1]}'.bytes(), 631 | sha256.sum, sha256.block_size).bytestr().bytes() 632 | 633 | signature_from_token := base64.url_decode(token_split[2]) 634 | 635 | return hmac.equal(signature_from_token, signature_mirror) 636 | // return true 637 | } 638 | " 639 | } 640 | c.files << ProjectFiles{ 641 | path: '${c.name}/src/index.html' 642 | content: " 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | \${title} 653 | 654 | 655 |
@include 'templates/header_component.html'
656 |
657 |
658 |
659 | 660 | 661 |
662 |
663 | 664 | 665 |
666 |
667 | 710 |
711 | 712 | " 713 | } 714 | c.files << ProjectFiles{ 715 | path: '${c.name}/src/main.v' 716 | content: "module main 717 | 718 | import vweb 719 | import databases 720 | import os 721 | 722 | const ( 723 | port = 8082 724 | ) 725 | 726 | struct App { 727 | vweb.Context 728 | } 729 | 730 | pub fn (app App) before_request() { 731 | println('[web] before_request: \${app.req.method} \${app.req.url}') 732 | } 733 | 734 | fn main() { 735 | mut db := databases.create_db_connection() or { panic(err) } 736 | 737 | sql db { 738 | create table User 739 | } or { panic('error on create table: \${err}') } 740 | 741 | db.close() or { panic(err) } 742 | 743 | mut app := &App{} 744 | app.serve_static('/favicon.ico', 'src/assets/favicon.ico') 745 | // makes all static files available. 746 | app.mount_static_folder_at(os.resource_abs_path('.'), '/') 747 | 748 | vweb.run(app, port) 749 | } 750 | 751 | pub fn (mut app App) index() vweb.Result { 752 | title := 'vweb app' 753 | 754 | return \$vweb.html() 755 | } 756 | " 757 | } 758 | c.files << ProjectFiles{ 759 | path: '${c.name}/src/product_controller.v' 760 | content: "module main 761 | 762 | import vweb 763 | import encoding.base64 764 | import json 765 | 766 | ['/controller/products'; get] 767 | pub fn (mut app App) controller_get_all_products() vweb.Result { 768 | token := app.req.header.get_custom('token') or { '' } 769 | 770 | if !auth_verify(token) { 771 | app.set_status(401, '') 772 | return app.text('Not valid token') 773 | } 774 | 775 | jwt_payload_stringify := base64.url_decode_str(token.split('.')[1]) 776 | 777 | jwt_payload := json.decode(JwtPayload, jwt_payload_stringify) or { 778 | app.set_status(501, '') 779 | return app.text('jwt decode error') 780 | } 781 | 782 | user_id := jwt_payload.sub 783 | 784 | response := app.service_get_all_products_from(user_id.int()) or { 785 | app.set_status(400, '') 786 | return app.text('\${err}') 787 | } 788 | return app.json(response) 789 | // return app.text('response') 790 | } 791 | 792 | ['/controller/product/create'; post] 793 | pub fn (mut app App) controller_create_product(product_name string) vweb.Result { 794 | if product_name == '' { 795 | app.set_status(400, '') 796 | return app.text('product name cannot be empty') 797 | } 798 | 799 | token := app.req.header.get_custom('token') or { '' } 800 | 801 | if !auth_verify(token) { 802 | app.set_status(401, '') 803 | return app.text('Not valid token') 804 | } 805 | 806 | jwt_payload_stringify := base64.url_decode_str(token.split('.')[1]) 807 | 808 | jwt_payload := json.decode(JwtPayload, jwt_payload_stringify) or { 809 | app.set_status(501, '') 810 | return app.text('jwt decode error') 811 | } 812 | 813 | user_id := jwt_payload.sub 814 | 815 | app.service_add_product(product_name, user_id.int()) or { 816 | app.set_status(400, '') 817 | return app.text('error: \${err}') 818 | } 819 | app.set_status(201, '') 820 | return app.text('product created successfully') 821 | } 822 | " 823 | } 824 | c.files << ProjectFiles{ 825 | path: '${c.name}/src/product_entities.v' 826 | content: "module main 827 | 828 | [table: 'products'] 829 | struct Product { 830 | id int [primary; sql: serial] 831 | user_id int 832 | name string [required; sql_type: 'TEXT'] 833 | created_at string [default: 'CURRENT_TIMESTAMP'] 834 | } 835 | " 836 | } 837 | c.files << ProjectFiles{ 838 | path: '${c.name}/src/product_service.v' 839 | content: "module main 840 | 841 | import databases 842 | 843 | fn (mut app App) service_add_product(product_name string, user_id int) ! { 844 | mut db := databases.create_db_connection()! 845 | 846 | defer { 847 | db.close() or { panic(err) } 848 | } 849 | 850 | product_model := Product{ 851 | name: product_name 852 | user_id: user_id 853 | } 854 | 855 | mut insert_error := '' 856 | 857 | sql db { 858 | insert product_model into Product 859 | } or { insert_error = err.msg() } 860 | 861 | if insert_error != '' { 862 | return error(insert_error) 863 | } 864 | } 865 | 866 | fn (mut app App) service_get_all_products_from(user_id int) ?[]Product { 867 | mut db := databases.create_db_connection() or { 868 | println(err) 869 | return err 870 | } 871 | 872 | defer { 873 | db.close() or { panic(err) } 874 | } 875 | 876 | results := sql db { 877 | select from Product where user_id == user_id 878 | } 879 | 880 | return results 881 | } 882 | " 883 | } 884 | c.files << ProjectFiles{ 885 | path: '${c.name}/src/product_view_api.v' 886 | content: "module main 887 | 888 | import json 889 | import net.http 890 | 891 | pub fn get_products(token string) ![]Product { 892 | mut header := http.new_header() 893 | header.add_custom('token', token)! 894 | url := 'http://localhost:8082/controller/products' 895 | 896 | mut config := http.FetchConfig{ 897 | header: header 898 | } 899 | 900 | resp := http.fetch(http.FetchConfig{ ...config, url: url })! 901 | products := json.decode([]Product, resp.body)! 902 | 903 | return products 904 | } 905 | 906 | pub fn get_product(token string) ![]User { 907 | mut header := http.new_header() 908 | header.add_custom('token', token)! 909 | 910 | url := 'http://localhost:8082/controller/product' 911 | 912 | mut config := http.FetchConfig{ 913 | header: header 914 | } 915 | 916 | resp := http.fetch(http.FetchConfig{ ...config, url: url })! 917 | products := json.decode([]User, resp.body)! 918 | 919 | return products 920 | } 921 | " 922 | } 923 | c.files << ProjectFiles{ 924 | path: '${c.name}/src/product_view.v' 925 | content: "module main 926 | 927 | import vweb 928 | 929 | ['/products'; get] 930 | pub fn (mut app App) products() !vweb.Result { 931 | token := app.get_cookie('token') or { 932 | app.set_status(400, '') 933 | return app.text('\${err}') 934 | } 935 | 936 | user := get_user(token) or { 937 | app.set_status(400, '') 938 | return app.text('Failed to fetch data from the server. Error: \${err}') 939 | } 940 | 941 | return \$vweb.html() 942 | } 943 | " 944 | } 945 | c.files << ProjectFiles{ 946 | path: '${c.name}/src/user_controllers.v' 947 | content: "module main 948 | 949 | import vweb 950 | import encoding.base64 951 | import json 952 | 953 | ['/controller/users'; get] 954 | pub fn (mut app App) controller_get_all_user() vweb.Result { 955 | token := app.req.header.get_custom('token') or { '' } 956 | 957 | if !auth_verify(token) { 958 | app.set_status(401, '') 959 | return app.text('Not valid token') 960 | } 961 | 962 | response := app.service_get_all_user() or { 963 | app.set_status(400, '') 964 | return app.text('\${err}') 965 | } 966 | return app.json(response) 967 | } 968 | 969 | ['/controller/user'; get] 970 | pub fn (mut app App) controller_get_user() vweb.Result { 971 | token := app.req.header.get_custom('token') or { '' } 972 | 973 | if !auth_verify(token) { 974 | app.set_status(401, '') 975 | return app.text('Not valid token') 976 | } 977 | 978 | jwt_payload_stringify := base64.url_decode_str(token.split('.')[1]) 979 | 980 | jwt_payload := json.decode(JwtPayload, jwt_payload_stringify) or { 981 | app.set_status(501, '') 982 | return app.text('jwt decode error') 983 | } 984 | 985 | user_id := jwt_payload.sub 986 | 987 | response := app.service_get_user(user_id.int()) or { 988 | app.set_status(400, '') 989 | return app.text('\${err}') 990 | } 991 | return app.json(response) 992 | } 993 | 994 | ['/controller/user/create'; post] 995 | pub fn (mut app App) controller_create_user(username string, password string) vweb.Result { 996 | if username == '' { 997 | app.set_status(400, '') 998 | return app.text('username cannot be empty') 999 | } 1000 | if password == '' { 1001 | app.set_status(400, '') 1002 | return app.text('password cannot be empty') 1003 | } 1004 | app.service_add_user(username, password) or { 1005 | app.set_status(400, '') 1006 | return app.text('error: \${err}') 1007 | } 1008 | app.set_status(201, '') 1009 | return app.text('User created successfully') 1010 | } 1011 | " 1012 | } 1013 | c.files << ProjectFiles{ 1014 | path: '${c.name}/src/user_entities.v' 1015 | content: "module main 1016 | 1017 | [table: 'users'] 1018 | pub struct User { 1019 | mut: 1020 | id int [primary; sql: serial] 1021 | username string [required; sql_type: 'TEXT'; unique] 1022 | password string [required; sql_type: 'TEXT'] 1023 | active bool 1024 | products []Product [fkey: 'user_id'] 1025 | } 1026 | " 1027 | } 1028 | c.files << ProjectFiles{ 1029 | path: '${c.name}/src/user_services.v' 1030 | content: "module main 1031 | 1032 | import crypto.bcrypt 1033 | import databases 1034 | 1035 | fn (mut app App) service_add_user(username string, password string) ! { 1036 | mut db := databases.create_db_connection()! 1037 | 1038 | defer { 1039 | db.close() or { panic(err) } 1040 | } 1041 | 1042 | hashed_password := bcrypt.generate_from_password(password.bytes(), bcrypt.min_cost) or { 1043 | eprintln(err) 1044 | return err 1045 | } 1046 | 1047 | user_model := User{ 1048 | username: username 1049 | password: hashed_password 1050 | active: true 1051 | } 1052 | 1053 | mut insert_error := '' 1054 | sql db { 1055 | insert user_model into User 1056 | } or { insert_error = err.msg() } 1057 | if insert_error != '' { 1058 | return error(insert_error) 1059 | } 1060 | } 1061 | 1062 | fn (mut app App) service_get_all_user() ?[]User { 1063 | mut db := databases.create_db_connection() or { 1064 | println(err) 1065 | return err 1066 | } 1067 | 1068 | defer { 1069 | db.close() or { panic(err) } 1070 | } 1071 | 1072 | results := sql db { 1073 | select from User 1074 | } 1075 | 1076 | return results 1077 | } 1078 | 1079 | fn (mut app App) service_get_user(id int) ?User { 1080 | mut db := databases.create_db_connection() or { 1081 | println(err) 1082 | return err 1083 | } 1084 | 1085 | defer { 1086 | db.close() or { panic(err) } 1087 | } 1088 | 1089 | results := sql db { 1090 | select from User where id == id 1091 | } 1092 | 1093 | return results 1094 | } 1095 | " 1096 | } 1097 | c.files << ProjectFiles{ 1098 | path: '${c.name}/src/user_view_api.v' 1099 | content: "module main 1100 | 1101 | import json 1102 | import net.http 1103 | 1104 | pub fn get_users(token string) ![]User { 1105 | mut header := http.new_header() 1106 | header.add_custom('token', token)! 1107 | 1108 | url := 'http://localhost:8082/controller/users' 1109 | 1110 | mut config := http.FetchConfig{ 1111 | header: header 1112 | } 1113 | 1114 | resp := http.fetch(http.FetchConfig{ ...config, url: url })! 1115 | users := json.decode([]User, resp.body)! 1116 | 1117 | return users 1118 | } 1119 | 1120 | pub fn get_user(token string) !User { 1121 | mut header := http.new_header() 1122 | header.add_custom('token', token)! 1123 | 1124 | url := 'http://localhost:8082/controller/user' 1125 | 1126 | mut config := http.FetchConfig{ 1127 | header: header 1128 | } 1129 | 1130 | resp := http.fetch(http.FetchConfig{ ...config, url: url })! 1131 | users := json.decode(User, resp.body)! 1132 | 1133 | return users 1134 | } 1135 | " 1136 | } 1137 | } 1138 | --------------------------------------------------------------------------------