├── .gitpod.yml ├── .gitpod ├── Dockerfile ├── novnc-index.html └── start-vnc-session.sh ├── GtkApplication.lua ├── GtkApplicationWindow.lua ├── GtkBox1.lua ├── GtkBuilder-02.ui ├── GtkBuilder.ui ├── GtkBuilder1.lua ├── GtkBuilder2.lua ├── GtkButton1.lua ├── GtkButton2.lua ├── GtkButton3.lua ├── GtkComboBox.lua ├── GtkComboBoxText.lua ├── GtkCssProvider1.lua ├── GtkDialog1.lua ├── GtkDialog2.lua ├── GtkEntry.lua ├── GtkEntryCompletion.lua ├── GtkExpander.lua ├── GtkFileChooserButton.lua ├── GtkFileChooserDialog.lua ├── GtkFixed.lua ├── GtkFlowBox.lua ├── GtkGrid-Explained.png ├── GtkGrid1.lua ├── GtkGrid2.lua ├── GtkGrid3.lua ├── GtkHeaderBar.lua ├── GtkImage1.lua ├── GtkImage2.lua ├── GtkInfoBar.lua ├── GtkLabel1.lua ├── GtkLabel2.lua ├── GtkListBox1.lua ├── GtkMessageDialog.lua ├── GtkMessageDialog2.lua ├── GtkNotebook.lua ├── GtkOverlay.lua ├── GtkPaned.lua ├── GtkRevealer.lua ├── GtkScrolledWindow.lua ├── GtkSpinner.lua ├── GtkStack1.lua ├── GtkStack2.lua ├── GtkStatusbar.lua ├── GtkWindow.lua ├── LICENSE ├── Moonsteal-Logo.jpg ├── README.md ├── action-bar.lua ├── mobdebug.lua └── styles └── GtkCssProvider1.css /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod/Dockerfile 3 | 4 | tasks: 5 | - name: Setup workspace 6 | command: | 7 | sudo apt update -q 8 | sudo apt upgrade -yq 9 | sudo apt install -yq libgtk-3-dev lua5.1 lua-lgi 10 | 11 | vscode: 12 | extensions: 13 | - sumneko.lua -------------------------------------------------------------------------------- /.gitpod/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-base:latest 2 | 3 | # Install Xvfb, JavaFX-helpers, Openbox window manager and emacs GTK 4 | RUN sudo install-packages xvfb x11vnc xterm openjfx libopenjfx-java openbox 5 | 6 | # Overwrite this env variable to use a different window manager 7 | ENV WINDOW_MANAGER="openbox" 8 | 9 | USER root 10 | 11 | # Install novnc 12 | RUN git clone --depth 1 https://github.com/novnc/noVNC.git /opt/novnc \ 13 | && git clone --depth 1 https://github.com/novnc/websockify /opt/novnc/utils/websockify 14 | COPY .gitpod/novnc-index.html /opt/novnc/index.html 15 | 16 | # Add VNC startup script 17 | COPY .gitpod/start-vnc-session.sh /usr/bin/ 18 | RUN chmod +x /usr/bin/start-vnc-session.sh 19 | 20 | # This is a bit of a hack. At the moment we have no means of starting background 21 | # tasks from a Dockerfile. This workaround checks, on each bashrc eval, if the X 22 | # server is running on screen 0, and if not starts Xvfb, x11vnc and novnc. 23 | RUN echo "export DISPLAY=:0" >> ~/.bashrc 24 | RUN echo "[ ! -e /tmp/.X0-lock ] && (/usr/bin/start-vnc-session.sh &> /tmp/display-\${DISPLAY}.log)" >> ~/.bashrc 25 | 26 | ### checks ### 27 | # no root-owned files in the home directory 28 | RUN notOwnedFile=$(find . -not "(" -user gitpod -and -group gitpod ")" -print -quit) \ 29 | && { [ -z "$notOwnedFile" ] \ 30 | || { echo "Error: not all files/dirs in $HOME are owned by 'gitpod' user & group"; exit 1; } } 31 | 32 | USER gitpod -------------------------------------------------------------------------------- /.gitpod/novnc-index.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /.gitpod/start-vnc-session.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # see https://en.wikipedia.org/wiki/Xvfb#Remote_control_over_SSH 4 | 5 | DISP=${DISPLAY:1} 6 | 7 | Xvfb -screen "$DISP" "${CUSTOM_XVFB_WxHxD:=1280x720x16}" -ac -pn -noreset & 8 | 9 | $WINDOW_MANAGER & 10 | 11 | VNC_PORT=$((5900 + "$DISP")) 12 | NOVNC_PORT=$((6080 + "$DISP")) 13 | 14 | x11vnc -localhost -shared -display :"$DISP" -forever -rfbport ${VNC_PORT} -bg -o "/tmp/x11vnc-${DISP}.log" 15 | cd /opt/novnc/utils && ./novnc_proxy --vnc "localhost:${VNC_PORT}" --listen "${NOVNC_PORT}" & -------------------------------------------------------------------------------- /GtkApplication.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | --[[ GtkApplication: 5 | 6 | Provides an application interface. I recommend you to read the 7 | info in the links below. 8 | 9 | ]] 10 | local App = Gtk.Application({ 11 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkApplication" 12 | }) 13 | 14 | -- For more info about these signals, see this: 15 | -- https://wiki.gnome.org/HowDoI/GtkApplication 16 | -- https://developer.gnome.org/gio/stable/GApplication.html 17 | -- https://developer.gnome.org/gtk3/stable/GtkApplication.html 18 | function App:on_startup() 19 | print("Application::startup called") 20 | end 21 | 22 | function App:on_activate() 23 | print("Application::activate called") 24 | end 25 | 26 | return App:run(arg) -------------------------------------------------------------------------------- /GtkApplicationWindow.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkApplicationWindow" 6 | }) 7 | 8 | function App:on_startup() 9 | --[[ GtkApplicationWindow: 10 | 11 | A GtkWindow thats has a better integration with GtkApplication 12 | 13 | ]] 14 | Gtk.ApplicationWindow({ 15 | application = self, 16 | default_width = 400, 17 | default_height = 400 18 | }) 19 | end 20 | 21 | function App:on_activate() 22 | self.active_window:present() 23 | end 24 | 25 | return App:run(arg) -------------------------------------------------------------------------------- /GtkBox1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkBox1" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkBox", 21 | subtitle = "Example 1" 22 | })) 23 | 24 | --[[ GtkBox: 25 | 26 | A container thats organize widgets lineal in one of these directions: 27 | - Vertical 28 | - Horizontal 29 | 30 | ]] 31 | local Box = Gtk.Box({ 32 | visible = true, 33 | orientation = Gtk.Orientation.VERTICAL, 34 | spacing = 10, 35 | valign = Gtk.Align.CENTER 36 | }) 37 | 38 | Box:pack_start( 39 | Gtk.Label({ visible = true, label = "Label 1" }), 40 | false, 41 | true, 42 | 0 43 | ) 44 | 45 | Box:pack_start( 46 | Gtk.Label({ visible = true, label = "Label 2" }), 47 | false, 48 | true, 49 | 0 50 | ) 51 | 52 | self.active_window:add(Box) 53 | self.active_window:present() 54 | end 55 | 56 | return App:run(arg) -------------------------------------------------------------------------------- /GtkBuilder-02.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | False 8 | center 9 | 400 10 | 400 11 | center 12 | 13 | 14 | 15 | True 16 | False 17 | True 18 | True 19 | 20 | 21 | True 22 | True 23 | True 24 | 25 | 26 | True 27 | False 28 | 0 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 39 | 40 | 41 | 42 | True 43 | True 44 | True 45 | 46 | 47 | True 48 | False 49 | 0 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 1 58 | 0 59 | 60 | 61 | 62 | 63 | True 64 | True 65 | True 66 | 67 | 68 | True 69 | False 70 | 0 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 2 79 | 0 80 | 81 | 82 | 83 | 84 | True 85 | True 86 | True 87 | 88 | 89 | True 90 | False 91 | 0 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 0 100 | 1 101 | 102 | 103 | 104 | 105 | True 106 | True 107 | True 108 | 109 | 110 | True 111 | False 112 | 0 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 1 121 | 1 122 | 123 | 124 | 125 | 126 | True 127 | True 128 | True 129 | 130 | 131 | True 132 | False 133 | 0 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 2 142 | 1 143 | 144 | 145 | 146 | 147 | True 148 | True 149 | True 150 | 151 | 152 | True 153 | False 154 | 0 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 0 163 | 2 164 | 165 | 166 | 167 | 168 | True 169 | True 170 | True 171 | 172 | 173 | True 174 | False 175 | 0 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 1 184 | 2 185 | 186 | 187 | 188 | 189 | True 190 | True 191 | True 192 | 193 | 194 | True 195 | False 196 | 0 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 2 205 | 2 206 | 207 | 208 | 209 | 210 | 211 | 212 | True 213 | False 214 | Here is the title 215 | And here is the subtitle 216 | True 217 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /GtkBuilder.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | center 8 | 400 9 | 400 10 | center 11 | 12 | 13 | 14 | 15 | True 16 | True 17 | Here is the title 18 | And here is the subtitle 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | True 27 | Hello, world! 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /GtkBuilder1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | --[[ GtkBuilder: 5 | 6 | A mechanism provided by Gtk that allows build user interfaces 7 | using a XML definition. That XML definition can be in a file 8 | or an string of the programming language that you use. 9 | 10 | This can be very helpful, because you don't need create the 11 | entire UI from code, instead, you can use a XML definition 12 | of your app UI and in the code side, you write the whatever 13 | your app will do. 14 | 15 | ]] 16 | local Builder = Gtk.Builder.new_from_file("GtkBuilder.ui") 17 | 18 | local App = Gtk.Application({ 19 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkBuilder1" 20 | }) 21 | 22 | function App:on_startup() 23 | self:add_window(Builder:get_object("Window")) 24 | end 25 | 26 | function App:on_activate() 27 | self.active_window:present() 28 | end 29 | 30 | return App:run(arg) -------------------------------------------------------------------------------- /GtkBuilder2.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local Builder = Gtk.Builder.new_from_file("GtkBuilder.ui") 5 | 6 | -- Another way to use GtkBuilder in LGI 7 | local UI = Builder.objects 8 | local App = Gtk.Application({ 9 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkBuilder2" 10 | }) 11 | 12 | function App:on_startup() 13 | self:add_window(UI.Window) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:present() 18 | end 19 | 20 | return App:run(arg) -------------------------------------------------------------------------------- /GtkButton1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local Gio = lgi.require("Gio", "2.0") 4 | local GLib = lgi.require("GLib", "2.0") 5 | 6 | local App = Gtk.Application({ 7 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkButton1" 8 | }) 9 | 10 | function App:on_startup() 11 | Gtk.ApplicationWindow({ 12 | application = self, 13 | default_width = 400, 14 | default_height = 400 15 | }) 16 | end 17 | 18 | function App:on_activate() 19 | self.active_window:set_titlebar(Gtk.HeaderBar({ 20 | visible = true, 21 | show_close_button = true, 22 | title = "GtkButton", 23 | subtitle = "Example 1" 24 | })) 25 | 26 | --[[ GtkButton: 27 | 28 | Just a button lol 29 | 30 | ]] 31 | local Button = Gtk.Button({ 32 | visible = true, 33 | label = "Click me!", 34 | valign = Gtk.Align.CENTER, 35 | halign = Gtk.Align.CENTER 36 | }) 37 | 38 | local Notification_ID = 0 39 | 40 | --for k, v in pairs(GLib:_resolve(true)._struct) do print(k, v) end 41 | 42 | -- The "clicked" signal is emited when the user clicks 43 | -- the button 44 | function Button:on_clicked() 45 | -- Notifications are not yet supported on Windows 46 | if GLib.get_os_info("ID") == "windows" then 47 | print(("Clicked %d times!"):format(Notification_ID)) 48 | else 49 | local Notification = Gio.Notification() 50 | Notification:set_title("GtkButton example 1") 51 | Notification:set_body("You cliked the button!") 52 | 53 | -- 'self' can't be used here, because 'self' here 54 | -- reffers to the GtkButton that's clicked 55 | App:send_notification(Notification_ID, Notification) 56 | end 57 | 58 | -- Incrementing this value makes the app spawn various 59 | -- notifications 60 | Notification_ID = Notification_ID + 1 61 | end 62 | 63 | self.active_window:add(Button) 64 | self.active_window:present() 65 | end 66 | 67 | return App:run(arg) -------------------------------------------------------------------------------- /GtkButton2.lua: -------------------------------------------------------------------------------- 1 | 2 | if arg[#arg] == "-debug" then require"mobdebug".start() end 3 | 4 | local lgi = require"lgi" 5 | local Gtk = lgi.require("Gtk", "3.0") 6 | 7 | local App = Gtk.Application{ 8 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkButton2" 9 | } 10 | 11 | function App:on_startup() 12 | Gtk.ApplicationWindow{ 13 | application = self, 14 | default_width = 400, 15 | default_height = 400 16 | } 17 | end 18 | 19 | function App:on_activate() 20 | local window = self.active_window 21 | 22 | window:set_titlebar(Gtk.HeaderBar{ 23 | visible = true, 24 | show_close_button = true, 25 | title = "GtkButton", 26 | subtitle = "Example 2" 27 | }) 28 | 29 | local Button = Gtk.Button{ 30 | visible = true, 31 | label = "Click me!", 32 | valign = Gtk.Align.CENTER, 33 | halign = Gtk.Align.CENTER 34 | } 35 | 36 | local Notification_ID = 1 37 | 38 | function Button:on_clicked() 39 | 40 | local label = ("Clicked %d times!"):format(Notification_ID) 41 | self.label = label 42 | print(label) 43 | 44 | if Notification_ID >= 3 then window:destroy() end 45 | 46 | Notification_ID = Notification_ID + 1 47 | 48 | end 49 | 50 | window:add(Button) 51 | window:present() 52 | end 53 | 54 | return App:run(arg) -------------------------------------------------------------------------------- /GtkButton3.lua: -------------------------------------------------------------------------------- 1 | 2 | if arg[#arg] == "-debug" then require"mobdebug".start() end 3 | 4 | local lgi = require"lgi" 5 | local Gtk = lgi.require("Gtk", "3.0") 6 | 7 | local Builder = Gtk.Builder.new_from_file("GtkBuilder-02.ui") 8 | local UI = Builder.objects 9 | 10 | local App = Gtk.Application{ 11 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkButton3" 12 | } 13 | 14 | function App:on_startup() 15 | self:add_window(UI.Window) 16 | end 17 | 18 | function grid_click(button) 19 | local counter = 1 20 | function button:on_clicked() 21 | local label_parent = self.child[1] or self 22 | label_parent.label = counter 23 | counter = counter + 1 24 | end 25 | end 26 | 27 | function App:on_activate() 28 | local window = self.active_window 29 | for row=1,3 do 30 | for col=1,3 do 31 | local name="button"..row..col 32 | grid_click(UI[name]) 33 | end 34 | end 35 | window:present() 36 | end 37 | 38 | return App:run(arg) -------------------------------------------------------------------------------- /GtkComboBox.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local GObject = lgi.require("GObject", "2.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkComboBox" 7 | }) 8 | 9 | function App:on_startup() 10 | Gtk.ApplicationWindow({ 11 | application = self, 12 | default_width = 400, 13 | default_height = 400, 14 | border_width = 10 15 | }) 16 | end 17 | 18 | function App:on_activate() 19 | self.active_window:set_titlebar(Gtk.HeaderBar({ 20 | visible = true, 21 | show_close_button = true, 22 | title = "GtkComboBox" 23 | })) 24 | 25 | -- Model for the combo box 26 | local Model = Gtk.ListStore.new({ GObject.Type.STRING }) 27 | local Items = { "GNOME", "KDE Plasma", "XFCE", "MATE", "Cinnamon", "Pantheon", "LXDE", "LXQT" } 28 | 29 | -- Add the items to the model 30 | for _, Name in ipairs(Items) do 31 | Model:append({ Name }) 32 | end 33 | 34 | -- Label to be updated 35 | local Label = Gtk.Label({ visible = true, label = "Default option: 0" }) 36 | 37 | --[[ GtkComboBox: 38 | 39 | A container that allows the user to select "something" among several 40 | valid options. 41 | 42 | ]] 43 | local Combo = Gtk.ComboBox({ 44 | visible = true, 45 | model = Model, 46 | active = 0, 47 | cells = { 48 | { 49 | Gtk.CellRendererText(), 50 | { text = 1 }, 51 | align = Gtk.Align.START 52 | } 53 | } 54 | }) 55 | 56 | -- Changes the 'Label' text when user change the combo box value 57 | function Combo:on_changed() 58 | local n = self:get_active() 59 | Label.label = "Option "..n.." selected ("..Items[n + 1]..")" 60 | end 61 | 62 | local Box = Gtk.Box({ 63 | visible = true, 64 | orientation = Gtk.Orientation.VERTICAL, 65 | spacing = 10, 66 | halign = Gtk.Align.CENTER, 67 | valign = Gtk.Align.CENTER, 68 | 69 | Gtk.Label({ visible = true, label = "Select an option" }), 70 | Combo, 71 | Label 72 | }) 73 | 74 | self.active_window:add(Box) 75 | self.active_window:present() 76 | end 77 | 78 | return App:run(arg) -------------------------------------------------------------------------------- /GtkComboBoxText.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local GObject = lgi.require("GObject", "2.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkComboBoxText" 7 | }) 8 | 9 | function App:on_startup() 10 | Gtk.ApplicationWindow({ 11 | application = self, 12 | default_width = 400, 13 | default_height = 400, 14 | border_width = 10 15 | }) 16 | end 17 | 18 | function App:on_activate() 19 | self.active_window:set_titlebar(Gtk.HeaderBar({ 20 | visible = true, 21 | show_close_button = true, 22 | title = "GtkComboBoxText" 23 | })) 24 | 25 | -- Label to be updated 26 | local Label = Gtk.Label({ visible = true, label = "Default id: gnome" }) 27 | 28 | --[[ GtkComboBoxText: 29 | 30 | Just a text-only GtkComboBox 31 | 32 | ]] 33 | local Combo = Gtk.ComboBoxText({ visible = true }) 34 | 35 | local Items = { 36 | gnome = "GNOME", 37 | plasma = "KDE Plasma", 38 | xfce = "XFCE", 39 | mate = "MATE", 40 | cinnamon = "Cinnamon", 41 | pantheon = "Pantheon", 42 | lxde = "LXDE", 43 | lxqt = "LXQT" 44 | } 45 | 46 | for ID, Value in pairs(Items) do 47 | Combo:append(ID, Value) 48 | end 49 | 50 | Combo.active_id = "gnome" 51 | 52 | -- Changes the 'Label' text when user change the combo box value 53 | function Combo:on_changed() 54 | Label.label = "Option id: " .. self:get_active_id() 55 | end 56 | 57 | local Box = Gtk.Box({ 58 | visible = true, 59 | orientation = Gtk.Orientation.VERTICAL, 60 | spacing = 10, 61 | halign = Gtk.Align.CENTER, 62 | valign = Gtk.Align.CENTER, 63 | 64 | Gtk.Label({ visible = true, label = "Select an option" }), 65 | Combo, 66 | Label 67 | }) 68 | 69 | self.active_window:add(Box) 70 | self.active_window:present() 71 | end 72 | 73 | return App:run(arg) -------------------------------------------------------------------------------- /GtkCssProvider1.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | @author Díaz Urbaneja Víctor Eduardo Diex 3 | @date 28.02.2021 02:26:39 -04 4 | ]] 5 | 6 | local lgi = require('lgi') 7 | local Gtk = lgi.require('Gtk', '3.0') 8 | local Gdk = lgi.require('Gdk', '3.0') 9 | assert = lgi.assert -- With this function I will confirm if a file (in this case custom.css) exists. 10 | 11 | --- I load my css 12 | local Provider = Gtk.CssProvider() 13 | 14 | -- Show a message if custom.css does not exist 15 | assert(Provider:load_from_path('styles/GtkCssProvider1.css'), 'ERROR: file.css not found') 16 | 17 | --- I add my CSS to the current window 18 | local Screen = Gdk.Display.get_default_screen(Gdk.Display:get_default()) 19 | local GTK_STYLE_PROVIDER_PRIORITY_APPLICATION = 600 20 | Gtk.StyleContext.add_provider_for_screen( 21 | Screen, Provider, 22 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION 23 | ) 24 | 25 | local Window = Gtk.Window { 26 | title = 'Linking CSS Styles', 27 | width = 200, 28 | height = 200, 29 | window_position = Gtk.WindowPosition.CENTER, 30 | { 31 | Gtk.Button { 32 | halign = Gtk.Align.CENTER, 33 | valign = Gtk.Align.CENTER, 34 | label = 'Example of the button with css', 35 | on_clicked = function (self) 36 | self:get_style_context():add_class('red') 37 | end 38 | } 39 | }, 40 | on_destroy = function() 41 | Gtk.main_quit() 42 | end 43 | } 44 | 45 | Window:show_all() 46 | Gtk.main() 47 | -------------------------------------------------------------------------------- /GtkDialog1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local Pango = lgi.require("Pango", "1.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkDialog1" 7 | }) 8 | 9 | function App:on_startup() 10 | --[[ GtkDialog: 11 | 12 | A (generic) popup window 13 | 14 | ]] 15 | local Dialog = Gtk.Dialog({ 16 | application = self, 17 | default_width = 400, 18 | border_width = 10, 19 | title = "GtkDialog - Example 1" 20 | }) 21 | 22 | Dialog:add_button("Yes 👍", Gtk.ResponseType.OK) 23 | Dialog:add_button("No 🛑", Gtk.ResponseType.CANCEL) 24 | 25 | local TitleText = [[Universe destruction]] 26 | local Title = Gtk.Label({ visible = true, label = TitleText, use_markup = true }) 27 | 28 | local SummaryText = "Our universe has a lot of problems and the only way to fix it is destroying the entire universe and this important decision is now in your hands." 29 | local Summary = Gtk.Label({ 30 | visible = true, 31 | label = SummaryText, 32 | xalign = 0, 33 | wrap = true, 34 | wrap_mode = Pango.WrapMode.CHAR 35 | }) 36 | 37 | local EpilogText = [[Do you accept?]] 38 | local Epilog = Gtk.Label({ visible = true, label = EpilogText, use_markup = true }) 39 | 40 | local Content = Dialog:get_content_area() 41 | Content.spacing = 10 42 | Content:pack_start(Title, false, true, 0) 43 | Content:pack_start(Summary, false, true, 0) 44 | Content:pack_start(Epilog, false, true, 10) 45 | 46 | self:add_window(Dialog) 47 | end 48 | 49 | function App:on_activate() 50 | -- When you work with dialogs, use this instead of 'present()' 51 | local Response = self.active_window:run() 52 | 53 | if Response == Gtk.ResponseType.OK then 54 | self.active_window:destroy() 55 | print("Universe destroyed! 💥") 56 | elseif Response == Gtk.ResponseType.CANCEL then 57 | self.active_window:destroy() 58 | print("Universe is in peace now! 🙏") 59 | else 60 | self.active_window:destroy() 61 | print("Nothing happens! 🤔") 62 | end 63 | end 64 | 65 | return App:run(arg) -------------------------------------------------------------------------------- /GtkDialog2.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local Pango = lgi.require("Pango", "1.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkDialog2" 7 | }) 8 | 9 | function App:on_startup() 10 | --[[ GtkDialog: 11 | 12 | A (generic) popup window 13 | 14 | ]] 15 | local Dialog = Gtk.Dialog({ 16 | application = self, 17 | use_header_bar = 1, 18 | default_width = 400, 19 | border_width = 10 20 | }) 21 | 22 | Dialog:add_button("Yes 👍", Gtk.ResponseType.OK) 23 | Dialog:add_button("No 🛑", Gtk.ResponseType.CANCEL) 24 | 25 | local Header = Dialog:get_header_bar() 26 | Header.title = "GtkDialog" 27 | Header.subtitle = "Example 2" 28 | 29 | local TitleText = [[Universe destruction]] 30 | local Title = Gtk.Label({ visible = true, label = TitleText, use_markup = true }) 31 | 32 | local SummaryText = "Our universe has a lot of problems and the only way to fix it is destroying the entire universe and this important decision is now in your hands." 33 | local Summary = Gtk.Label({ 34 | visible = true, 35 | label = SummaryText, 36 | xalign = 0, 37 | wrap = true, 38 | wrap_mode = Pango.WrapMode.CHAR 39 | }) 40 | 41 | local EpilogText = [[Do you accept?]] 42 | local Epilog = Gtk.Label({ visible = true, label = EpilogText, use_markup = true }) 43 | 44 | local Content = Dialog:get_content_area() 45 | Content.spacing = 10 46 | Content:pack_start(Title, false, true, 0) 47 | Content:pack_start(Summary, false, true, 0) 48 | Content:pack_start(Epilog, false, true, 10) 49 | 50 | self:add_window(Dialog) 51 | end 52 | 53 | function App:on_activate() 54 | -- When you work with dialogs, use this instead of 'present()' 55 | local Response = self.active_window:run() 56 | 57 | if Response == Gtk.ResponseType.OK then 58 | self.active_window:destroy() 59 | print("Universe destroyed! 💥") 60 | elseif Response == Gtk.ResponseType.CANCEL then 61 | self.active_window:destroy() 62 | print("Universe is in peace now! 🙏") 63 | else 64 | self.active_window:destroy() 65 | print("Nothing happens! 🤔") 66 | end 67 | end 68 | 69 | return App:run(arg) -------------------------------------------------------------------------------- /GtkEntry.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local GObject = lgi.require("GObject", "2.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkEntry" 7 | }) 8 | 9 | function App:on_startup() 10 | Gtk.ApplicationWindow({ 11 | application = self, 12 | default_width = 400, 13 | default_height = 400, 14 | border_width = 10 15 | }) 16 | end 17 | 18 | function App:on_activate() 19 | self.active_window:set_titlebar(Gtk.HeaderBar({ 20 | visible = true, 21 | show_close_button = true, 22 | title = "GtkEntry" 23 | })) 24 | 25 | -- Label to be updated 26 | local Label = Gtk.Label({ visible = true }) 27 | 28 | --[[ GtkEntry: 29 | 30 | An input widget 31 | 32 | ]] 33 | local Entry = Gtk.Entry({ visible = true }) 34 | 35 | -- Updates the label text while typing 36 | function Entry:on_key_release_event() 37 | Label.label = Entry.text 38 | end 39 | 40 | local Box = Gtk.Box({ 41 | visible = true, 42 | orientation = Gtk.Orientation.VERTICAL, 43 | spacing = 10, 44 | halign = Gtk.Align.CENTER, 45 | valign = Gtk.Align.CENTER, 46 | 47 | Gtk.Label({ visible = true, label = "Enter some text" }), 48 | Entry, 49 | Label 50 | }) 51 | 52 | self.active_window:add(Box) 53 | self.active_window:present() 54 | end 55 | 56 | return App:run(arg) -------------------------------------------------------------------------------- /GtkEntryCompletion.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local GObject = lgi.require("GObject", "2.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkEntryCompletion" 7 | }) 8 | 9 | function App:on_startup() 10 | Gtk.ApplicationWindow({ 11 | application = self, 12 | default_width = 400, 13 | default_height = 400, 14 | border_width = 10 15 | }) 16 | end 17 | 18 | function App:on_activate() 19 | self.active_window:set_titlebar(Gtk.HeaderBar({ 20 | visible = true, 21 | show_close_button = true, 22 | title = "GtkEntryCompletion" 23 | })) 24 | 25 | -- Model for the entry completion 26 | local Model = Gtk.ListStore.new({ GObject.Type.STRING }) 27 | 28 | -- Items to be completed 29 | local Items = { "GNOME", "Lua", "LGI", "GTK", "Moonsteal", "Example" } 30 | 31 | -- Add the items to the model 32 | for _, Name in ipairs(Items) do Model:append({ Name }) end 33 | 34 | -- The entry completion 35 | local Completion = Gtk.EntryCompletion({ 36 | model = Model, 37 | text_column = 0, 38 | popup_completion = true 39 | }) 40 | 41 | -- The entry 42 | local Entry = Gtk.Entry({ visible = true, completion = Completion }) 43 | 44 | local Box = Gtk.Box({ 45 | visible = true, 46 | orientation = Gtk.Orientation.VERTICAL, 47 | spacing = 10, 48 | halign = Gtk.Align.CENTER, 49 | valign = Gtk.Align.CENTER, 50 | 51 | Gtk.Label({ visible = true, label = "Try \"gnome\" or \"gtk\"" }), 52 | Entry 53 | }) 54 | 55 | self.active_window:add(Box) 56 | self.active_window:present() 57 | end 58 | 59 | return App:run(arg) -------------------------------------------------------------------------------- /GtkExpander.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkExpander" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | border_width = 10, 13 | resizable = false 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "Your app title", 22 | subtitle = "Your app subtitle" 23 | })) 24 | 25 | local LoremIpsum = [[Duis in metus eros. Duis faucibus rutrum eros eu vestibulum. 26 | Proin et arcu nulla. Etiam at lacinia nibh. Vivamus pellentesque nunc nibh, 27 | ac dignissim massa lobortis ut. Integer eu felis in elit semper ullamcorper 28 | at in ipsum. Suspendisse tempus massa vel nibh tristique vestibulum. 29 | Vestibulum varius eu nunc eu interdum. Curabitur pulvinar velit in purus 30 | facilisis, et auctor augue consequat. Donec finibus felis ligula, a convallis 31 | justo tristique a.]] 32 | 33 | --[[ GtkExpander: 34 | 35 | A widget that allows hide its child, like a GtkRevealer, but you don't need to 36 | show/hide the child programmatically and has a small "hint" that there is 37 | "something" hidden. 38 | 39 | ]] 40 | local Expand = Gtk.Expander({ 41 | visible = true, 42 | label = [[ Lorem ipsum ]], 43 | use_markup = true, 44 | resize_toplevel = true, 45 | 46 | Gtk.Label({ visible = true, label = LoremIpsum, wrap = true }) 47 | }) 48 | 49 | self.active_window:add(Expand) 50 | self.active_window:present() 51 | end 52 | 53 | return App:run(arg) -------------------------------------------------------------------------------- /GtkFileChooserButton.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkFileChooserButton" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400, 13 | border_width = 10 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkFileChooserButton" 22 | })) 23 | 24 | local Box = Gtk.Box({ 25 | visible = true, 26 | spacing = 10, 27 | orientation = Gtk.Orientation.VERTICAL, 28 | valign = Gtk.Align.CENTER, 29 | halign = Gtk.Align.CENTER, 30 | 31 | Gtk.Label({ visible = true, label = "Open a file" }), 32 | }) 33 | 34 | --[[ GtkFileChooserButton: 35 | 36 | A button that exclusively opens a file selection dialog 37 | 38 | ]] 39 | local Btn = Gtk.FileChooserButton({ 40 | visible = true, 41 | valign = Gtk.Align.CENTER, 42 | halign = Gtk.Align.CENTER 43 | }) 44 | 45 | local Lbl = Gtk.Label({ visible = true, wrap = true }) 46 | 47 | function Btn:on_file_set() 48 | Lbl.label = "Selected file: " .. self:get_filename() 49 | end 50 | 51 | Box:pack_start(Btn, false, true, 0) 52 | Box:pack_start(Lbl, false, true, 0) 53 | 54 | self.active_window:add(Box) 55 | self.active_window:present() 56 | end 57 | 58 | return App:run() -------------------------------------------------------------------------------- /GtkFileChooserDialog.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkFileChooserDialog" 6 | }) 7 | 8 | function App:on_startup() 9 | --[[ GtkFileChooserDialog: 10 | 11 | A file selection dialog 12 | 13 | ]] 14 | local Dialog = Gtk.FileChooserDialog({ 15 | title = "Select a file", 16 | action = Gtk.FileChooserAction.OPEN 17 | }) 18 | 19 | Dialog:add_button("Open", Gtk.ResponseType.OK) 20 | Dialog:add_button("Cancel", Gtk.ResponseType.CANCEL) 21 | 22 | self:add_window(Dialog) 23 | end 24 | 25 | function App:on_activate() 26 | local Res = self.active_window:run() 27 | 28 | if Res == Gtk.ResponseType.OK then 29 | local name = self.active_window:get_filename() 30 | self.active_window:destroy() 31 | print("You selected: " .. name) 32 | elseif Res == Gtk.ResponseType.CANCEL then 33 | self.active_window:destroy() 34 | print("Canceled") 35 | else 36 | self.active_window:destroy() 37 | print("Something else") 38 | end 39 | end 40 | 41 | return App:run() -------------------------------------------------------------------------------- /GtkFixed.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local app = Gtk.Application { application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkFixed" } 5 | 6 | function app:on_activate() 7 | self.active_window:present() 8 | end 9 | 10 | function app:on_startup() 11 | local win = Gtk.ApplicationWindow { 12 | application = self, 13 | default_width = 400, 14 | default_height = 400, 15 | border_width = 10 16 | } 17 | 18 | win:set_titlebar(Gtk.HeaderBar { 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkFlowBox" 22 | }) 23 | 24 | local fixed = Gtk.Fixed { 25 | visible = true, 26 | 27 | { Gtk.Label { visible = true, label = "A" }, y = 20, x = 10 }, 28 | { Gtk.Label { visible = true, label = "B" }, y = 200, x = 100 }, 29 | { Gtk.Label { visible = true, label = "C" }, y = 99, x = 326 } 30 | } 31 | 32 | win:add(fixed) 33 | end 34 | 35 | return app:run(arg) -------------------------------------------------------------------------------- /GtkFlowBox.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local app = Gtk.Application { application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkFlowBox" } 5 | 6 | function app:on_activate() 7 | self.active_window:present() 8 | end 9 | 10 | function app:on_startup() 11 | local win = Gtk.ApplicationWindow { 12 | application = self, 13 | default_width = 400, 14 | default_height = 400, 15 | border_width = 10 16 | } 17 | 18 | win:set_titlebar(Gtk.HeaderBar { 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkFlowBox" 22 | }) 23 | 24 | local flowbox = Gtk.FlowBox { 25 | visible = true, 26 | valign = Gtk.Align.START, 27 | selection_mode = Gtk.SelectionMode.NONE, 28 | max_children_per_line = 30 29 | } 30 | 31 | local scroll = Gtk.ScrolledWindow { 32 | visible = true, 33 | hscrollbar_policy = Gtk.PolicyType.NEVER 34 | } 35 | 36 | local icons = { 37 | "face-angry", 38 | "face-surprise", 39 | "face-laugh", 40 | "face-plain", 41 | "face-sad", 42 | "face-cool", 43 | "face-smirk", 44 | "face-sick", 45 | "face-kiss", 46 | "face-smile" 47 | } 48 | 49 | math.randomseed(os.time()) 50 | 51 | for i = 0, 399 do 52 | flowbox:insert(Gtk.Image { 53 | visible = true, 54 | icon_name = icons[math.random(1, 10)], 55 | pixel_size = 32 56 | }, i) 57 | end 58 | 59 | scroll:add(flowbox) 60 | win:add(scroll) 61 | end 62 | 63 | return app:run(arg) -------------------------------------------------------------------------------- /GtkGrid-Explained.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Miqueas/Lua-GTK-Examples/cf3269795477eea5c21396779e433701d9ee2326/GtkGrid-Explained.png -------------------------------------------------------------------------------- /GtkGrid1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkGrid1" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400, 13 | border_width = 10 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkGrid", 22 | subtitle = "Example 1" 23 | })) 24 | 25 | --[[ GtkGrid: 26 | 27 | A container that acts like a grid, with columns, rows and cells. 28 | Just look at this example: 29 | 30 | ]] 31 | local Grid = Gtk.Grid({ 32 | visible = true, 33 | -- This makes all columns of the same width 34 | column_homogeneous = true, 35 | -- Same, but for rows 36 | row_homogeneous = true, 37 | -- Sets the spacing between all columns 38 | column_spacing = 10, 39 | -- Same, but for rows 40 | row_spacing = 10, 41 | 42 | halign = Gtk.Align.CENTER, 43 | valign = Gtk.Align.CENTER, 44 | 45 | -- If you can't understand how GtkGrid works, see the GtkGrid-Explained.png file 46 | { Gtk.Label({ visible = true, label = "Top: 0. Left: 0" }), top_attach = 0, left_attach = 0 }, 47 | { Gtk.Label({ visible = true, label = "Top: 0. Left: 1" }), top_attach = 0, left_attach = 1 }, 48 | { Gtk.Label({ visible = true, label = "Top: 0. Left: 2" }), top_attach = 0, left_attach = 2 }, 49 | { Gtk.Label({ visible = true, label = "Top: 1. Left: 0" }), top_attach = 1, left_attach = 0 }, 50 | { Gtk.Label({ visible = true, label = "Top: 1. Left: 1" }), top_attach = 1, left_attach = 1 }, 51 | { Gtk.Label({ visible = true, label = "Top: 1. Left: 2" }), top_attach = 1, left_attach = 2 }, 52 | { Gtk.Label({ visible = true, label = "Top: 2. Left: 0" }), top_attach = 2, left_attach = 0 }, 53 | { Gtk.Label({ visible = true, label = "Top: 2. Left: 1" }), top_attach = 2, left_attach = 1 }, 54 | { Gtk.Label({ visible = true, label = "Top: 2. Left: 2" }), top_attach = 2, left_attach = 2 } 55 | }) 56 | 57 | self.active_window:add(Grid) 58 | self.active_window:present() 59 | end 60 | 61 | return App:run(arg) -------------------------------------------------------------------------------- /GtkGrid2.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkGrid2" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400, 13 | border_width = 10 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkGrid", 22 | subtitle = "Example 2" 23 | })) 24 | 25 | --[[ 26 | 27 | In the example GtkGrid1.lua you see a basic grid. Here, you can see a little complex 28 | grid with two items that fills more than 1 cell in the grid. 29 | 30 | ]] 31 | local Grid = Gtk.Grid({ 32 | visible = true, 33 | column_homogeneous = true, 34 | row_homogeneous = true, 35 | column_spacing = 10, 36 | row_spacing = 10, 37 | 38 | halign = Gtk.Align.CENTER, 39 | valign = Gtk.Align.CENTER, 40 | 41 | { Gtk.Label({ visible = true, label = "Columns" }), top_attach = 0, left_attach = 1, width = 2 }, 42 | { Gtk.Label({ visible = true, label = "Rows" }), top_attach = 0, left_attach = 0, height = 3 }, 43 | { Gtk.Label({ visible = true, label = "Top: 1. Left: 1" }), top_attach = 1, left_attach = 1 }, 44 | { Gtk.Label({ visible = true, label = "Top: 1. Left: 2" }), top_attach = 1, left_attach = 2 }, 45 | { Gtk.Label({ visible = true, label = "Top: 2. Left: 1" }), top_attach = 2, left_attach = 1 }, 46 | { Gtk.Label({ visible = true, label = "Top: 2. Left: 2" }), top_attach = 2, left_attach = 2 } 47 | }) 48 | 49 | self.active_window:add(Grid) 50 | self.active_window:present() 51 | end 52 | 53 | return App:run(arg) -------------------------------------------------------------------------------- /GtkGrid3.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkGrid3" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400, 13 | border_width = 10 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkGrid", 22 | subtitle = "Example 3" 23 | })) 24 | 25 | --[[ GtkGrid: 26 | 27 | A container that acts like a grid, with columns, rows and cells. 28 | Just look at this example: 29 | 30 | ]] 31 | local Grid = Gtk.Grid({ 32 | visible = true, 33 | -- This makes all columns of the same width 34 | column_homogeneous = true, 35 | -- Same, but for rows 36 | row_homogeneous = true, 37 | -- Sets the spacing between all columns 38 | column_spacing = 10, 39 | -- Same, but for rows 40 | row_spacing = 10, 41 | 42 | halign = Gtk.Align.CENTER, 43 | valign = Gtk.Align.CENTER, 44 | }) 45 | 46 | for top = 0, 2 do 47 | for left = 0, 2 do 48 | Grid:attach( 49 | Gtk.Label({ 50 | visible = true, 51 | label = ("Top: %d. Left: %d."):format(top, left) 52 | }), 53 | left, top, 1, 1 54 | ) 55 | end 56 | end 57 | 58 | self.active_window:add(Grid) 59 | self.active_window:present() 60 | end 61 | 62 | return App:run(arg) -------------------------------------------------------------------------------- /GtkHeaderBar.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkHeaderBar" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | --[[ GtkHeaderBar: 18 | 19 | A container that acts as the title bar of the window. 20 | This is a CSD (Client Side Decorations), since we are 21 | setting up our own title bar for our window, rather 22 | than leaving it up to the WM to decorate it. 23 | 24 | In other examples we talk a little more about CSD using CSS. 25 | 26 | ]] 27 | self.active_window:set_titlebar(Gtk.HeaderBar({ 28 | -- By default, all GTK widgets are not visibles 29 | visible = true, 30 | -- GtkHeaderBar don't sets the window controls buttons by default 31 | -- this property changes that behavior 32 | show_close_button = true, 33 | title = "Your app title", 34 | subtitle = "Your app subtitle" 35 | })) 36 | 37 | self.active_window:present() 38 | end 39 | 40 | return App:run(arg) -------------------------------------------------------------------------------- /GtkImage1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkImage1" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkImage", 21 | subtitle = "Example 1" 22 | })) 23 | 24 | --[[ GtkImage: 25 | 26 | Well... A widget that display an image lol 27 | 28 | ]] 29 | local Img = Gtk.Image({ visible = true, file = "Moonsteal-Logo.jpg" }) 30 | 31 | self.active_window:add(Img) 32 | self.active_window:present() 33 | end 34 | 35 | return App:run(arg) -------------------------------------------------------------------------------- /GtkImage2.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkImage2" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkImage", 21 | subtitle = "Example 2" 22 | })) 23 | 24 | local Img = Gtk.Image({ visible = true, icon_name = "computer", pixel_size = 256 }) 25 | 26 | self.active_window:add(Img) 27 | self.active_window:present() 28 | end 29 | 30 | return App:run(arg) -------------------------------------------------------------------------------- /GtkInfoBar.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkInfoBar" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400, 13 | border_width = 10 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkInfoBar" 22 | })) 23 | 24 | local Box = Gtk.Box({ visible = true, orientation = Gtk.Orientation.VERTICAL }) 25 | local InfoBtn = Gtk.Button({ visible = true, label = "Info" }) 26 | local WarningBtn = Gtk.Button({ visible = true, label = "Warning" }) 27 | local QuestionBtn = Gtk.Button({ visible = true, label = "Question" }) 28 | local ErrorBtn = Gtk.Button({ visible = true, label = "Error" }) 29 | local OtherBtn = Gtk.Button({ visible = true, label = "Other" }) 30 | 31 | function CreateInfoBar(self, data) 32 | --[[ GtkInfoBar: 33 | 34 | A widget that can be used to show messages to the user. 35 | Think in it as In-App notifications. The GtkInfoBar API 36 | is very similar to the GtkDialog API 37 | 38 | ]] 39 | local InfoBar = Gtk.InfoBar({ 40 | visible = true, 41 | message_type = Gtk.MessageType[self.label:upper()], 42 | show_close_button = true 43 | }) 44 | 45 | -- For this widget, the 'get_content_area()' method always returns a Gtk.Box 46 | InfoBar 47 | :get_content_area() 48 | :pack_start(Gtk.Label({ visible = true, label = self.label }), false, true, 0) 49 | 50 | -- When an action widget is clicked, this signal is emitted 51 | function InfoBar:on_response() 52 | -- For this case, just remove it from the Gtk.Box and then destroy it 53 | Box:remove(self) 54 | self:destroy() 55 | end 56 | 57 | -- Add the info bar to the Gtk.Box 58 | Box:pack_start(InfoBar, false, true, 0) 59 | end 60 | 61 | InfoBtn.on_clicked = CreateInfoBar 62 | WarningBtn.on_clicked = CreateInfoBar 63 | QuestionBtn.on_clicked = CreateInfoBar 64 | ErrorBtn.on_clicked = CreateInfoBar 65 | OtherBtn.on_clicked = CreateInfoBar 66 | 67 | local Grid = Gtk.Grid({ 68 | visible = true, 69 | column_homogeneous = true, 70 | row_homogeneous = true, 71 | column_spacing = 10, 72 | row_spacing = 10, 73 | 74 | halign = Gtk.Align.CENTER, 75 | valign = Gtk.Align.CENTER, 76 | 77 | { InfoBtn, top_attach = 0, left_attach = 0 }, 78 | { WarningBtn, top_attach = 0, left_attach = 1 }, 79 | { QuestionBtn, top_attach = 1, left_attach = 0 }, 80 | { ErrorBtn, top_attach = 1, left_attach = 1 }, 81 | { OtherBtn, top_attach = 2, left_attach = 0, width = 2 }, 82 | }) 83 | 84 | Box:pack_end(Grid, true, true, 0) 85 | 86 | self.active_window:add(Box) 87 | self.active_window:present() 88 | end 89 | 90 | return App:run(arg) -------------------------------------------------------------------------------- /GtkLabel1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkLabel1" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkLabel", 21 | subtitle = "Example 1" 22 | })) 23 | 24 | --[[ GtkLabel: 25 | 26 | A text widget 27 | 28 | ]] 29 | local Label = Gtk.Label({ 30 | visible = true, 31 | label = "Hello, world!", 32 | valign = Gtk.Align.CENTER, 33 | halign = Gtk.Align.CENTER 34 | }) 35 | 36 | self.active_window:add(Label) 37 | self.active_window:present() 38 | end 39 | 40 | return App:run(arg) -------------------------------------------------------------------------------- /GtkLabel2.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkLabel2" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkLabel", 21 | subtitle = "Example 2" 22 | })) 23 | 24 | local Label = Gtk.Label({ 25 | visible = true, 26 | valign = Gtk.Align.CENTER, 27 | halign = Gtk.Align.CENTER, 28 | -- The use_markup property to use an HTML-like 29 | -- basic markup: 30 | use_markup = true, 31 | label = "Hello, world!" 32 | }) 33 | 34 | self.active_window:add(Label) 35 | self.active_window:present() 36 | end 37 | 38 | return App:run(arg) -------------------------------------------------------------------------------- /GtkListBox1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkListBox1" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400, 13 | border_width = 10 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkListBox", 22 | subtitle = "Example 1" 23 | })) 24 | 25 | local Box = Gtk.Box({ 26 | visible = true, 27 | orientation = Gtk.Orientation.VERTICAL 28 | }) 29 | 30 | --[[ GtkListBox: 31 | 32 | A container for lists. It works better for lists 33 | than a GtkBox because of its vertical nature and 34 | the API it offers. 35 | 36 | ]] 37 | local List = Gtk.ListBox({ visible = true }) 38 | 39 | local Scroll = Gtk.ScrolledWindow({ 40 | visible = true, 41 | shadow_type = Gtk.ShadowType.NONE, 42 | propagate_natural_width = true, 43 | propagate_natural_height = true, 44 | 45 | List 46 | }) 47 | 48 | local Btn = Gtk.Button({ 49 | visible = true, 50 | label = "Load", 51 | halign = Gtk.Align.CENTER, 52 | valign = Gtk.Align.CENTER 53 | }) 54 | 55 | function Btn:on_clicked() 56 | for i = 0, 100 do 57 | List:insert(Gtk.Label({ visible = true, label = "Text " .. i }), i) 58 | end 59 | end 60 | 61 | local Box = Gtk.Box({ 62 | visible = true, 63 | orientation = Gtk.Orientation.VERTICAL, 64 | spacing = 10, 65 | 66 | { Scroll, expand = true }, 67 | { Btn } 68 | }) 69 | 70 | self.active_window:add(Box) 71 | self.active_window:present() 72 | end 73 | 74 | return App:run(arg) -------------------------------------------------------------------------------- /GtkMessageDialog.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local Pango = lgi.require("Pango", "1.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkMessageDialog" 7 | }) 8 | 9 | function App:on_startup() 10 | local Title = [[Universe destruction]] 11 | local Message = "Our universe has a lot of problems and the only way to fix it is destroying the entire universe and this important decision is now in your hands." 12 | 13 | --[[ GtkMessageDialog: 14 | 15 | A GtkDialog for basic popup windows with a predefined design: title, message and choice buttons. 16 | Useful if you don't want to make a complex dialog. 17 | 18 | ]] 19 | local Dialog = Gtk.MessageDialog({ 20 | application = self, 21 | buttons = Gtk.ButtonsType.NONE, 22 | message_type = Gtk.MessageType.QUESTION, 23 | title = "GtkMessageDialog", 24 | text = Title, 25 | use_markup = true, 26 | secondary_text = Message 27 | }) 28 | 29 | Dialog:add_button("Yes 👍", Gtk.ResponseType.OK) 30 | Dialog:add_button("No 🛑", Gtk.ResponseType.CANCEL) 31 | end 32 | 33 | function App:on_activate() 34 | -- When you work with dialogs, use this instead of 'present()' 35 | local Res = self.active_window:run() 36 | 37 | if Res == Gtk.ResponseType.OK then 38 | self.active_window:destroy() 39 | print("Universe destroyed! 💥") 40 | elseif Res == Gtk.ResponseType.CANCEL then 41 | self.active_window:destroy() 42 | print("Universe is in peace now! 🙏") 43 | else 44 | self.active_window:destroy() 45 | print("Nothing happens! 🤔") 46 | end 47 | end 48 | 49 | return App:run(arg) -------------------------------------------------------------------------------- /GtkMessageDialog2.lua: -------------------------------------------------------------------------------- 1 | lgi = require "lgi" 2 | Gtk = lgi.require "Gtk" 3 | 4 | dialog = Gtk.MessageDialog { text = "This is a text message.", buttons = "YES_NO" } 5 | 6 | -- https://docs.gtk.org/gtk3/method.Dialog.response.html 7 | function dialog:on_response(response_id) 8 | --print("response_id:",response_id) 9 | 10 | -- https://valadoc.org/gtk+-3.0/Gtk.ResponseType.html 11 | if response_id==Gtk.ResponseType.YES then print("yes") 12 | elseif response_id==Gtk.ResponseType.NO then print("no") 13 | elseif response_id==Gtk.ResponseType.DELETE_EVENT then print("delete-event") 14 | end 15 | 16 | Gtk.main_quit() 17 | end 18 | 19 | dialog:show_all() 20 | Gtk.main() 21 | -------------------------------------------------------------------------------- /GtkNotebook.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | local GObject = lgi.require("GObject", "2.0") 4 | 5 | local App = Gtk.Application({ 6 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkNotebook" 7 | }) 8 | 9 | function App:on_startup() 10 | Gtk.ApplicationWindow({ 11 | application = self, 12 | default_width = 400, 13 | default_height = 400 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | -- Append button 19 | local Btn = Gtk.Button({ 20 | visible = true, 21 | 22 | Gtk.Box({ 23 | visible = true, 24 | 25 | Gtk.Image({ visible = true, icon_name = "list-add-symbolic" }) 26 | }) 27 | }) 28 | 29 | -- Header bar 30 | local Header = Gtk.HeaderBar({ 31 | visible = true, 32 | show_close_button = true, 33 | title = "GtkNotebook", 34 | subtitle = "Click in the + button!", 35 | 36 | Btn 37 | }) 38 | 39 | self.active_window:set_titlebar(Header) 40 | 41 | --[[ GtkNotebook: 42 | 43 | Similar to a GtkStack, it shows only 1 child at a time, 44 | but with the difference that it uses "tabs" to switch 45 | between the visible children. 46 | 47 | ]] 48 | local Notebook = Gtk.Notebook({ 49 | visible = true, 50 | -- Removes a default border around the widget 51 | show_border = false, 52 | -- If has a lot of tabs, then makes it "scrollable" 53 | scrollable = true 54 | }) 55 | 56 | local count = 1 57 | 58 | function Btn:on_clicked() 59 | Notebook:append_page( 60 | -- Page content 61 | Gtk.Label({ visible = true, label = "Page " .. count }), 62 | -- Page tab widget 63 | Gtk.Label({ visible = true, label = "Tab " .. count }) 64 | ) 65 | 66 | count = count + 1 67 | end 68 | 69 | self.active_window:add(Notebook) 70 | self.active_window:present() 71 | end 72 | 73 | return App:run(arg) -------------------------------------------------------------------------------- /GtkOverlay.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkOverlay" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkOverlay" 21 | })) 22 | 23 | --[[ GtkOverlay: 24 | 25 | A widget that allows widgets on top of other (like an stack). GtkOverlay can have 26 | only one main (child) widget, trying to add another widget using the 'add()' 27 | method will throw an error. Use 'add_overlay()' instead, that put the given widget 28 | on top of the main widget or another widget. 29 | 30 | ]] 31 | local Overlay = Gtk.Overlay({ 32 | visible = true, 33 | 34 | -- Our main widget 35 | Gtk.Image({ 36 | visible = true, 37 | icon_name = "computer", 38 | pixel_size = 256 39 | }) 40 | }) 41 | 42 | Overlay:add_overlay(Gtk.Image({ 43 | visible = true, 44 | icon_name = "computer-symbolic", 45 | pixel_size = 128 46 | })) 47 | 48 | Overlay:add_overlay(Gtk.Image({ 49 | visible = true, 50 | icon_name = "input-gaming", 51 | pixel_size = 64 52 | })) 53 | 54 | Overlay:add_overlay(Gtk.Image({ 55 | visible = true, 56 | icon_name = "input-gaming-symbolic", 57 | pixel_size = 32 58 | })) 59 | 60 | self.active_window:add(Overlay) 61 | self.active_window:present() 62 | end 63 | 64 | return App:run(arg) -------------------------------------------------------------------------------- /GtkPaned.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkPaned" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkPaned" 21 | })) 22 | 23 | --[[ GtkPaned: 24 | 25 | A widget with two adjustable panes 26 | 27 | ]] 28 | local Paned = Gtk.Paned({ visible = true, orientation = Gtk.Orientation.HORIZONTAL }) 29 | Paned:pack1(Gtk.Label({ visible = true, label = "Space 1" }), true, false) 30 | Paned:pack2(Gtk.Label({ visible = true, label = "Space 2" }), true, false) 31 | 32 | self.active_window:add(Paned) 33 | self.active_window:present() 34 | end 35 | 36 | return App:run(arg) -------------------------------------------------------------------------------- /GtkRevealer.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkRevealer" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 10 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | local Header = Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkRevealer" 21 | }) 22 | 23 | local Btn = Gtk.Button({ visible = true, label = "Toggle message" }) 24 | Header:pack_start(Btn) 25 | self.active_window:set_titlebar(Header) 26 | 27 | local Box = Gtk.Box({ visible = true, orientation = Gtk.Orientation.VERTICAL }) 28 | 29 | --[[ GtkRevealer: 30 | 31 | A container for show/hide a widget with animation. 32 | 33 | ]] 34 | local Rev = Gtk.Revealer({ 35 | visible = true, 36 | reveal_child = true, 37 | 38 | Gtk.Label({ visible = true, label = "Hello there!", margin = 10 }) 39 | }) 40 | 41 | function Btn:on_clicked() 42 | Rev.reveal_child = (not Rev.reveal_child) and true or false 43 | end 44 | 45 | Box:pack_start(Rev, false, true, 0) 46 | 47 | self.active_window:add(Box) 48 | self.active_window:present() 49 | end 50 | 51 | return App:run(arg) -------------------------------------------------------------------------------- /GtkScrolledWindow.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkScrolledWindow" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkScrolledWindow" 21 | })) 22 | 23 | local Grid = Gtk.Grid({ 24 | visible = true, 25 | column_homogeneous = true, 26 | row_homogeneous = true, 27 | column_spacing = 10, 28 | row_spacing = 10, 29 | halign = Gtk.Align.START, 30 | valign = Gtk.Align.START 31 | }) 32 | 33 | --[[ GtkScrolledWindow: 34 | 35 | A container for scrollable content 36 | 37 | ]] 38 | local Scroll = Gtk.ScrolledWindow({ 39 | visible = true, 40 | -- Removes a shadow that is added by default 41 | shadow_type = Gtk.ShadowType.NONE, 42 | -- "Expands" the child width, and makes the child use the real allocated width 43 | propagate_natural_width = true, 44 | -- Same, but for height 45 | propagate_natural_height = true, 46 | 47 | Grid 48 | }) 49 | 50 | for top = 1, 100 do 51 | for left = 1, 100 do 52 | Grid:attach( 53 | Gtk.Label({ visible = true, label = "Top: "..top..". Left: "..left }), 54 | left - 1, 55 | top - 1, 56 | 1, 1 57 | ) 58 | end 59 | end 60 | 61 | self.active_window:add(Scroll) 62 | self.active_window:present() 63 | end 64 | 65 | return App:run(arg) -------------------------------------------------------------------------------- /GtkSpinner.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local app = Gtk.Application { application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkSpinner" } 5 | 6 | function app:on_activate() 7 | self.active_window:present() 8 | end 9 | 10 | function app:on_startup() 11 | local win = Gtk.ApplicationWindow { 12 | application = self, 13 | default_width = 300, 14 | default_height = 300, 15 | border_width = 10 16 | } 17 | 18 | win:set_titlebar(Gtk.HeaderBar { 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkSpinner" 22 | }) 23 | 24 | local spinner = Gtk.Spinner { visible = true } 25 | local btn = Gtk.Button { visible = true, label = "Start" } 26 | 27 | local box = Gtk.Box { 28 | visible = true, 29 | spacing = 10, 30 | orientation = Gtk.Orientation.VERTICAL, 31 | valign = Gtk.Align.CENTER, 32 | halign = Gtk.Align.CENTER, 33 | 34 | spinner, 35 | btn 36 | } 37 | 38 | function btn:on_clicked() 39 | if spinner.active then 40 | self.label = "Start" 41 | spinner:stop() 42 | else 43 | self.label = "Stop" 44 | spinner:start() 45 | end 46 | end 47 | 48 | win:add(box) 49 | end 50 | 51 | return app:run(arg) -------------------------------------------------------------------------------- /GtkStack1.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkStack1" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 600, 12 | default_height = 400, 13 | border_width = 10 14 | }) 15 | end 16 | 17 | function App:on_activate() 18 | self.active_window:set_titlebar(Gtk.HeaderBar({ 19 | visible = true, 20 | show_close_button = true, 21 | title = "GtkStack", 22 | subtitle = "Example 1" 23 | })) 24 | 25 | --[[ GtkStack: 26 | 27 | A container that shows only one (1) child at a time 28 | 29 | ]] 30 | local Stack = Gtk.Stack({ 31 | visible = true, 32 | transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT, 33 | -- Increment this value to see the animation more slow 34 | transition_duration = 280, 35 | 36 | { 37 | Gtk.Label({ 38 | visible = true, 39 | label = "Page 0", 40 | use_markup = true 41 | }), 42 | title = "Page 0", 43 | name = "Page0" 44 | }, 45 | { 46 | Gtk.Label({ 47 | visible = true, 48 | label = "Page 1", 49 | use_markup = true 50 | }), 51 | title = "Page 1", 52 | name = "Page1" 53 | }, 54 | { 55 | Gtk.Label({ 56 | visible = true, 57 | label = "Page 2", 58 | use_markup = true 59 | }), 60 | title = "Page 2", 61 | name = "Page2" 62 | }, 63 | { 64 | Gtk.Label({ 65 | visible = true, 66 | label = "Page 3", 67 | use_markup = true 68 | }), 69 | title = "Page 3", 70 | name = "Page3" 71 | } 72 | }) 73 | 74 | local Box = Gtk.Box({ 75 | visible = true, 76 | orientation = Gtk.Orientation.VERTICAL, 77 | 78 | { 79 | Stack, 80 | expand = true 81 | }, 82 | 83 | Gtk.StackSwitcher({ 84 | visible = true, 85 | stack = Stack, 86 | halign = Gtk.Align.CENTER 87 | }) 88 | }) 89 | 90 | self.active_window:add(Box) 91 | self.active_window:present() 92 | end 93 | 94 | return App:run(arg) -------------------------------------------------------------------------------- /GtkStack2.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkStack2" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 600, 12 | default_height = 400 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkStack", 21 | subtitle = "Example 2" 22 | })) 23 | 24 | --[[ GtkStack: 25 | 26 | A container that shows only one (1) child at a time 27 | 28 | ]] 29 | local Stack = Gtk.Stack({ 30 | visible = true, 31 | transition_type = Gtk.StackTransitionType.SLIDE_UP_DOWN, 32 | -- Increment this value to see the animation more slow 33 | transition_duration = 280, 34 | 35 | { 36 | Gtk.Label({ 37 | visible = true, 38 | label = "Page 0", 39 | use_markup = true 40 | }), 41 | title = "Page 0", 42 | name = "Page0" 43 | }, 44 | { 45 | Gtk.Label({ 46 | visible = true, 47 | label = "Page 1", 48 | use_markup = true 49 | }), 50 | title = "Page 1", 51 | name = "Page1" 52 | }, 53 | { 54 | Gtk.Label({ 55 | visible = true, 56 | label = "Page 2", 57 | use_markup = true 58 | }), 59 | title = "Page 2", 60 | name = "Page2" 61 | }, 62 | { 63 | Gtk.Label({ 64 | visible = true, 65 | label = "Page 3", 66 | use_markup = true 67 | }), 68 | title = "Page 3", 69 | name = "Page3" 70 | } 71 | }) 72 | 73 | local Box = Gtk.Box({ 74 | visible = true, 75 | orientation = Gtk.Orientation.HORIZONTAL, 76 | 77 | Gtk.StackSidebar({ 78 | visible = true, 79 | stack = Stack, 80 | halign = Gtk.Align.CENTER 81 | }), 82 | { 83 | Stack, 84 | expand = true 85 | } 86 | }) 87 | 88 | self.active_window:add(Box) 89 | self.active_window:present() 90 | end 91 | 92 | return App:run(arg) -------------------------------------------------------------------------------- /GtkStatusbar.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | local App = Gtk.Application({ 5 | application_id = "com.github.Miqueas.Lua-GTK3-Examples.GtkStatusbar" 6 | }) 7 | 8 | function App:on_startup() 9 | Gtk.ApplicationWindow({ 10 | application = self, 11 | default_width = 400, 12 | default_height = 200 13 | }) 14 | end 15 | 16 | function App:on_activate() 17 | self.active_window:set_titlebar(Gtk.HeaderBar({ 18 | visible = true, 19 | show_close_button = true, 20 | title = "GtkStatusbar" 21 | })) 22 | 23 | --[[ GtkStatusbar: 24 | 25 | A widget like GtkActionBar for report some info to the user. 26 | 27 | ]] 28 | local Status = Gtk.Statusbar({ visible = true }) 29 | 30 | local Btn = Gtk.Button({ 31 | visible = true, 32 | label = "Click me", 33 | halign = Gtk.Align.CENTER, 34 | valign = Gtk.Align.CENTER 35 | }) 36 | 37 | 38 | local c = 0 39 | function Btn:on_clicked() 40 | c = c + 1 41 | Status:push(c, "You clicked " .. c .. " times") 42 | end 43 | 44 | local Box = Gtk.Box({ visible = true, orientation = Gtk.Orientation.VERTICAL }) 45 | Box:pack_start(Btn, true, true, 0) 46 | Box:pack_end(Status, false, true, 0) 47 | 48 | self.active_window:add(Box) 49 | self.active_window:present() 50 | end 51 | 52 | return App:run(arg) -------------------------------------------------------------------------------- /GtkWindow.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | --[[ GtkWindow: 5 | 6 | Well... A window lmao 7 | 8 | ]] 9 | local Window = Gtk.Window({ 10 | default_width = 400, 11 | default_height = 400 12 | }) 13 | 14 | -- The "destroy" signal is emited when the user clicks in the 15 | -- close button 16 | Window.on_destroy = Gtk.main_quit 17 | Window:present() 18 | 19 | Gtk.main() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | zlib License 2 | 3 | Copyright (c) 2021 Moonsteal 4 | 5 | This software is provided 'as-is', without any express or implied 6 | warranty. In no event will the authors be held liable for any damages 7 | arising from the use of this software. 8 | 9 | Permission is granted to anyone to use this software for any purpose, 10 | including commercial applications, and to alter it and redistribute it 11 | freely, subject to the following restrictions: 12 | 13 | 1. The origin of this software must not be misrepresented; you must not 14 | claim that you wrote the original software. If you use this software 15 | in a product, an acknowledgment in the product documentation would be 16 | appreciated but is not required. 17 | 18 | 2. Altered source versions must be plainly marked as such, and must not be 19 | misrepresented as being the original software. 20 | 21 | 3. This notice may not be removed or altered from any source 22 | distribution. -------------------------------------------------------------------------------- /Moonsteal-Logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Miqueas/Lua-GTK-Examples/cf3269795477eea5c21396779e433701d9ee2326/Moonsteal-Logo.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Lua GTK examples are [here](https://github.com/Miqueas/GTK-Examples) now 2 | 3 | Hi! As you see, I archived this repo! Please go to the above link for more details :) 4 | -------------------------------------------------------------------------------- /action-bar.lua: -------------------------------------------------------------------------------- 1 | local lgi = require("lgi") 2 | local Gtk = lgi.require("Gtk", "3.0") 3 | 4 | -- GtkActionBar: A full width container to add contextual actions. 5 | 6 | local app_id = "io.github.Miqueas.Lua-GTK-Examples.Gtk3.ActionBar" 7 | local app_title = "GtkActionBar" 8 | local app = Gtk.Application { application_id = app_id } 9 | 10 | function app:on_startup() 11 | local win = Gtk.ApplicationWindow { 12 | title = app_title, 13 | application = self, 14 | default_width = 400, 15 | default_height = 400 16 | } 17 | 18 | local bar = Gtk.ActionBar({ visible = true }) 19 | local box = Gtk.Box({ visible = true, orientation = Gtk.Orientation.VERTICAL }) 20 | 21 | bar:pack_start(Gtk.Label({ visible = true, label = "Something" })) 22 | bar:pack_end(Gtk.Button({ visible = true, label = "A button" })) 23 | 24 | box:pack_start(Gtk.Label({ visible = true, label = "Here is the content of your app" }), true, true, 0) 25 | box:pack_end(bar, false, true, 0) 26 | 27 | win:add(box) 28 | end 29 | 30 | function app:on_activate() 31 | self.active_window:present() 32 | end 33 | 34 | return app:run(arg) -------------------------------------------------------------------------------- /mobdebug.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- MobDebug -- Lua remote debugger 3 | -- Copyright 2011-20 Paul Kulchenko 4 | -- Based on RemDebug 1.0 Copyright Kepler Project 2005 5 | -- 6 | 7 | -- use loaded modules or load explicitly on those systems that require that 8 | local require = require 9 | local io = io or require "io" 10 | local table = table or require "table" 11 | local string = string or require "string" 12 | local coroutine = coroutine or require "coroutine" 13 | local debug = require "debug" 14 | -- protect require "os" as it may fail on embedded systems without os module 15 | local os = os or (function(module) 16 | local ok, res = pcall(require, module) 17 | return ok and res or nil 18 | end)("os") 19 | 20 | local mobdebug = { 21 | _NAME = "mobdebug", 22 | _VERSION = "0.803", 23 | _COPYRIGHT = "Paul Kulchenko", 24 | _DESCRIPTION = "Mobile Remote Debugger for the Lua programming language", 25 | port = os and os.getenv and tonumber((os.getenv("MOBDEBUG_PORT"))) or 8172, 26 | checkcount = 200, 27 | yieldtimeout = 0.02, -- yield timeout (s) 28 | connecttimeout = 2, -- connect timeout (s) 29 | } 30 | 31 | local HOOKMASK = "lcr" 32 | local error = error 33 | local getfenv = getfenv 34 | local setfenv = setfenv 35 | local loadstring = loadstring or load -- "load" replaced "loadstring" in Lua 5.2 36 | local pairs = pairs 37 | local setmetatable = setmetatable 38 | local tonumber = tonumber 39 | local unpack = table.unpack or unpack 40 | local rawget = rawget 41 | local gsub, sub, find = string.gsub, string.sub, string.find 42 | 43 | -- if strict.lua is used, then need to avoid referencing some global 44 | -- variables, as they can be undefined; 45 | -- use rawget to avoid complaints from strict.lua at run-time. 46 | -- it's safe to do the initialization here as all these variables 47 | -- should get defined values (if any) before the debugging starts. 48 | -- there is also global 'wx' variable, which is checked as part of 49 | -- the debug loop as 'wx' can be loaded at any time during debugging. 50 | local genv = _G or _ENV 51 | local jit = rawget(genv, "jit") 52 | local MOAICoroutine = rawget(genv, "MOAICoroutine") 53 | 54 | -- ngx_lua/Openresty requires special handling as its coroutine.* 55 | -- methods use a different mechanism that doesn't allow resume calls 56 | -- from debug hook handlers. 57 | -- Instead, the "original" coroutine.* methods are used. 58 | -- `rawget` needs to be used to protect against `strict` checks 59 | local ngx = rawget(genv, "ngx") 60 | if not ngx then 61 | -- "older" versions of ngx_lua (0.10.x at least) hide ngx table in metatable, 62 | -- so need to use that 63 | local metagindex = getmetatable(genv) and getmetatable(genv).__index 64 | ngx = type(metagindex) == "table" and metagindex.rawget and metagindex:rawget("ngx") or nil 65 | end 66 | local corocreate = ngx and coroutine._create or coroutine.create 67 | local cororesume = ngx and coroutine._resume or coroutine.resume 68 | local coroyield = ngx and coroutine._yield or coroutine.yield 69 | local corostatus = ngx and coroutine._status or coroutine.status 70 | local corowrap = coroutine.wrap 71 | 72 | if not setfenv then -- Lua 5.2+ 73 | -- based on http://lua-users.org/lists/lua-l/2010-06/msg00314.html 74 | -- this assumes f is a function 75 | local function findenv(f) 76 | local level = 1 77 | repeat 78 | local name, value = debug.getupvalue(f, level) 79 | if name == '_ENV' then return level, value end 80 | level = level + 1 81 | until name == nil 82 | return nil end 83 | getfenv = function (f) return(select(2, findenv(f)) or _G) end 84 | setfenv = function (f, t) 85 | local level = findenv(f) 86 | if level then debug.setupvalue(f, level, t) end 87 | return f end 88 | end 89 | 90 | -- check for OS and convert file names to lower case on windows 91 | -- (its file system is case insensitive, but case preserving), as setting a 92 | -- breakpoint on x:\Foo.lua will not work if the file was loaded as X:\foo.lua. 93 | -- OSX and Windows behave the same way (case insensitive, but case preserving). 94 | -- OSX can be configured to be case-sensitive, so check for that. This doesn't 95 | -- handle the case of different partitions having different case-sensitivity. 96 | local win = os and os.getenv and (os.getenv('WINDIR') or (os.getenv('OS') or ''):match('[Ww]indows')) and true or false 97 | local mac = not win and (os and os.getenv and os.getenv('DYLD_LIBRARY_PATH') or not io.open("/proc")) and true or false 98 | local iscasepreserving = win or (mac and io.open('/library') ~= nil) 99 | 100 | -- turn jit off based on Mike Pall's comment in this discussion: 101 | -- http://www.freelists.org/post/luajit/Debug-hooks-and-JIT,2 102 | -- "You need to turn it off at the start if you plan to receive 103 | -- reliable hook calls at any later point in time." 104 | if jit and jit.off then jit.off() end 105 | 106 | local socket = require "socket" 107 | local coro_debugger 108 | local coro_debugee 109 | local coroutines = {}; setmetatable(coroutines, {__mode = "k"}) -- "weak" keys 110 | local events = { BREAK = 1, WATCH = 2, RESTART = 3, STACK = 4 } 111 | local breakpoints = {} 112 | local watches = {} 113 | local lastsource 114 | local lastfile 115 | local watchescnt = 0 116 | local abort -- default value is nil; this is used in start/loop distinction 117 | local seen_hook = false 118 | local checkcount = 0 119 | local step_into = false 120 | local step_over = false 121 | local step_level = 0 122 | local stack_level = 0 123 | local server 124 | local buf 125 | local outputs = {} 126 | local iobase = {print = print} 127 | local basedir = "" 128 | local deferror = "execution aborted at default debugee" 129 | local debugee = function () 130 | local a = 1 131 | for _ = 1, 10 do a = a + 1 end 132 | error(deferror) 133 | end 134 | local function q(s) return string.gsub(s, '([%(%)%.%%%+%-%*%?%[%^%$%]])','%%%1') end 135 | 136 | local serpent = (function() ---- include Serpent module for serialization 137 | local n, v = "serpent", "0.302" -- (C) 2012-18 Paul Kulchenko; MIT License 138 | local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" 139 | local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} 140 | local badtype = {thread = true, userdata = true, cdata = true} 141 | local getmetatable = debug and debug.getmetatable or getmetatable 142 | local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ 143 | local keyword, globals, G = {}, {}, (_G or _ENV) 144 | for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 145 | 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 146 | 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end 147 | for k,v in pairs(G) do globals[v] = k end -- build func to name mapping 148 | for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do 149 | for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end 150 | 151 | local function s(t, opts) 152 | local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum 153 | local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge 154 | local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) 155 | local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring 156 | local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) 157 | local numformat = opts.numformat or "%.17g" 158 | local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 159 | local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", 160 | -- tostring(val) is needed because __tostring may return a non-string value 161 | function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end 162 | local function safestr(s) return type(s) == "number" and tostring(huge and snum[tostring(s)] or numformat:format(s)) 163 | or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 164 | or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end 165 | local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end 166 | local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal 167 | and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end 168 | local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] 169 | local n = name == nil and '' or name 170 | local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] 171 | local safe = plain and n or '['..safestr(n)..']' 172 | return (path or '')..(plain and path and '.' or '')..safe, safe end 173 | local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding 174 | local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} 175 | local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end 176 | table.sort(k, function(a,b) 177 | -- sort numeric keys first: k[key] is not nil for numerical keys 178 | return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) 179 | < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end 180 | local function val2str(t, name, indent, insref, path, plainindex, level) 181 | local ttype, level, mt = type(t), (level or 0), getmetatable(t) 182 | local spath, sname = safename(path, name) 183 | local tag = plainindex and 184 | ((type(name) == "number") and '' or name..space..'='..space) or 185 | (name ~= nil and sname..space..'='..space or '') 186 | if seen[t] then -- already seen this element 187 | sref[#sref+1] = spath..space..'='..space..seen[t] 188 | return tag..'nil'..comment('ref', level) end 189 | -- protect from those cases where __tostring may fail 190 | if type(mt) == 'table' and metatostring ~= false then 191 | local to, tr = pcall(function() return mt.__tostring(t) end) 192 | local so, sr = pcall(function() return mt.__serialize(t) end) 193 | if (to or so) then -- knows how to serialize itself 194 | seen[t] = insref or spath 195 | t = so and sr or tr 196 | ttype = type(t) 197 | end -- new value falls through to be serialized 198 | end 199 | if ttype == "table" then 200 | if level >= maxl then return tag..'{}'..comment('maxlvl', level) end 201 | seen[t] = insref or spath 202 | if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty 203 | if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end 204 | local maxn, o, out = math.min(#t, maxnum or #t), {}, {} 205 | for key = 1, maxn do o[key] = key end 206 | if not maxnum or #o < maxnum then 207 | local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables 208 | for key in pairs(t) do if o[key] ~= key then n = n + 1; o[n] = key end end end 209 | if maxnum and #o > maxnum then o[maxnum+1] = nil end 210 | if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end 211 | local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) 212 | for n, key in ipairs(o) do 213 | local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse 214 | if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing 215 | or opts.keyallow and not opts.keyallow[key] 216 | or opts.keyignore and opts.keyignore[key] 217 | or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types 218 | or sparse and value == nil then -- skipping nils; do nothing 219 | elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then 220 | if not seen[key] and not globals[key] then 221 | sref[#sref+1] = 'placeholder' 222 | local sname = safename(iname, gensym(key)) -- iname is table for local variables 223 | sref[#sref] = val2str(key,sname,indent,sname,iname,true) end 224 | sref[#sref+1] = 'placeholder' 225 | local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' 226 | sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) 227 | else 228 | out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1) 229 | if maxlen then 230 | maxlen = maxlen - #out[#out] 231 | if maxlen < 0 then break end 232 | end 233 | end 234 | end 235 | local prefix = string.rep(indent or '', level) 236 | local head = indent and '{\n'..prefix..indent or '{' 237 | local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) 238 | local tail = indent and "\n"..prefix..'}' or '}' 239 | return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) 240 | elseif badtype[ttype] then 241 | seen[t] = insref or spath 242 | return tag..globerr(t, level) 243 | elseif ttype == 'function' then 244 | seen[t] = insref or spath 245 | if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end 246 | local ok, res = pcall(string.dump, t) 247 | local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) 248 | return tag..(func or globerr(t, level)) 249 | else return tag..safestr(t) end -- handle all other types 250 | end 251 | local sepr = indent and "\n" or ";"..space 252 | local body = val2str(t, name, indent) -- this call also populates sref 253 | local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' 254 | local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' 255 | return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" 256 | end 257 | 258 | local function deserialize(data, opts) 259 | local env = (opts and opts.safe == false) and G 260 | or setmetatable({}, { 261 | __index = function(t,k) return t end, 262 | __call = function(t,...) error("cannot call functions") end 263 | }) 264 | local f, res = (loadstring or load)('return '..data, nil, nil, env) 265 | if not f then f, res = (loadstring or load)(data, nil, nil, env) end 266 | if not f then return f, res end 267 | if setfenv then setfenv(f, env) end 268 | return pcall(f) 269 | end 270 | 271 | local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end 272 | return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, 273 | load = deserialize, 274 | dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, 275 | line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, 276 | block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } 277 | end)() ---- end of Serpent module 278 | 279 | mobdebug.line = serpent.line 280 | mobdebug.dump = serpent.dump 281 | mobdebug.linemap = nil 282 | mobdebug.loadstring = loadstring 283 | 284 | local function removebasedir(path, basedir) 285 | if iscasepreserving then 286 | -- check if the lowercased path matches the basedir 287 | -- if so, return substring of the original path (to not lowercase it) 288 | return path:lower():find('^'..q(basedir:lower())) 289 | and path:sub(#basedir+1) or path 290 | else 291 | return string.gsub(path, '^'..q(basedir), '') 292 | end 293 | end 294 | 295 | local function stack(start) 296 | local function vars(f) 297 | local func = debug.getinfo(f, "f").func 298 | local i = 1 299 | local locals = {} 300 | -- get locals 301 | while true do 302 | local name, value = debug.getlocal(f, i) 303 | if not name then break end 304 | if string.sub(name, 1, 1) ~= '(' then 305 | locals[name] = {value, select(2,pcall(tostring,value))} 306 | end 307 | i = i + 1 308 | end 309 | -- get varargs (these use negative indices) 310 | i = 1 311 | while true do 312 | local name, value = debug.getlocal(f, -i) 313 | if not name then break end 314 | locals[name:gsub("%)$"," "..i..")")] = {value, select(2,pcall(tostring,value))} 315 | i = i + 1 316 | end 317 | -- get upvalues 318 | i = 1 319 | local ups = {} 320 | while func do -- check for func as it may be nil for tail calls 321 | local name, value = debug.getupvalue(func, i) 322 | if not name then break end 323 | ups[name] = {value, select(2,pcall(tostring,value))} 324 | i = i + 1 325 | end 326 | return locals, ups 327 | end 328 | 329 | local stack = {} 330 | local linemap = mobdebug.linemap 331 | for i = (start or 0), 100 do 332 | local source = debug.getinfo(i, "Snl") 333 | if not source then break end 334 | 335 | local src = source.source 336 | if src:find("@") == 1 then 337 | src = src:sub(2):gsub("\\", "/") 338 | if src:find("%./") == 1 then src = src:sub(3) end 339 | end 340 | 341 | table.insert(stack, { -- remove basedir from source 342 | {source.name, removebasedir(src, basedir), 343 | linemap and linemap(source.linedefined, source.source) or source.linedefined, 344 | linemap and linemap(source.currentline, source.source) or source.currentline, 345 | source.what, source.namewhat, source.short_src}, 346 | vars(i+1)}) 347 | end 348 | return stack 349 | end 350 | 351 | local function set_breakpoint(file, line) 352 | if file == '-' and lastfile then file = lastfile 353 | elseif iscasepreserving then file = string.lower(file) end 354 | if not breakpoints[line] then breakpoints[line] = {} end 355 | breakpoints[line][file] = true 356 | end 357 | 358 | local function remove_breakpoint(file, line) 359 | if file == '-' and lastfile then file = lastfile 360 | elseif file == '*' and line == 0 then breakpoints = {} 361 | elseif iscasepreserving then file = string.lower(file) end 362 | if breakpoints[line] then breakpoints[line][file] = nil end 363 | end 364 | 365 | local function has_breakpoint(file, line) 366 | return breakpoints[line] 367 | and breakpoints[line][iscasepreserving and string.lower(file) or file] 368 | end 369 | 370 | local function restore_vars(vars) 371 | if type(vars) ~= 'table' then return end 372 | 373 | -- locals need to be processed in the reverse order, starting from 374 | -- the inner block out, to make sure that the localized variables 375 | -- are correctly updated with only the closest variable with 376 | -- the same name being changed 377 | -- first loop find how many local variables there is, while 378 | -- the second loop processes them from i to 1 379 | local i = 1 380 | while true do 381 | local name = debug.getlocal(3, i) 382 | if not name then break end 383 | i = i + 1 384 | end 385 | i = i - 1 386 | local written_vars = {} 387 | while i > 0 do 388 | local name = debug.getlocal(3, i) 389 | if not written_vars[name] then 390 | if string.sub(name, 1, 1) ~= '(' then 391 | debug.setlocal(3, i, rawget(vars, name)) 392 | end 393 | written_vars[name] = true 394 | end 395 | i = i - 1 396 | end 397 | 398 | i = 1 399 | local func = debug.getinfo(3, "f").func 400 | while true do 401 | local name = debug.getupvalue(func, i) 402 | if not name then break end 403 | if not written_vars[name] then 404 | if string.sub(name, 1, 1) ~= '(' then 405 | debug.setupvalue(func, i, rawget(vars, name)) 406 | end 407 | written_vars[name] = true 408 | end 409 | i = i + 1 410 | end 411 | end 412 | 413 | local function capture_vars(level, thread) 414 | level = (level or 0)+2 -- add two levels for this and debug calls 415 | local func = (thread and debug.getinfo(thread, level, "f") or debug.getinfo(level, "f") or {}).func 416 | if not func then return {} end 417 | 418 | local vars = {['...'] = {}} 419 | local i = 1 420 | while true do 421 | local name, value = debug.getupvalue(func, i) 422 | if not name then break end 423 | if string.sub(name, 1, 1) ~= '(' then vars[name] = value end 424 | i = i + 1 425 | end 426 | i = 1 427 | while true do 428 | local name, value 429 | if thread then 430 | name, value = debug.getlocal(thread, level, i) 431 | else 432 | name, value = debug.getlocal(level, i) 433 | end 434 | if not name then break end 435 | if string.sub(name, 1, 1) ~= '(' then vars[name] = value end 436 | i = i + 1 437 | end 438 | -- get varargs (these use negative indices) 439 | i = 1 440 | while true do 441 | local name, value 442 | if thread then 443 | name, value = debug.getlocal(thread, level, -i) 444 | else 445 | name, value = debug.getlocal(level, -i) 446 | end 447 | if not name then break end 448 | vars['...'][i] = value 449 | i = i + 1 450 | end 451 | -- returned 'vars' table plays a dual role: (1) it captures local values 452 | -- and upvalues to be restored later (in case they are modified in "eval"), 453 | -- and (2) it provides an environment for evaluated chunks. 454 | -- getfenv(func) is needed to provide proper environment for functions, 455 | -- including access to globals, but this causes vars[name] to fail in 456 | -- restore_vars on local variables or upvalues with `nil` values when 457 | -- 'strict' is in effect. To avoid this `rawget` is used in restore_vars. 458 | setmetatable(vars, { __index = getfenv(func), __newindex = getfenv(func), __mode = "v" }) 459 | return vars 460 | end 461 | 462 | local function stack_depth(start_depth) 463 | for i = start_depth, 0, -1 do 464 | if debug.getinfo(i, "l") then return i+1 end 465 | end 466 | return start_depth 467 | end 468 | 469 | local function is_safe(stack_level) 470 | -- the stack grows up: 0 is getinfo, 1 is is_safe, 2 is debug_hook, 3 is user function 471 | if stack_level == 3 then return true end 472 | for i = 3, stack_level do 473 | -- return if it is not safe to abort 474 | local info = debug.getinfo(i, "S") 475 | if not info then return true end 476 | if info.what == "C" then return false end 477 | end 478 | return true 479 | end 480 | 481 | local function in_debugger() 482 | local this = debug.getinfo(1, "S").source 483 | -- only need to check few frames as mobdebug frames should be close 484 | for i = 3, 7 do 485 | local info = debug.getinfo(i, "S") 486 | if not info then return false end 487 | if info.source == this then return true end 488 | end 489 | return false 490 | end 491 | 492 | local function is_pending(peer) 493 | -- if there is something already in the buffer, skip check 494 | if not buf and checkcount >= mobdebug.checkcount then 495 | peer:settimeout(0) -- non-blocking 496 | buf = peer:receive(1) 497 | peer:settimeout() -- back to blocking 498 | checkcount = 0 499 | end 500 | return buf 501 | end 502 | 503 | local function readnext(peer, num) 504 | peer:settimeout(0) -- non-blocking 505 | local res, err, partial = peer:receive(num) 506 | peer:settimeout() -- back to blocking 507 | return res or partial or '', err 508 | end 509 | 510 | local function handle_breakpoint(peer) 511 | -- check if the buffer has the beginning of SETB/DELB command; 512 | -- this is to avoid reading the entire line for commands that 513 | -- don't need to be handled here. 514 | if not buf or not (buf:sub(1,1) == 'S' or buf:sub(1,1) == 'D') then return end 515 | 516 | -- check second character to avoid reading STEP or other S* and D* commands 517 | if #buf == 1 then buf = buf .. readnext(peer, 1) end 518 | if buf:sub(2,2) ~= 'E' then return end 519 | 520 | -- need to read few more characters 521 | buf = buf .. readnext(peer, 5-#buf) 522 | if buf ~= 'SETB ' and buf ~= 'DELB ' then return end 523 | 524 | local res, _, partial = peer:receive("*l") -- get the rest of the line; blocking 525 | if not res then 526 | if partial then buf = buf .. partial end 527 | return 528 | end 529 | 530 | local _, _, cmd, file, line = (buf..res):find("^([A-Z]+)%s+(.-)%s+(%d+)%s*$") 531 | if cmd == 'SETB' then set_breakpoint(file, tonumber(line)) 532 | elseif cmd == 'DELB' then remove_breakpoint(file, tonumber(line)) 533 | else 534 | -- this looks like a breakpoint command, but something went wrong; 535 | -- return here to let the "normal" processing to handle, 536 | -- although this is likely to not go well. 537 | return 538 | end 539 | 540 | buf = nil 541 | end 542 | 543 | local function normalize_path(file) 544 | local n 545 | repeat 546 | file, n = file:gsub("/+%.?/+","/") -- remove all `//` and `/./` references 547 | until n == 0 548 | -- collapse all up-dir references: this will clobber UNC prefix (\\?\) 549 | -- and disk on Windows when there are too many up-dir references: `D:\foo\..\..\bar`; 550 | -- handle the case of multiple up-dir references: `foo/bar/baz/../../../more`; 551 | -- only remove one at a time as otherwise `../../` could be removed; 552 | repeat 553 | file, n = file:gsub("[^/]+/%.%./", "", 1) 554 | until n == 0 555 | -- there may still be a leading up-dir reference left (as `/../` or `../`); remove it 556 | return (file:gsub("^(/?)%.%./", "%1")) 557 | end 558 | 559 | local function debug_hook(event, line) 560 | -- (1) LuaJIT needs special treatment. Because debug_hook is set for 561 | -- *all* coroutines, and not just the one being debugged as in regular Lua 562 | -- (http://lua-users.org/lists/lua-l/2011-06/msg00513.html), 563 | -- need to avoid debugging mobdebug's own code as LuaJIT doesn't 564 | -- always correctly generate call/return hook events (there are more 565 | -- calls than returns, which breaks stack depth calculation and 566 | -- 'step' and 'step over' commands stop working; possibly because 567 | -- 'tail return' events are not generated by LuaJIT). 568 | -- the next line checks if the debugger is run under LuaJIT and if 569 | -- one of debugger methods is present in the stack, it simply returns. 570 | -- ngx_lua/Openresty requires a slightly different handling, as it 571 | -- creates a coroutine wrapper, so this processing needs to be skipped. 572 | if jit and not (ngx and type(ngx) == "table" and ngx.say) then 573 | -- when luajit is compiled with LUAJIT_ENABLE_LUA52COMPAT, 574 | -- coroutine.running() returns non-nil for the main thread. 575 | local coro, main = coroutine.running() 576 | if not coro or main then coro = 'main' end 577 | local disabled = coroutines[coro] == false 578 | or coroutines[coro] == nil and coro ~= (coro_debugee or 'main') 579 | if coro_debugee and disabled or not coro_debugee and (disabled or in_debugger()) then 580 | return 581 | end 582 | end 583 | 584 | -- (2) check if abort has been requested and it's safe to abort 585 | if abort and is_safe(stack_level) then error(abort) end 586 | 587 | -- (3) also check if this debug hook has not been visited for any reason. 588 | -- this check is needed to avoid stepping in too early 589 | -- (for example, when coroutine.resume() is executed inside start()). 590 | if not seen_hook and in_debugger() then return end 591 | 592 | if event == "call" then 593 | stack_level = stack_level + 1 594 | elseif event == "return" or event == "tail return" then 595 | stack_level = stack_level - 1 596 | elseif event == "line" then 597 | if mobdebug.linemap then 598 | local ok, mappedline = pcall(mobdebug.linemap, line, debug.getinfo(2, "S").source) 599 | if ok then line = mappedline end 600 | if not line then return end 601 | end 602 | 603 | -- may need to fall through because of the following: 604 | -- (1) step_into 605 | -- (2) step_over and stack_level <= step_level (need stack_level) 606 | -- (3) breakpoint; check for line first as it's known; then for file 607 | -- (4) socket call (only do every Xth check) 608 | -- (5) at least one watch is registered 609 | if not ( 610 | step_into or step_over or breakpoints[line] or watchescnt > 0 611 | or is_pending(server) 612 | ) then checkcount = checkcount + 1; return end 613 | 614 | checkcount = mobdebug.checkcount -- force check on the next command 615 | 616 | -- this is needed to check if the stack got shorter or longer. 617 | -- unfortunately counting call/return calls is not reliable. 618 | -- the discrepancy may happen when "pcall(load, '')" call is made 619 | -- or when "error()" is called in a function. 620 | -- in either case there are more "call" than "return" events reported. 621 | -- this validation is done for every "line" event, but should be "cheap" 622 | -- as it checks for the stack to get shorter (or longer by one call). 623 | -- start from one level higher just in case we need to grow the stack. 624 | -- this may happen after coroutine.resume call to a function that doesn't 625 | -- have any other instructions to execute. it triggers three returns: 626 | -- "return, tail return, return", which needs to be accounted for. 627 | stack_level = stack_depth(stack_level+1) 628 | 629 | local caller = debug.getinfo(2, "S") 630 | 631 | -- grab the filename and fix it if needed 632 | local file = lastfile 633 | if (lastsource ~= caller.source) then 634 | file, lastsource = caller.source, caller.source 635 | -- technically, users can supply names that may not use '@', 636 | -- for example when they call loadstring('...', 'filename.lua'). 637 | -- Unfortunately, there is no reliable/quick way to figure out 638 | -- what is the filename and what is the source code. 639 | -- If the name doesn't start with `@`, assume it's a file name if it's all on one line. 640 | if find(file, "^@") or not find(file, "[\r\n]") then 641 | file = gsub(gsub(file, "^@", ""), "\\", "/") 642 | -- normalize paths that may include up-dir or same-dir references 643 | -- if the path starts from the up-dir or reference, 644 | -- prepend `basedir` to generate absolute path to keep breakpoints working. 645 | -- ignore qualified relative path (`D:../`) and UNC paths (`\\?\`) 646 | if find(file, "^%.%./") then file = basedir..file end 647 | if find(file, "/%.%.?/") then file = normalize_path(file) end 648 | -- need this conversion to be applied to relative and absolute 649 | -- file names as you may write "require 'Foo'" to 650 | -- load "foo.lua" (on a case insensitive file system) and breakpoints 651 | -- set on foo.lua will not work if not converted to the same case. 652 | if iscasepreserving then file = string.lower(file) end 653 | if find(file, "^%./") then file = sub(file, 3) end 654 | -- remove basedir, so that breakpoints are checked properly 655 | file = gsub(file, "^"..q(basedir), "") 656 | -- some file systems allow newlines in file names; remove these. 657 | file = gsub(file, "\n", ' ') 658 | else 659 | file = mobdebug.line(file) 660 | end 661 | 662 | -- set to true if we got here; this only needs to be done once per 663 | -- session, so do it here to at least avoid setting it for every line. 664 | seen_hook = true 665 | lastfile = file 666 | end 667 | 668 | if is_pending(server) then handle_breakpoint(server) end 669 | 670 | local vars, status, res 671 | if (watchescnt > 0) then 672 | vars = capture_vars(1) 673 | for index, value in pairs(watches) do 674 | setfenv(value, vars) 675 | local ok, fired = pcall(value) 676 | if ok and fired then 677 | status, res = cororesume(coro_debugger, events.WATCH, vars, file, line, index) 678 | break -- any one watch is enough; don't check multiple times 679 | end 680 | end 681 | end 682 | 683 | -- need to get into the "regular" debug handler, but only if there was 684 | -- no watch that was fired. If there was a watch, handle its result. 685 | local getin = (status == nil) and 686 | (step_into 687 | -- when coroutine.running() return `nil` (main thread in Lua 5.1), 688 | -- step_over will equal 'main', so need to check for that explicitly. 689 | or (step_over and step_over == (coroutine.running() or 'main') and stack_level <= step_level) 690 | or has_breakpoint(file, line) 691 | or is_pending(server)) 692 | 693 | if getin then 694 | vars = vars or capture_vars(1) 695 | step_into = false 696 | step_over = false 697 | status, res = cororesume(coro_debugger, events.BREAK, vars, file, line) 698 | end 699 | 700 | -- handle 'stack' command that provides stack() information to the debugger 701 | while status and res == 'stack' do 702 | -- resume with the stack trace and variables 703 | if vars then restore_vars(vars) end -- restore vars so they are reflected in stack values 704 | status, res = cororesume(coro_debugger, events.STACK, stack(3), file, line) 705 | end 706 | 707 | -- need to recheck once more as resume after 'stack' command may 708 | -- return something else (for example, 'exit'), which needs to be handled 709 | if status and res and res ~= 'stack' then 710 | if not abort and res == "exit" then mobdebug.onexit(1, true); return end 711 | if not abort and res == "done" then mobdebug.done(); return end 712 | abort = res 713 | -- only abort if safe; if not, there is another (earlier) check inside 714 | -- debug_hook, which will abort execution at the first safe opportunity 715 | if is_safe(stack_level) then error(abort) end 716 | elseif not status and res then 717 | error(res, 2) -- report any other (internal) errors back to the application 718 | end 719 | 720 | if vars then restore_vars(vars) end 721 | 722 | -- last command requested Step Over/Out; store the current thread 723 | if step_over == true then step_over = coroutine.running() or 'main' end 724 | end 725 | end 726 | 727 | local function stringify_results(params, status, ...) 728 | if not status then return status, ... end -- on error report as it 729 | 730 | params = params or {} 731 | if params.nocode == nil then params.nocode = true end 732 | if params.comment == nil then params.comment = 1 end 733 | 734 | local t = {} 735 | for i = 1, select('#', ...) do -- stringify each of the returned values 736 | local ok, res = pcall(mobdebug.line, select(i, ...), params) 737 | t[i] = ok and res or ("%q"):format(res):gsub("\010","n"):gsub("\026","\\026") 738 | end 739 | -- stringify table with all returned values 740 | -- this is done to allow each returned value to be used (serialized or not) 741 | -- intependently and to preserve "original" comments 742 | return pcall(mobdebug.dump, t, {sparse = false}) 743 | end 744 | 745 | local function isrunning() 746 | return coro_debugger and (corostatus(coro_debugger) == 'suspended' or corostatus(coro_debugger) == 'running') 747 | end 748 | 749 | -- this is a function that removes all hooks and closes the socket to 750 | -- report back to the controller that the debugging is done. 751 | -- the script that called `done` can still continue. 752 | local function done() 753 | if not (isrunning() and server) then return end 754 | 755 | if not jit then 756 | for co, debugged in pairs(coroutines) do 757 | if debugged then debug.sethook(co) end 758 | end 759 | end 760 | 761 | debug.sethook() 762 | server:close() 763 | 764 | coro_debugger = nil -- to make sure isrunning() returns `false` 765 | seen_hook = nil -- to make sure that the next start() call works 766 | abort = nil -- to make sure that callback calls use proper "abort" value 767 | basedir = "" -- to reset basedir in case the same module/state is reused 768 | end 769 | 770 | local function debugger_loop(sev, svars, sfile, sline) 771 | local command 772 | local eval_env = svars or {} 773 | local function emptyWatch () return false end 774 | local loaded = {} 775 | for k in pairs(package.loaded) do loaded[k] = true end 776 | 777 | while true do 778 | local line, err 779 | if mobdebug.yield and server.settimeout then server:settimeout(mobdebug.yieldtimeout) end 780 | while true do 781 | line, err = server:receive("*l") 782 | if not line then 783 | if err == "timeout" then 784 | if mobdebug.yield then mobdebug.yield() end 785 | elseif err == "closed" then 786 | error("Debugger connection closed", 0) 787 | else 788 | error(("Unexpected socket error: %s"):format(err), 0) 789 | end 790 | else 791 | -- if there is something in the pending buffer, prepend it to the line 792 | if buf then line = buf .. line; buf = nil end 793 | break 794 | end 795 | end 796 | if server.settimeout then server:settimeout() end -- back to blocking 797 | command = string.sub(line, string.find(line, "^[A-Z]+")) 798 | if command == "SETB" then 799 | local _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$") 800 | if file and line then 801 | set_breakpoint(file, tonumber(line)) 802 | server:send("200 OK\n") 803 | else 804 | server:send("400 Bad Request\n") 805 | end 806 | elseif command == "DELB" then 807 | local _, _, _, file, line = string.find(line, "^([A-Z]+)%s+(.-)%s+(%d+)%s*$") 808 | if file and line then 809 | remove_breakpoint(file, tonumber(line)) 810 | server:send("200 OK\n") 811 | else 812 | server:send("400 Bad Request\n") 813 | end 814 | elseif command == "EXEC" then 815 | -- extract any optional parameters 816 | local params = string.match(line, "--%s*(%b{})%s*$") 817 | local _, _, chunk = string.find(line, "^[A-Z]+%s+(.+)$") 818 | if chunk then 819 | local func, res = mobdebug.loadstring(chunk) 820 | local status 821 | if func then 822 | local pfunc = params and loadstring("return "..params) -- use internal function 823 | params = pfunc and pfunc() 824 | params = (type(params) == "table" and params or {}) 825 | local stack = tonumber(params.stack) 826 | -- if the requested stack frame is not the current one, then use a new capture 827 | -- with a specific stack frame: `capture_vars(0, coro_debugee)` 828 | local env = stack and coro_debugee and capture_vars(stack-1, coro_debugee) or eval_env 829 | setfenv(func, env) 830 | status, res = stringify_results(params, pcall(func, unpack(rawget(env,'...') or {}))) 831 | end 832 | if status then 833 | if mobdebug.onscratch then mobdebug.onscratch(res) end 834 | server:send("200 OK " .. tostring(#res) .. "\n") 835 | server:send(res) 836 | else 837 | -- fix error if not set (for example, when loadstring is not present) 838 | if not res then res = "Unknown error" end 839 | server:send("401 Error in Expression " .. tostring(#res) .. "\n") 840 | server:send(res) 841 | end 842 | else 843 | server:send("400 Bad Request\n") 844 | end 845 | elseif command == "LOAD" then 846 | local _, _, size, name = string.find(line, "^[A-Z]+%s+(%d+)%s+(%S.-)%s*$") 847 | size = tonumber(size) 848 | 849 | if abort == nil then -- no LOAD/RELOAD allowed inside start() 850 | if size > 0 then server:receive(size) end 851 | if sfile and sline then 852 | server:send("201 Started " .. sfile .. " " .. tostring(sline) .. "\n") 853 | else 854 | server:send("200 OK 0\n") 855 | end 856 | else 857 | -- reset environment to allow required modules to load again 858 | -- remove those packages that weren't loaded when debugger started 859 | for k in pairs(package.loaded) do 860 | if not loaded[k] then package.loaded[k] = nil end 861 | end 862 | 863 | if size == 0 and name == '-' then -- RELOAD the current script being debugged 864 | server:send("200 OK 0\n") 865 | coroyield("load") 866 | else 867 | -- receiving 0 bytes blocks (at least in luasocket 2.0.2), so skip reading 868 | local chunk = size == 0 and "" or server:receive(size) 869 | if chunk then -- LOAD a new script for debugging 870 | local func, res = mobdebug.loadstring(chunk, "@"..name) 871 | if func then 872 | server:send("200 OK 0\n") 873 | debugee = func 874 | coroyield("load") 875 | else 876 | server:send("401 Error in Expression " .. tostring(#res) .. "\n") 877 | server:send(res) 878 | end 879 | else 880 | server:send("400 Bad Request\n") 881 | end 882 | end 883 | end 884 | elseif command == "SETW" then 885 | local _, _, exp = string.find(line, "^[A-Z]+%s+(.+)%s*$") 886 | if exp then 887 | local func, res = mobdebug.loadstring("return(" .. exp .. ")") 888 | if func then 889 | watchescnt = watchescnt + 1 890 | local newidx = #watches + 1 891 | watches[newidx] = func 892 | server:send("200 OK " .. tostring(newidx) .. "\n") 893 | else 894 | server:send("401 Error in Expression " .. tostring(#res) .. "\n") 895 | server:send(res) 896 | end 897 | else 898 | server:send("400 Bad Request\n") 899 | end 900 | elseif command == "DELW" then 901 | local _, _, index = string.find(line, "^[A-Z]+%s+(%d+)%s*$") 902 | index = tonumber(index) 903 | if index > 0 and index <= #watches then 904 | watchescnt = watchescnt - (watches[index] ~= emptyWatch and 1 or 0) 905 | watches[index] = emptyWatch 906 | server:send("200 OK\n") 907 | else 908 | server:send("400 Bad Request\n") 909 | end 910 | elseif command == "RUN" then 911 | server:send("200 OK\n") 912 | 913 | local ev, vars, file, line, idx_watch = coroyield() 914 | eval_env = vars 915 | if ev == events.BREAK then 916 | server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n") 917 | elseif ev == events.WATCH then 918 | server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n") 919 | elseif ev == events.RESTART then 920 | -- nothing to do 921 | else 922 | server:send("401 Error in Execution " .. tostring(#file) .. "\n") 923 | server:send(file) 924 | end 925 | elseif command == "STEP" then 926 | server:send("200 OK\n") 927 | step_into = true 928 | 929 | local ev, vars, file, line, idx_watch = coroyield() 930 | eval_env = vars 931 | if ev == events.BREAK then 932 | server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n") 933 | elseif ev == events.WATCH then 934 | server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n") 935 | elseif ev == events.RESTART then 936 | -- nothing to do 937 | else 938 | server:send("401 Error in Execution " .. tostring(#file) .. "\n") 939 | server:send(file) 940 | end 941 | elseif command == "OVER" or command == "OUT" then 942 | server:send("200 OK\n") 943 | step_over = true 944 | 945 | -- OVER and OUT are very similar except for 946 | -- the stack level value at which to stop 947 | if command == "OUT" then step_level = stack_level - 1 948 | else step_level = stack_level end 949 | 950 | local ev, vars, file, line, idx_watch = coroyield() 951 | eval_env = vars 952 | if ev == events.BREAK then 953 | server:send("202 Paused " .. file .. " " .. tostring(line) .. "\n") 954 | elseif ev == events.WATCH then 955 | server:send("203 Paused " .. file .. " " .. tostring(line) .. " " .. tostring(idx_watch) .. "\n") 956 | elseif ev == events.RESTART then 957 | -- nothing to do 958 | else 959 | server:send("401 Error in Execution " .. tostring(#file) .. "\n") 960 | server:send(file) 961 | end 962 | elseif command == "BASEDIR" then 963 | local _, _, dir = string.find(line, "^[A-Z]+%s+(.+)%s*$") 964 | if dir then 965 | basedir = iscasepreserving and string.lower(dir) or dir 966 | -- reset cached source as it may change with basedir 967 | lastsource = nil 968 | server:send("200 OK\n") 969 | else 970 | server:send("400 Bad Request\n") 971 | end 972 | elseif command == "SUSPEND" then 973 | -- do nothing; it already fulfilled its role 974 | elseif command == "DONE" then 975 | coroyield("done") 976 | return -- done with all the debugging 977 | elseif command == "STACK" then 978 | -- first check if we can execute the stack command 979 | -- as it requires yielding back to debug_hook it cannot be executed 980 | -- if we have not seen the hook yet as happens after start(). 981 | -- in this case we simply return an empty result 982 | local vars, ev = {} 983 | if seen_hook then 984 | ev, vars = coroyield("stack") 985 | end 986 | if ev and ev ~= events.STACK then 987 | server:send("401 Error in Execution " .. tostring(#vars) .. "\n") 988 | server:send(vars) 989 | else 990 | local params = string.match(line, "--%s*(%b{})%s*$") 991 | local pfunc = params and loadstring("return "..params) -- use internal function 992 | params = pfunc and pfunc() 993 | params = (type(params) == "table" and params or {}) 994 | if params.nocode == nil then params.nocode = true end 995 | if params.sparse == nil then params.sparse = false end 996 | -- take into account additional levels for the stack frames and data management 997 | if tonumber(params.maxlevel) then params.maxlevel = tonumber(params.maxlevel)+4 end 998 | 999 | local ok, res = pcall(mobdebug.dump, vars, params) 1000 | if ok then 1001 | server:send("200 OK " .. tostring(res) .. "\n") 1002 | else 1003 | server:send("401 Error in Execution " .. tostring(#res) .. "\n") 1004 | server:send(res) 1005 | end 1006 | end 1007 | elseif command == "OUTPUT" then 1008 | local _, _, stream, mode = string.find(line, "^[A-Z]+%s+(%w+)%s+([dcr])%s*$") 1009 | if stream and mode and stream == "stdout" then 1010 | -- assign "print" in the global environment 1011 | local default = mode == 'd' 1012 | genv.print = default and iobase.print or corowrap(function() 1013 | -- wrapping into coroutine.wrap protects this function from 1014 | -- being stepped through in the debugger. 1015 | -- don't use vararg (...) as it adds a reference for its values, 1016 | -- which may affect how they are garbage collected 1017 | while true do 1018 | local tbl = {coroutine.yield()} 1019 | if mode == 'c' then iobase.print(unpack(tbl)) end 1020 | for n = 1, #tbl do 1021 | tbl[n] = select(2, pcall(mobdebug.line, tbl[n], {nocode = true, comment = false})) end 1022 | local file = table.concat(tbl, "\t").."\n" 1023 | server:send("204 Output " .. stream .. " " .. tostring(#file) .. "\n" .. file) 1024 | end 1025 | end) 1026 | if not default then genv.print() end -- "fake" print to start printing loop 1027 | server:send("200 OK\n") 1028 | else 1029 | server:send("400 Bad Request\n") 1030 | end 1031 | elseif command == "EXIT" then 1032 | server:send("200 OK\n") 1033 | coroyield("exit") 1034 | else 1035 | server:send("400 Bad Request\n") 1036 | end 1037 | end 1038 | end 1039 | 1040 | local function output(stream, data) 1041 | if server then return server:send("204 Output "..stream.." "..tostring(#data).."\n"..data) end 1042 | end 1043 | 1044 | local function connect(controller_host, controller_port) 1045 | local sock, err = socket.tcp() 1046 | if not sock then return nil, err end 1047 | 1048 | if sock.settimeout then sock:settimeout(mobdebug.connecttimeout) end 1049 | local res, err = sock:connect(controller_host, tostring(controller_port)) 1050 | if sock.settimeout then sock:settimeout() end 1051 | 1052 | if not res then return nil, err end 1053 | return sock 1054 | end 1055 | 1056 | local lasthost, lastport 1057 | 1058 | -- Starts a debug session by connecting to a controller 1059 | local function start(controller_host, controller_port) 1060 | -- only one debugging session can be run (as there is only one debug hook) 1061 | if isrunning() then return end 1062 | 1063 | lasthost = controller_host or lasthost 1064 | lastport = controller_port or lastport 1065 | 1066 | controller_host = lasthost or "localhost" 1067 | controller_port = lastport or mobdebug.port 1068 | 1069 | local err 1070 | server, err = mobdebug.connect(controller_host, controller_port) 1071 | if server then 1072 | -- correct stack depth which already has some calls on it 1073 | -- so it doesn't go into negative when those calls return 1074 | -- as this breaks subsequence checks in stack_depth(). 1075 | -- start from 16th frame, which is sufficiently large for this check. 1076 | stack_level = stack_depth(16) 1077 | 1078 | coro_debugger = corocreate(debugger_loop) 1079 | debug.sethook(debug_hook, HOOKMASK) 1080 | seen_hook = nil -- reset in case the last start() call was refused 1081 | step_into = true -- start with step command 1082 | return true 1083 | else 1084 | print(("Could not connect to %s:%s: %s") 1085 | :format(controller_host, controller_port, err or "unknown error")) 1086 | end 1087 | end 1088 | 1089 | local function controller(controller_host, controller_port, scratchpad) 1090 | -- only one debugging session can be run (as there is only one debug hook) 1091 | if isrunning() then return end 1092 | 1093 | lasthost = controller_host or lasthost 1094 | lastport = controller_port or lastport 1095 | 1096 | controller_host = lasthost or "localhost" 1097 | controller_port = lastport or mobdebug.port 1098 | 1099 | local exitonerror = not scratchpad 1100 | local err 1101 | server, err = mobdebug.connect(controller_host, controller_port) 1102 | if server then 1103 | local function report(trace, err) 1104 | local msg = err .. "\n" .. trace 1105 | server:send("401 Error in Execution " .. tostring(#msg) .. "\n") 1106 | server:send(msg) 1107 | return err 1108 | end 1109 | 1110 | seen_hook = true -- allow to accept all commands 1111 | coro_debugger = corocreate(debugger_loop) 1112 | 1113 | while true do 1114 | step_into = true -- start with step command 1115 | abort = false -- reset abort flag from the previous loop 1116 | if scratchpad then checkcount = mobdebug.checkcount end -- force suspend right away 1117 | 1118 | coro_debugee = corocreate(debugee) 1119 | debug.sethook(coro_debugee, debug_hook, HOOKMASK) 1120 | local status, err = cororesume(coro_debugee, unpack(arg or {})) 1121 | 1122 | -- was there an error or is the script done? 1123 | -- 'abort' state is allowed here; ignore it 1124 | if abort then 1125 | if tostring(abort) == 'exit' then break end 1126 | else 1127 | if status then -- no errors 1128 | if corostatus(coro_debugee) == "suspended" then 1129 | -- the script called `coroutine.yield` in the "main" thread 1130 | error("attempt to yield from the main thread", 3) 1131 | end 1132 | break -- normal execution is done 1133 | elseif err and not string.find(tostring(err), deferror) then 1134 | -- report the error back 1135 | -- err is not necessarily a string, so convert to string to report 1136 | report(debug.traceback(coro_debugee), tostring(err)) 1137 | if exitonerror then break end 1138 | -- check if the debugging is done (coro_debugger is nil) 1139 | if not coro_debugger then break end 1140 | -- resume once more to clear the response the debugger wants to send 1141 | -- need to use capture_vars(0) to capture only two (default) levels, 1142 | -- as even though there is controller() call, because of the tail call, 1143 | -- the caller may not exist for it; 1144 | -- This is not entirely safe as the user may see the local 1145 | -- variable from console, but they will be reset anyway. 1146 | -- This functionality is used when scratchpad is paused to 1147 | -- gain access to remote console to modify global variables. 1148 | local status, err = cororesume(coro_debugger, events.RESTART, capture_vars(0)) 1149 | if not status or status and err == "exit" then break end 1150 | end 1151 | end 1152 | end 1153 | else 1154 | print(("Could not connect to %s:%s: %s") 1155 | :format(controller_host, controller_port, err or "unknown error")) 1156 | return false 1157 | end 1158 | return true 1159 | end 1160 | 1161 | local function scratchpad(controller_host, controller_port) 1162 | return controller(controller_host, controller_port, true) 1163 | end 1164 | 1165 | local function loop(controller_host, controller_port) 1166 | return controller(controller_host, controller_port, false) 1167 | end 1168 | 1169 | local function on() 1170 | if not (isrunning() and server) then return end 1171 | 1172 | -- main is set to true under Lua5.2 for the "main" chunk. 1173 | -- Lua5.1 returns co as `nil` in that case. 1174 | local co, main = coroutine.running() 1175 | if main then co = nil end 1176 | if co then 1177 | coroutines[co] = true 1178 | debug.sethook(co, debug_hook, HOOKMASK) 1179 | else 1180 | if jit then coroutines.main = true end 1181 | debug.sethook(debug_hook, HOOKMASK) 1182 | end 1183 | end 1184 | 1185 | local function off() 1186 | if not (isrunning() and server) then return end 1187 | 1188 | -- main is set to true under Lua5.2 for the "main" chunk. 1189 | -- Lua5.1 returns co as `nil` in that case. 1190 | local co, main = coroutine.running() 1191 | if main then co = nil end 1192 | 1193 | -- don't remove coroutine hook under LuaJIT as there is only one (global) hook 1194 | if co then 1195 | coroutines[co] = false 1196 | if not jit then debug.sethook(co) end 1197 | else 1198 | if jit then coroutines.main = false end 1199 | if not jit then debug.sethook() end 1200 | end 1201 | 1202 | -- check if there is any thread that is still being debugged under LuaJIT; 1203 | -- if not, turn the debugging off 1204 | if jit then 1205 | local remove = true 1206 | for _, debugged in pairs(coroutines) do 1207 | if debugged then remove = false; break end 1208 | end 1209 | if remove then debug.sethook() end 1210 | end 1211 | end 1212 | 1213 | -- Handles server debugging commands 1214 | local function handle(params, client, options) 1215 | -- when `options.verbose` is not provided, use normal `print`; verbose output can be 1216 | -- disabled (`options.verbose == false`) or redirected (`options.verbose == function()...end`) 1217 | local verbose = not options or options.verbose ~= nil and options.verbose 1218 | local print = verbose and (type(verbose) == "function" and verbose or print) or function() end 1219 | local file, line, watch_idx 1220 | local _, _, command = string.find(params, "^([a-z]+)") 1221 | if command == "run" or command == "step" or command == "out" 1222 | or command == "over" or command == "exit" then 1223 | client:send(string.upper(command) .. "\n") 1224 | client:receive("*l") -- this should consume the first '200 OK' response 1225 | while true do 1226 | local done = true 1227 | local breakpoint = client:receive("*l") 1228 | if not breakpoint then 1229 | print("Program finished") 1230 | return nil, nil, false 1231 | end 1232 | local _, _, status = string.find(breakpoint, "^(%d+)") 1233 | if status == "200" then 1234 | -- don't need to do anything 1235 | elseif status == "202" then 1236 | _, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$") 1237 | if file and line then 1238 | print("Paused at file " .. file .. " line " .. line) 1239 | end 1240 | elseif status == "203" then 1241 | _, _, file, line, watch_idx = string.find(breakpoint, "^203 Paused%s+(.-)%s+(%d+)%s+(%d+)%s*$") 1242 | if file and line and watch_idx then 1243 | print("Paused at file " .. file .. " line " .. line .. " (watch expression " .. watch_idx .. ": [" .. watches[watch_idx] .. "])") 1244 | end 1245 | elseif status == "204" then 1246 | local _, _, stream, size = string.find(breakpoint, "^204 Output (%w+) (%d+)$") 1247 | if stream and size then 1248 | local size = tonumber(size) 1249 | local msg = size > 0 and client:receive(size) or "" 1250 | print(msg) 1251 | if outputs[stream] then outputs[stream](msg) end 1252 | -- this was just the output, so go back reading the response 1253 | done = false 1254 | end 1255 | elseif status == "401" then 1256 | local _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)$") 1257 | if size then 1258 | local msg = client:receive(tonumber(size)) 1259 | print("Error in remote application: " .. msg) 1260 | return nil, nil, msg 1261 | end 1262 | else 1263 | print("Unknown error") 1264 | return nil, nil, "Debugger error: unexpected response '" .. breakpoint .. "'" 1265 | end 1266 | if done then break end 1267 | end 1268 | elseif command == "done" then 1269 | client:send(string.upper(command) .. "\n") 1270 | -- no response is expected 1271 | elseif command == "setb" or command == "asetb" then 1272 | _, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$") 1273 | if file and line then 1274 | -- if this is a file name, and not a file source 1275 | if not file:find('^".*"$') then 1276 | file = string.gsub(file, "\\", "/") -- convert slash 1277 | file = removebasedir(file, basedir) 1278 | end 1279 | client:send("SETB " .. file .. " " .. line .. "\n") 1280 | if command == "asetb" or client:receive("*l") == "200 OK" then 1281 | set_breakpoint(file, line) 1282 | else 1283 | print("Error: breakpoint not inserted") 1284 | end 1285 | else 1286 | print("Invalid command") 1287 | end 1288 | elseif command == "setw" then 1289 | local _, _, exp = string.find(params, "^[a-z]+%s+(.+)$") 1290 | if exp then 1291 | client:send("SETW " .. exp .. "\n") 1292 | local answer = client:receive("*l") 1293 | local _, _, watch_idx = string.find(answer, "^200 OK (%d+)%s*$") 1294 | if watch_idx then 1295 | watches[watch_idx] = exp 1296 | print("Inserted watch exp no. " .. watch_idx) 1297 | else 1298 | local _, _, size = string.find(answer, "^401 Error in Expression (%d+)$") 1299 | if size then 1300 | local err = client:receive(tonumber(size)):gsub(".-:%d+:%s*","") 1301 | print("Error: watch expression not set: " .. err) 1302 | else 1303 | print("Error: watch expression not set") 1304 | end 1305 | end 1306 | else 1307 | print("Invalid command") 1308 | end 1309 | elseif command == "delb" or command == "adelb" then 1310 | _, _, _, file, line = string.find(params, "^([a-z]+)%s+(.-)%s+(%d+)%s*$") 1311 | if file and line then 1312 | -- if this is a file name, and not a file source 1313 | if not file:find('^".*"$') then 1314 | file = string.gsub(file, "\\", "/") -- convert slash 1315 | file = removebasedir(file, basedir) 1316 | end 1317 | client:send("DELB " .. file .. " " .. line .. "\n") 1318 | if command == "adelb" or client:receive("*l") == "200 OK" then 1319 | remove_breakpoint(file, line) 1320 | else 1321 | print("Error: breakpoint not removed") 1322 | end 1323 | else 1324 | print("Invalid command") 1325 | end 1326 | elseif command == "delallb" then 1327 | local file, line = "*", 0 1328 | client:send("DELB " .. file .. " " .. tostring(line) .. "\n") 1329 | if client:receive("*l") == "200 OK" then 1330 | remove_breakpoint(file, line) 1331 | else 1332 | print("Error: all breakpoints not removed") 1333 | end 1334 | elseif command == "delw" then 1335 | local _, _, index = string.find(params, "^[a-z]+%s+(%d+)%s*$") 1336 | if index then 1337 | client:send("DELW " .. index .. "\n") 1338 | if client:receive("*l") == "200 OK" then 1339 | watches[index] = nil 1340 | else 1341 | print("Error: watch expression not removed") 1342 | end 1343 | else 1344 | print("Invalid command") 1345 | end 1346 | elseif command == "delallw" then 1347 | for index, exp in pairs(watches) do 1348 | client:send("DELW " .. index .. "\n") 1349 | if client:receive("*l") == "200 OK" then 1350 | watches[index] = nil 1351 | else 1352 | print("Error: watch expression at index " .. index .. " [" .. exp .. "] not removed") 1353 | end 1354 | end 1355 | elseif command == "eval" or command == "exec" 1356 | or command == "load" or command == "loadstring" 1357 | or command == "reload" then 1358 | local _, _, exp = string.find(params, "^[a-z]+%s+(.+)$") 1359 | if exp or (command == "reload") then 1360 | if command == "eval" or command == "exec" then 1361 | exp = exp:gsub("\n", "\r") -- convert new lines, so the fragment can be passed as one line 1362 | if command == "eval" then exp = "return " .. exp end 1363 | client:send("EXEC " .. exp .. "\n") 1364 | elseif command == "reload" then 1365 | client:send("LOAD 0 -\n") 1366 | elseif command == "loadstring" then 1367 | local _, _, _, file, lines = string.find(exp, "^([\"'])(.-)%1%s(.+)") 1368 | if not file then 1369 | _, _, file, lines = string.find(exp, "^(%S+)%s(.+)") 1370 | end 1371 | client:send("LOAD " .. tostring(#lines) .. " " .. file .. "\n") 1372 | client:send(lines) 1373 | else 1374 | local file = io.open(exp, "r") 1375 | if not file and pcall(require, "winapi") then 1376 | -- if file is not open and winapi is there, try with a short path; 1377 | -- this may be needed for unicode paths on windows 1378 | winapi.set_encoding(winapi.CP_UTF8) 1379 | local shortp = winapi.short_path(exp) 1380 | file = shortp and io.open(shortp, "r") 1381 | end 1382 | if not file then return nil, nil, "Cannot open file " .. exp end 1383 | -- read the file and remove the shebang line as it causes a compilation error 1384 | local lines = file:read("*all"):gsub("^#!.-\n", "\n") 1385 | file:close() 1386 | 1387 | local fname = string.gsub(exp, "\\", "/") -- convert slash 1388 | fname = removebasedir(fname, basedir) 1389 | client:send("LOAD " .. tostring(#lines) .. " " .. fname .. "\n") 1390 | if #lines > 0 then client:send(lines) end 1391 | end 1392 | while true do 1393 | local params, err = client:receive("*l") 1394 | if not params then 1395 | return nil, nil, "Debugger connection " .. (err or "error") 1396 | end 1397 | local done = true 1398 | local _, _, status, len = string.find(params, "^(%d+).-%s+(%d+)%s*$") 1399 | if status == "200" then 1400 | len = tonumber(len) 1401 | if len > 0 then 1402 | local status, res 1403 | local str = client:receive(len) 1404 | -- handle serialized table with results 1405 | local func, err = loadstring(str) 1406 | if func then 1407 | status, res = pcall(func) 1408 | if not status then err = res 1409 | elseif type(res) ~= "table" then 1410 | err = "received "..type(res).." instead of expected 'table'" 1411 | end 1412 | end 1413 | if err then 1414 | print("Error in processing results: " .. err) 1415 | return nil, nil, "Error in processing results: " .. err 1416 | end 1417 | print(unpack(res)) 1418 | return res[1], res 1419 | end 1420 | elseif status == "201" then 1421 | _, _, file, line = string.find(params, "^201 Started%s+(.-)%s+(%d+)%s*$") 1422 | elseif status == "202" or params == "200 OK" then 1423 | -- do nothing; this only happens when RE/LOAD command gets the response 1424 | -- that was for the original command that was aborted 1425 | elseif status == "204" then 1426 | local _, _, stream, size = string.find(params, "^204 Output (%w+) (%d+)$") 1427 | if stream and size then 1428 | local size = tonumber(size) 1429 | local msg = size > 0 and client:receive(size) or "" 1430 | print(msg) 1431 | if outputs[stream] then outputs[stream](msg) end 1432 | -- this was just the output, so go back reading the response 1433 | done = false 1434 | end 1435 | elseif status == "401" then 1436 | len = tonumber(len) 1437 | local res = client:receive(len) 1438 | print("Error in expression: " .. res) 1439 | return nil, nil, res 1440 | else 1441 | print("Unknown error") 1442 | return nil, nil, "Debugger error: unexpected response after EXEC/LOAD '" .. params .. "'" 1443 | end 1444 | if done then break end 1445 | end 1446 | else 1447 | print("Invalid command") 1448 | end 1449 | elseif command == "listb" then 1450 | for l, v in pairs(breakpoints) do 1451 | for f in pairs(v) do 1452 | print(f .. ": " .. l) 1453 | end 1454 | end 1455 | elseif command == "listw" then 1456 | for i, v in pairs(watches) do 1457 | print("Watch exp. " .. i .. ": " .. v) 1458 | end 1459 | elseif command == "suspend" then 1460 | client:send("SUSPEND\n") 1461 | elseif command == "stack" then 1462 | local opts = string.match(params, "^[a-z]+%s+(.+)$") 1463 | client:send("STACK" .. (opts and " "..opts or "") .."\n") 1464 | local resp = client:receive("*l") 1465 | local _, _, status, res = string.find(resp, "^(%d+)%s+%w+%s+(.+)%s*$") 1466 | if status == "200" then 1467 | local func, err = loadstring(res) 1468 | if func == nil then 1469 | print("Error in stack information: " .. err) 1470 | return nil, nil, err 1471 | end 1472 | local ok, stack = pcall(func) 1473 | if not ok then 1474 | print("Error in stack information: " .. stack) 1475 | return nil, nil, stack 1476 | end 1477 | for _,frame in ipairs(stack) do 1478 | print(mobdebug.line(frame[1], {comment = false})) 1479 | end 1480 | return stack 1481 | elseif status == "401" then 1482 | local _, _, len = string.find(resp, "%s+(%d+)%s*$") 1483 | len = tonumber(len) 1484 | local res = len > 0 and client:receive(len) or "Invalid stack information." 1485 | print("Error in expression: " .. res) 1486 | return nil, nil, res 1487 | else 1488 | print("Unknown error") 1489 | return nil, nil, "Debugger error: unexpected response after STACK" 1490 | end 1491 | elseif command == "output" then 1492 | local _, _, stream, mode = string.find(params, "^[a-z]+%s+(%w+)%s+([dcr])%s*$") 1493 | if stream and mode then 1494 | client:send("OUTPUT "..stream.." "..mode.."\n") 1495 | local resp, err = client:receive("*l") 1496 | if not resp then 1497 | print("Unknown error: "..err) 1498 | return nil, nil, "Debugger connection error: "..err 1499 | end 1500 | local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$") 1501 | if status == "200" then 1502 | print("Stream "..stream.." redirected") 1503 | outputs[stream] = type(options) == 'table' and options.handler or nil 1504 | -- the client knows when she is doing, so install the handler 1505 | elseif type(options) == 'table' and options.handler then 1506 | outputs[stream] = options.handler 1507 | else 1508 | print("Unknown error") 1509 | return nil, nil, "Debugger error: can't redirect "..stream 1510 | end 1511 | else 1512 | print("Invalid command") 1513 | end 1514 | elseif command == "basedir" then 1515 | local _, _, dir = string.find(params, "^[a-z]+%s+(.+)$") 1516 | if dir then 1517 | dir = string.gsub(dir, "\\", "/") -- convert slash 1518 | if not string.find(dir, "/$") then dir = dir .. "/" end 1519 | 1520 | local remdir = dir:match("\t(.+)") 1521 | if remdir then dir = dir:gsub("/?\t.+", "/") end 1522 | basedir = dir 1523 | 1524 | client:send("BASEDIR "..(remdir or dir).."\n") 1525 | local resp, err = client:receive("*l") 1526 | if not resp then 1527 | print("Unknown error: "..err) 1528 | return nil, nil, "Debugger connection error: "..err 1529 | end 1530 | local _, _, status = string.find(resp, "^(%d+)%s+%w+%s*$") 1531 | if status == "200" then 1532 | print("New base directory is " .. basedir) 1533 | else 1534 | print("Unknown error") 1535 | return nil, nil, "Debugger error: unexpected response after BASEDIR" 1536 | end 1537 | else 1538 | print(basedir) 1539 | end 1540 | elseif command == "help" then 1541 | print("setb -- sets a breakpoint") 1542 | print("delb -- removes a breakpoint") 1543 | print("delallb -- removes all breakpoints") 1544 | print("setw -- adds a new watch expression") 1545 | print("delw -- removes the watch expression at index") 1546 | print("delallw -- removes all watch expressions") 1547 | print("run -- runs until next breakpoint") 1548 | print("step -- runs until next line, stepping into function calls") 1549 | print("over -- runs until next line, stepping over function calls") 1550 | print("out -- runs until line after returning from current function") 1551 | print("listb -- lists breakpoints") 1552 | print("listw -- lists watch expressions") 1553 | print("eval -- evaluates expression on the current context and returns its value") 1554 | print("exec -- executes statement on the current context") 1555 | print("load -- loads a local file for debugging") 1556 | print("reload -- restarts the current debugging session") 1557 | print("stack -- reports stack trace") 1558 | print("output stdout -- capture and redirect io stream (default|copy|redirect)") 1559 | print("basedir [] -- sets the base path of the remote application, or shows the current one") 1560 | print("done -- stops the debugger and continues application execution") 1561 | print("exit -- exits debugger and the application") 1562 | else 1563 | local _, _, spaces = string.find(params, "^(%s*)$") 1564 | if spaces then 1565 | return nil, nil, "Empty command" 1566 | else 1567 | print("Invalid command") 1568 | return nil, nil, "Invalid command" 1569 | end 1570 | end 1571 | return file, line 1572 | end 1573 | 1574 | -- Starts debugging server 1575 | local function listen(host, port) 1576 | host = host or "*" 1577 | port = port or mobdebug.port 1578 | 1579 | local socket = require "socket" 1580 | 1581 | print("Lua Remote Debugger") 1582 | print("Run the program you wish to debug") 1583 | 1584 | local server = socket.bind(host, port) 1585 | local client = server:accept() 1586 | 1587 | client:send("STEP\n") 1588 | client:receive("*l") 1589 | 1590 | local breakpoint = client:receive("*l") 1591 | local _, _, file, line = string.find(breakpoint, "^202 Paused%s+(.-)%s+(%d+)%s*$") 1592 | if file and line then 1593 | print("Paused at file " .. file ) 1594 | print("Type 'help' for commands") 1595 | else 1596 | local _, _, size = string.find(breakpoint, "^401 Error in Execution (%d+)%s*$") 1597 | if size then 1598 | print("Error in remote application: ") 1599 | print(client:receive(size)) 1600 | end 1601 | end 1602 | 1603 | while true do 1604 | io.write("> ") 1605 | local file, _, err = handle(io.read("*line"), client) 1606 | if not file and err == false then break end -- completed debugging 1607 | end 1608 | 1609 | client:close() 1610 | end 1611 | 1612 | local cocreate 1613 | local function coro() 1614 | if cocreate then return end -- only set once 1615 | cocreate = cocreate or coroutine.create 1616 | coroutine.create = function(f, ...) 1617 | return cocreate(function(...) 1618 | mobdebug.on() 1619 | return f(...) 1620 | end, ...) 1621 | end 1622 | end 1623 | 1624 | local moconew 1625 | local function moai() 1626 | if moconew then return end -- only set once 1627 | moconew = moconew or (MOAICoroutine and MOAICoroutine.new) 1628 | if not moconew then return end 1629 | MOAICoroutine.new = function(...) 1630 | local thread = moconew(...) 1631 | -- need to support both thread.run and getmetatable(thread).run, which 1632 | -- was used in earlier MOAI versions 1633 | local mt = thread.run and thread or getmetatable(thread) 1634 | local patched = mt.run 1635 | mt.run = function(self, f, ...) 1636 | return patched(self, function(...) 1637 | mobdebug.on() 1638 | return f(...) 1639 | end, ...) 1640 | end 1641 | return thread 1642 | end 1643 | end 1644 | 1645 | -- make public functions available 1646 | mobdebug.setbreakpoint = set_breakpoint 1647 | mobdebug.removebreakpoint = remove_breakpoint 1648 | mobdebug.listen = listen 1649 | mobdebug.loop = loop 1650 | mobdebug.scratchpad = scratchpad 1651 | mobdebug.handle = handle 1652 | mobdebug.connect = connect 1653 | mobdebug.start = start 1654 | mobdebug.on = on 1655 | mobdebug.off = off 1656 | mobdebug.moai = moai 1657 | mobdebug.coro = coro 1658 | mobdebug.done = done 1659 | mobdebug.pause = function() step_into = true end 1660 | mobdebug.yield = nil -- callback 1661 | mobdebug.output = output 1662 | mobdebug.onexit = os and os.exit or done 1663 | mobdebug.onscratch = nil -- callback 1664 | mobdebug.basedir = function(b) if b then basedir = b end return basedir end 1665 | 1666 | return mobdebug 1667 | -------------------------------------------------------------------------------- /styles/GtkCssProvider1.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Díaz Urbaneja Víctor Eduardo Diex 3 | * @date 28.02.2021 02:26:39 -04 4 | */ 5 | 6 | /** 7 | * I add to the children of window (GtkWindow) that are buttons (GtkButton) 8 | * a blue color 9 | */ 10 | window button { 11 | color: #1E90FF; 12 | } 13 | 14 | /** 15 | * the labels that are inside the buttons and have the class red 16 | * will have a red color 17 | */ 18 | button.red label { 19 | color: #E32424; 20 | } 21 | --------------------------------------------------------------------------------