I am looking into writing a CLI application in Go.
One of the requirements is auto complete. Not of the command itself but of possible options.
Imagine I want to add a new entry using the CLI. Each entry can have a category.
The categories are available in a slice. What I want to do now is to enable the user to tab through the available categories when typing in add.
I am aware of libraries like https://github.com/chzyer/readline and https://github.com/spf13/cobra but could not find if or how they support this.
Thank you #ain and #JimB for pointing me in the right direction.
Based on the example provided at https://github.com/chzyer/readline/tree/master/example/readline-demo I was able to achieve the desired functionality.
The following code has to main commands newEntry and newCategory. If the user types newEntry and than presses TAB he can choose from the available categories. The newCategory command allow to add a new custom category which is immediately available the next time newEntry is executed.
package main
import (
"io"
"log"
"strconv"
"strings"
"github.com/chzyer/readline"
)
// completer defines which commands the user can use
var completer = readline.NewPrefixCompleter()
// categories holding the initial default categories. The user can add categories.
var categories = []string{"Category A", "Category B", "Category C"}
var l *readline.Instance
func main() {
// Initialize config
config := readline.Config{
Prompt: "\033[31m»\033[0m ",
HistoryFile: "/tmp/readline.tmp",
AutoComplete: completer,
InterruptPrompt: "^C",
EOFPrompt: "exit",
HistorySearchFold: true,
}
var err error
// Create instance
l, err = readline.NewEx(&config)
if err != nil {
panic(err)
}
defer l.Close()
// Initial initialization of the completer
updateCompleter(categories)
log.SetOutput(l.Stderr())
// This loop watches for user input and process it
for {
line, err := l.Readline()
if err == readline.ErrInterrupt {
if len(line) == 0 {
break
} else {
continue
}
} else if err == io.EOF {
break
}
line = strings.TrimSpace(line)
// Checking which command the user typed
switch {
// Add new category
case strings.HasPrefix(line, "newCategory"):
// Remove the "newCategory " prefix (including space)
if len(line) <= 12 {
log.Println("newCategory <NameOfCategory>")
break
}
// Append everything that comes behind the command as the name of the new category
categories = append(categories, line[12:])
// Update the completer to make the new category available in the cmd
updateCompleter(categories)
// Program is closed when user types "exit"
case line == "exit":
goto exit
// Log all commands we don't know
default:
log.Println("Unknown command:", strconv.Quote(line))
}
}
exit:
}
// updateCompleter is updates the completer allowing to add new command during runtime. The completer is recreated
// and the configuration of the instance update.
func updateCompleter(categories []string) {
var items []readline.PrefixCompleterInterface
for _, category := range categories {
items = append(items, readline.PcItem(category))
}
completer = readline.NewPrefixCompleter(
readline.PcItem("newEntry",
items...,
),
readline.PcItem("newCategory"),
)
l.Config.AutoComplete = completer
}
Related
I'm very new to coding so this is difficult for me to figure out how to do, I've tried looking docs n google but without help!
I got 3 buttons where I want one of them to change the new,vmax container with a new container having data specified in the button func, but I can't seem to figure out how I on the button can change the container content or the whole container
This is my code for now
package main
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
var Menu = []string{"Home", "App-Status"}
func main() {
var W fyne.Window
a := app.New()
a.Settings().SetTheme(theme.DarkTheme())
W = a.NewWindow("Application-OutSight")
W.Resize(fyne.NewSize(640, 460))
text := widget.NewLabel("Welcome to This App")
// start container with welcome text
contentcontainer := container.NewMax(text)
split := (container.NewHSplit(
menuBar(Menu),
contentcontainer,
))
split.Offset = 0.2
W.SetContent(split)
W.ShowAndRun()
}
func menuBar(Menu []string) *widget.List {
listView := widget.NewList(func() int {
return len(Menu)
},
func() fyne.CanvasObject {
return widget.NewLabel("template")
},
func(id widget.ListItemID, o fyne.CanvasObject) {
o.(*widget.Label).SetText(Menu[id])
})
listView.OnSelected = func(id widget.ListItemID) {
if id == 0 {
//when i click here i want to change the start container to this container but not the sidebare as shown on picture only the container with the welcome text
somevaluefunction()
anothervaluefunction()
contentcontainer = container.NewMax(somevaluefunction(), anothervaluefunction())
// return or refresh the container in main with this new one
} else if id == 1 {
fmt.Println("app")
}
if id == 2 {
fmt.Println("exit")
}
}
return listView
}
You could change split.Objects[1] or put a named container inside there and set it’s content directly (avoiding the [1] to access second item of a split.
I'm trying to extend fyne widget to have a simple clickable content with background.
I searched fyne widgets to find an example that could be used as a starter and found something similar in List/ListItem.
I basically copied the list item code and adapted it a little bit.
It does look similar to the simpler example on fyne documentation.
But for some unknown reason go gives me an error and I don't know what the cause is or how I can fix it:
custom_widget/simple_card.go:80:24: c.card.super undefined (type *SimpleCard has no field or method super)
Here is the code of the module (custom_widget/simple_card.go):
package custom_widget
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"log"
)
// Declare conformity with interfaces.
var _ fyne.Widget = (*SimpleCard)(nil)
var _ fyne.Tappable = (*SimpleCard)(nil)
type SimpleCard struct {
widget.BaseWidget
onTapped func()
background *canvas.Rectangle
content fyne.CanvasObject
selected bool
}
func NewSimpleCard(content fyne.CanvasObject, tapped func()) *SimpleCard {
card := &SimpleCard{onTapped: tapped, content: content}
card.ExtendBaseWidget(card)
return card
}
// CreateRenderer is a private method to Fyne which links this custom_widget to its renderer.
func (c *SimpleCard) CreateRenderer() fyne.WidgetRenderer {
c.ExtendBaseWidget(c)
c.background = canvas.NewRectangle(theme.SelectionColor())
c.background.Hide()
objects := []fyne.CanvasObject{c.background, c.content}
// NewBaseRenderer and BaseRenderer are copied from
// https://github.com/fyne-io/fyne/blob/master/internal/widget/base_renderer.go
// because the functionality is marked internal in fyne !?
return &SimpleCardRenderer{NewBaseRenderer(objects), c}
}
func (c *SimpleCard) Tapped(_ *fyne.PointEvent) {
log.Println("I have been tapped")
if c.onTapped != nil {
c.selected = true
c.Refresh()
c.onTapped()
}
}
// Declare conformity with the WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*SimpleCardRenderer)(nil)
type SimpleCardRenderer struct {
BaseRenderer
card *SimpleCard
}
// MinSize calculates the minimum size of a SimpleCardRenderer.
// This is based on the size of the status indicator and the size of the child object.
func (c *SimpleCardRenderer) MinSize() fyne.Size {
return c.card.content.MinSize()
}
// Layout the components of the SimpleCardRenderer custom_widget.
func (c *SimpleCardRenderer) Layout(size fyne.Size) {
c.card.background.Resize(size)
c.card.content.Resize(size)
}
func (c *SimpleCardRenderer) Refresh() {
if c.card.selected {
c.card.background.FillColor = theme.SelectionColor()
c.card.background.Show()
} else {
c.card.background.Hide()
}
c.card.background.Refresh()
canvas.Refresh(c.card.super()) // compiler error !
}
Remove all of the renderer type you created and in the CreateRenderer just return widget.NewSimpleRenderer(container .NewMax(c.background, c.content)). It is simpler than you think.
Copying code out of the main widgets is often not the best way as we have shortcuts and/or must support more functionality than your own widgets.
I am using the library https://github.com/go-telegram-bot-api/telegram-bot-api
But I do not understand how I can implement the output of buttons and arrows. A lot of data with pagination comes from my api, but I don't understand how to make pages in telegrams. If someone gives an example, I will be very grateful!
This is perfectly implemented in python: https://pypi.org/project/python-telegram-bot-pagination/
But i need for Golang :(
As I checked there were no plugins or wrappers for telegram-bot-api package, so you're gonna have to handle this manually.
Suppose we have this data:
var data = []string{"DummyData1", "DummyData2", "DummyData3", "DummyData4", "DummyData5", "DummyData6", "DummyData7", "DummyData8", "DummyData9", "DummyData10"}
If we're going to show 2 items on each page for 10 items, we would have 5 pages:
var count = 2
var maxPages = len(data) / count // = 5
First we should have a function that calculates data slice and gives us the inlineKeyboardMarkup:
func DummyDataTextMarkup(currentPage, count int) (text string, markup tgbotapi.InlineKeyboardMarkup) {
text = strings.Join(data[currentPage*count:currentPage*count+count], "\n")
var rows []tgbotapi.InlineKeyboardButton
if currentPage > 0 {
rows = append(rows, tgbotapi.NewInlineKeyboardButtonData("Previous", fmt.Sprintf("pager:prev:%d:%d", currentPage, count)))
}
if currentPage < maxPages-1 {
rows = append(rows, tgbotapi.NewInlineKeyboardButtonData("Next", fmt.Sprintf("pager:next:%d:%d", currentPage, count)))
}
markup = tgbotapi.NewInlineKeyboardMarkup(rows)
return
}
Then there is going to be a function to send/edit the calculated data and keyboard by passing chatId, currentPage, count and messageId:
func SendDummyData(chatId int64, currentPage, count int, messageId *int) {
text, keyboard := DummyDataTextMarkup(currentPage, count)
var cfg tgbotapi.Chattable
if messageId == nil {
msg := tgbotapi.NewMessage(chatId, text)
msg.ReplyMarkup = keyboard
cfg = msg
} else {
msg := tgbotapi.NewEditMessageText(chatId, *messageId, text)
msg.ReplyMarkup = &keyboard
cfg = msg
}
bot.Send(cfg)
}
(Note: If we pass a messageId to this function it's going to edit our message with new data)
The next step is to have a CallbackQueryHandler to handle a user's click on inline buttons:
func CallbackQueryHandler(query *tgbotapi.CallbackQuery) {
split := strings.Split(query.Data, ":")
if split[0] == "pager" {
HandleNavigationCallbackQuery(query.Message.MessageID, split[1:]...)
return
}
}
func HandleNavigationCallbackQuery(messageId int, data ...string) {
pagerType := data[0]
currentPage, _ := strconv.Atoi(data[1])
itemsPerPage, _ := strconv.Atoi(data[2])
if pagerType == "next" {
nextPage := currentPage + 1
if nextPage < maxPages {
SendDummyData(chatId, nextPage, itemsPerPage, &messageId)
}
}
if pagerType == "prev" {
previousPage := currentPage - 1
if previousPage >= 0 {
SendDummyData(chatId, previousPage, itemsPerPage, &messageId)
}
}
}
(Note: The first function CallbackQueryHandler is a global callback handler that calls our desired callbackhandler by splitting its query with : and getting the handler's name, here it is pager as we have defined and its handler is called HandleNavigationCallbackQuery).
The last step would be to call CallbackQueryHandler in your update loop as well as sending the initial data to your desired chat:
var chatId = int64(0) // <--- Place Chat Id Here
SendDummyData(chatId, 0, 2, nil) // Send initial data
for update := range updates {
if update.CallbackQuery != nil {
CallbackQueryHandler(update.CallbackQuery)
continue
}
}
You can check the full example on my GitHub's gist here
I want to combine some standard widget in one custom widget. I can do it if put all of widget fields into one container like so:
package main
import (
"fmt"
"fyne.io/fyne"
"fyne.io/fyne/app"
"fyne.io/fyne/layout"
"fyne.io/fyne/widget"
)
type MyWidget struct {
widget.BaseWidget
Cont *fyne.Container
text *widget.Label
statusBar *widget.Label
b1 *widget.Button
b2 *widget.Button
count uint
}
func (t *MyWidget) Init() {
t.b1 = widget.NewButton("1", func() {
t.text.SetText("1")
t.count++
t.statusBar.SetText(fmt.Sprint(t.count))
})
t.b2 = widget.NewButton("2", func() { t.text.SetText("2") })
t.statusBar = widget.NewLabel("status")
bottom := fyne.NewContainerWithLayout(layout.NewCenterLayout(), t.statusBar)
t.text = widget.NewLabelWithStyle("0", fyne.TextAlignTrailing, fyne.TextStyle{Bold: true})
t.Cont = fyne.NewContainerWithLayout(layout.NewBorderLayout(nil, bottom, nil, nil),
bottom, fyne.NewContainerWithLayout(
layout.NewGridLayoutWithRows(4),
fyne.NewContainerWithLayout(layout.NewCenterLayout(), t.text),
layout.NewSpacer(),
fyne.NewContainerWithLayout(layout.NewGridLayout(2), t.b1, t.b2),
layout.NewSpacer(),
))
}
func Load() *MyWidget {
obj := &MyWidget{BaseWidget: widget.BaseWidget{}}
obj.Init()
return obj
}
func main() {
f := app.New()
w := f.NewWindow("")
obj := Load()
w.SetContent(obj.Cont)
w.ShowAndRun()
}
I used to use GUI toolkits where top widget has opportunity to set container for holding child widgets. Is it possible get solution with Fyne without exported inner container?
I would recommend you look at using a container instead. (I.e. ‘fyne.NewContainerWithLayout(myLayout, widgets...)’.
Widgets and containers are distinct in Fyne. Widgets are an encapsulation for logic, with a renderer to display, Containers are used to group multiple widgets.
There are some widgets that bridge the gap, such as widget.Box and widget.Group, but they typically expose a container, or re-export the container methods.
Generally you don’t make a tree of widgets, but rather a container tree with widgets at the loop.
Iam using Gorilla/Sessions.
I got a template page, where the user can choose between different devices.
If he uses one of the submit buttons under each device my controller function should add the id value to my existing session value.
func Cart(w http.ResponseWriter, r *http.Request) {
data := CartData{
Name: "Cart",
Equipment: model.GetEquipment(model.Db),
Pages: []Page{
{
Title: "Meine Geräte",
Active: false,
Link: "/my-equipment",
},
{
Title: "Equipment",
Active: false,
Link: "/equipment",
},
{
Title: "Logout",
Active: false,
Link: "/logout",
},
},
}
equipment,_ := model.GetEquipmentByID(r.FormValue("id"))
session, _ := store.Get(r, "cookie-name")
Strings := strconv.Itoa(equipment.ID)
fmt.Println(Strings)
StringsWithComma := Strings + ","
session.Values["EquipmentIDs"] = session.Values["EquipmentIDs"] + StringsWithComma // THIS CODE LINE DOES NOT WORK, I want to expand "EquipmentIDs" with the new ID
tmpl:= template.Must(template.ParseFiles("template/base_user.html", "template/cart.html"))
tmpl.ExecuteTemplate(w, "base", data)
}
}
Example: User is visiting my Device page. He uses the submit button with id=2
SessionValue["EquipmentIDs"] should be = "2" right now.
After that the User is visiting the Device Page again an uses the submit button with id=6.
Now the SessionValue should be = "2,6"
I am attached to the problem all day and can not get any further
If you have Questions or want to see other parts of my code feel free to ask
Thanks in advance
I thought that you should get original equipment value from the session, combine the equipment ID that user clicks on and save into session for the next request.
I update your code as following:
// ...
equipment, _ := model.GetEquipmentByID(r.FormValue("id"))
session, _ := store.Get(r, "cookie-name")
equipmentIdsStr := strconv.Itoa(equipment.ID)
if original, exist := session.Values["EquipmentIDs"]; exist {
equipmentIdsStr = fmt.Sprintf("%v,%v", original, equipmentIdsStr)
}
session.Values["EquipmentIDs"] = equipmentIdsStr
//You have to save your updated session value
session.Save(r, w)
// ...
Hope this help.