lgi-devs / lgi

Dynamic Lua binding to GObject libraries using GObject-Introspection
MIT License
440 stars 70 forks source link

GLIb.timeout_add, search entry debounce help #259

Closed vredesbyyrd closed 3 years ago

vredesbyyrd commented 3 years ago

Hi,

I am working on a simple media database search tool, and am stuck trying to figure out how to add a timeout to the input entry. I am using rofi blocks as the "gui". Rofi-blocks does most of the heavy lifting here...

The module works by reading the stdin line by line to update rofi content. It can execute an application in the background, which will be used to communicate with the modi. The communication is made using file descriptors. The modi reads the output of the background program and writes events its input.

Currently every keypress is making a new query to the sqlite table. The end goal is to add a 500ms-ish timeout so the search function is only called once the user stops typing. This brought me to lgi, but after a day of fussing around I cannot figure it out. I am not that familiar with GObject let alone a programmer, just trying my best to make a tool to easily query some databases I use on the daily.

This is the current (and stripped down) implementation without any timeout/debounce. The project is only for linux

BUF = { name=nil, value=nil, data=nil}

function ParseView(arg)
  local t = {
    ["input action"] = 'send',
    ["lines"]        = arg.lines
  }

  local rofi_input = json.encode(t)

  io.stdout:setvbuf('line')
  io.stdout:write(rofi_input .. '\n')

  local rofi_output = json.decode( io.stdin:read('*l') )

  BUF['name']  = rofi_output.name
  BUF['value'] = rofi_output.value
  BUF['data']  = rofi_output.data
end

repeat
  if BUF['value'] ~= '' then
     --currently displays what user types, normally will call fts5 table with 'BUF['value']' as search term
    ParseView{lines={ {text=BUF['value']} }}
  else
    ParseView{lines={ {text='waiting for input'} }}
  end
until BUF['name'] == 'select entry' -- when user selects row from results set

I tried implementing a timout using GLib.timeout_add, but the query never gets sent...99% sure because my code sucks.

local counter = 0
function WaitForQuery()
  counter = counter + 1
  timer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, doQuery)
  --GLib.source_remove(timer)
  return false
end

function doQuery()
  counter = counter - 1

  if counter == 0 then
    ParseView{lines={ {text=BUF['value']} }}
  end
end

repeat
  if BUF['value'] ~= '' then
    WaitForQuery()
  else
    ParseView{lines={ {text='waiting for input'} }}
  end
until BUF['name'] == 'select entry'

Noob Questions:

Do I need to to use Glibs mainloop in order to use GLib.timeout_add. And just generally, if you have any tips/code or any suggestions on how to implement a 'search entry timeout' in lua, I would be very grateful.

Thanks for your time.

diazvictor commented 3 years ago

Hi, try GLib.timeout_add_seconds. It's the way I do a sleep (timeout).

function WaitForQuery()
    counter = counter + 1
    timer = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 500, doQuery)
    return false
end
psychon commented 3 years ago

Do I need to to use Glibs mainloop in order to use GLib.timeout_add.

Yes.

Besides that, I am not really sure how to help you since I am not quite sure what is going on.. Do I understand correctly, that rofi-blocks starts another program and you are working on that other program? How does that other program work? What kind of main loop is it using? Does it use LGI?

vredesbyyrd commented 3 years ago

Thanks @diazvictor

I went with GLib.timeout_add because the glib docs specify that it uses milliseconds. I did try your recommendation but no luck.

Do I understand correctly, that rofi-blocks starts another program and you are working on that other program?

That is correct. Rofi is a standalone C program that allows for scripting simple menu's in any language. rofi-blocks is a C plugin to extend rofi scripting capabilities....

This modi (plugin) updates rofi each time it reads a line from the stdin or background program output, and writes an event made from rofi, in most cases is an interaction with the window, as a single line to stdout or its input. The format used for communication is JSON format.

Basically, the program I am working on just writes a stream of data that rofi-blocks reads. Rofi parses the data and draws to the screen. Here is a link to my onedrive with two videos that may make it more clear. The terminal is showing rofis debug output, rofi is screen left. onedrive

The full script displayed in `sample_input.mkv' is as simple as this:

G_MESSAGES_DEBUG=Dialogs.BlocksMode rofi -modi blocks -show blocks -blocks-wrap '/path/to/script/test.lua'

#!/usr/bin/lua

local json = require('cjson')

BUF = { name=nil, value=nil, data=nil}

function ParseView(arg)
  local t = {
    ["input action"] = 'send',
    ["lines"]        = arg.lines
  }

  local rofi_input = json.encode(t)

  io.stdout:setvbuf('line')
  io.stdout:write(rofi_input .. '\n')

  local rofi_output = json.decode( io.stdin:read('*l') )

  BUF['name']  = rofi_output.name
  BUF['value'] = rofi_output.value
  BUF['data']  = rofi_output.data
end

repeat
  if BUF['value'] ~= '' then
     --currently displays what user types, normally will call fts5 table with 'BUF['value']' as search term
    ParseView{lines={ {text=BUF['value']} }}
  else
    ParseView{lines={ {text='waiting for input'} }}
  end
until BUF['name'] == 'select entry' -- when user selects row from results set

What kind of main loop is it using? Does it use LGI?

My script is only using a repeat until loop. No, it's not using LGI. In reading about how to do timeout's in lua one person suggested LGI. I already have LGI as a dependency of some other programs so I thought I would give it a go.

After looking at someones rofi-block script using javascript, they implemented a search entry timeout with some async code. Which tells me I would probably have to do something similar in lua.

psychon commented 3 years ago

Here is an example that reads lines from stdin and "does something" when no input was received for one second. Does this help you?

local lgi = require("lgi")
local GLib = lgi.GLib
local Gio = lgi.Gio

local pending_input = ""
local source_id = nil

local function my_pcall(f)
    local res, err = pcall(f)
    if not res then
        print(err)
    end
end

local function process_input()
    print("Do something with the input:", pending_input)
    source_id = nil
    pending_input = ""
end

Gio.Async.call(my_pcall)(function()
    local stdin = Gio.UnixInputStream.new(0, false)
    local stdin = Gio.DataInputStream.new(stdin)
    while true do
        local line, length = stdin:async_read_line(GLib.PRIORITY_DEFAULT)
        if type(line) == "nil" then
            print("End of file or error: ", tostring(length))
            break
        end
        print("Read line:", line)
        if source_id then
            GLib.source_remove(source_id)
        end
        source_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, process_input)
        pending_input = pending_input .. line .. "\n"
    end
end)
vredesbyyrd commented 3 years ago

I very much appreciate you making this. But I got to working on this again today and ended up going with a getch like module to do non-blocking reads : /