golang编写Linux下可视化界面

Posted by     "zengchengjie" on Wednesday, June 15, 2022

概述

接到了一个需求,需要服务器开机后启动一个程序,打印二维码到屏幕上,并显示机器相关信息,以及执行部分脚本,打印输出结果。

开始的设计是想要编写一个shell脚本来实现,但是考虑到shell脚本交互并不太友好,实现效果不理想,于是开始寻找合适的开源库来实现,最终在GitHub上选定了一个开源项目点击跳转GitHub,该项目提供的组件对我目前的需求已经够用了,本身star和issue数都不少,说明是一个较为活跃的项目,适合使用。

先甩图片,界面大致效果如下:

要完成项目要求,先要满足几个条件:

  • 打印二维码

    找了一圈开源库,有Python的、Java的、golang的种种,最后看上了go-qrcode这个项目

    使用方式:

    q, err := qrcode.New(content, qrcode.Low)
              if err != nil {
                  logger.Errorln(err)
                  continue
              }
    fmt.Print(q.ToSmallString(false))
    

    更多使用方式,如命令行下使用,生成图片,或者其他的花式二维码打印,参见该项目链接

  • 交互能够满足基本使用

    项目中提供了各种基础组件的使用demo,我们在本地运行调试即可了解各个组件的用法。

  • 开机自启

    将程序注册到Linux的服务中,达到开机自启的目的,使用systemctl start/stop/reload/restart命令即可控制服务状态

  • 自定义快捷键添加

    添加了多个自定义快捷键,Ctrl+F、Ctrl+R、ESC、F1、F12等等快捷键。

  • ctlr+c不退出

    老办法,拦截系统信号即可:

    sigs := make(chan os.Signal, 1)
          done := make(chan bool, 1)
          signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGTSTP)
          go func() {
              _ = <-sigs
              done <- true
          }()
          menu.InitMenu()
    
          <-done
    
  • 支持中英文切换

    在程序同级目录下新建i18n文件夹,放入语言包,命名为en.toml、zh.toml形式(最终打包时也要把这个配置文件的文件上传至二进制文件的同级目录)

    引入依赖:“github.com/gogf/gf/i18n/gi18n”

    var t *gi18n.Manager
    
    func main{
      t = gi18n.New()
    	if len(lang) > 0 {
          t.SetLanguage(lang)
    	} else {
          t.SetLanguage("en")
    	}
      fmt.println(t.Translate(nil, "testWord"))
    }
    
    

遇到的问题

  • 中文环境下,界面乱码

    查看了各个模块代码,不知所以,故上GitHub查看issue,作者说我啥也干不了,不解决。

    最终在另一个项目go-runewidthissue里找到了答案(界面绘制部分,tview引用的是go-runewidth项目)

    我们走到关键代码runewidth.go部分,注释相关判断语言环境的代码即可:

    func handleEnv() {
    	//env := os.Getenv("RUNEWIDTH_EASTASIAN")
    	//if env == "" {
    	//	EastAsianWidth = IsEastAsian()
    	//} else {
    	//	EastAsianWidth = env == "1"
    	//}
    	// update DefaultCondition
    	DefaultCondition.EastAsianWidth = EastAsianWidth
    }
    
  • ctrl+c 程序不退出,但是界面消失了

    解决办法:找到项目源码中的ctrl+c部分(追踪tcell.KeyCtrlC关键词),注释相关退出代码

    // Ctrl-C closes the application.
                  if event.Key() == tcell.KeyCtrlC {
                      //a.Stop()
                      break
                  }
    
  • 二维码占用面积过大,tty下显示不全

    解决办法:修改二维码打印代码,删除边框

    重写fmt.Print(q.ToSmallString(false))中的ToSmallString(false)

    将循环打印边框部分略过,关键代码:

    if x == 0 || x == 1 || x == 2 || x == len(bits[y])-3 || x == len(bits[y])-2 || x == len(bits[y])-1 {
                  continue
              }
    

    完整方法:

    func ToSmallString(q *qrcode.QRCode, inverseColor bool) string {
    	bits := q.Bitmap()
    	var buf bytes.Buffer
    	// if there is an odd number of rows, the last one needs special treatment
    	for y := 3; y < len(bits)-4; y += 2 {
    
          for x := range bits[y] {
              if x == 0 || x == 1 || x == 2 || x == len(bits[y])-3 || x == len(bits[y])-2 || x == len(bits[y])-1 {
                  continue
              }
              if bits[y][x] == bits[y+1][x] {
                  if bits[y][x] != inverseColor {
                      buf.WriteString(" ")
                  } else {
                      buf.WriteString("█")
                  }
              } else {
                  if bits[y][x] != inverseColor {
                      buf.WriteString("▄")
                  } else {
                      buf.WriteString("▀")
                  }
              }
          }
          buf.WriteString("\n")
    	}
    	// special treatment for the last row if odd
    	if len(bits)%2 == 1 {
          y := len(bits) - 1
          for x := range bits[y] {
              if x == 0 || x == 1 || x == 2 || x == len(bits[y])-3 || x == len(bits[y])-2 || x == len(bits[y])-1 {
                  continue
              }
              if bits[y][x] != inverseColor {
                  buf.WriteString(" ")
              } else {
                  buf.WriteString("▀")
              }
          }
          buf.WriteString("\n")
    	}
    	return buf.String()
    }
    
  • debian系统下,二维码乱码

    暂未找到解决方案,debian中的二维码显示异常,其中方块"▀"乱码

支持远程命令下发

场景:当终端收到执行命令 nodeadmin -show、-hide命令,需要界面做相应调整

实现方案:

收到命令时,使用ps命令获得程序进程号,并使用kill -1、kill -2命令将信号发送到终端即可完成监控,考虑可能启动多个终端,需要循环kill。

发送逻辑:

var showFakeCoverCmd = &cobra.Command{
	Use:   "cover",
	Short: "show fake cover",
	Long:  `nodeadmin cover`,
	Run: func(cmd *cobra.Command, args []string) {
		shellCmd := "ps -ef |grep -E 'nodeadmin$' |grep -v grep |awk '{print $2}'"

		pidResult, err := sysutils.ExeShellCmd(shellCmd)
		if len(pidResult) == 0 {
			fmt.Printf("nodeadmin not stared!")
			os.Exit(0)
		}
		if err != nil {
			fmt.Printf("err: %v", err)
			os.Exit(0)
		}

		regx := regexp.MustCompile(`\s+`)
		pids := regx.Split(strings.TrimSpace(pidResult), 1000)
		for _, pid := range pids {
			shellCmd = fmt.Sprintf("kill -1 %s", pid)
			_, err = sysutils.ExeShellCmd(shellCmd)
			if err != nil {
				fmt.Printf("err: %v", err)
				os.Exit(0)
			}
		}
	},
}

func init() {
	rootCmd.AddCommand(showFakeCoverCmd)
}

监听逻辑:

sigs := make(chan os.Signal, 1)
		done := make(chan bool, 1)
		signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGTSTP)
		go func() {
			for {
				sig := <-sigs
          logger.Infof("收到信号为:%v", sig)
				if sig == syscall.SIGHUP {
					menu.ShowFakeCover()
					continue
				}
				if sig == syscall.SIGINT {
					menu.HideFakeCover()
					continue
				}
				done <- true
			}
		}()
		menu.InitMenu()

		<-done