<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
      Fork me on GitHub

      go語言實現終端里的倒計時

      最近在更新系統的時候發現pacman的命令行界面變了,我有很久沒更新過設備上的Linux系統了,所以啥時候變的不好說。但這一變化成功勾起了我的好奇心。新版的更新進度界面如下:

      新的更新進度界面能同時顯示多個進度條,而且并沒有依靠ncurses這個傳統的TUI庫。為啥我能斷定沒有用ncurses呢,因為用過這個庫的人都會發現程序在繪制界面的時候會用背景色清屏,且退出后終端的內容會恢復成運行程序前的樣子,而上述表現都不存在。

      不借助專用的庫卻又能繪制出比較生動的效果,這難道不吸引人嗎?

      所以帶著好奇心,我簡單探索了實現的原理,并且用相同的原理做了個新東西:

      這是一個在終端中顯示倒計時的小玩具,原理和pacman的進度條是一樣的,我并沒有一比一去復現pacman的效果,那樣其實和對著范本寫作文一樣略顯無聊,所以我選擇活用知識做個新玩具。

      好了,我們先來復習下單個終端命令行的進度條是怎么實現的。

      單個進度條的原理其實很簡單,幾乎所有的終端和終端模擬器都支持一些特殊的控制字符,比如\n表示新加一個空白行并把光標移動到這個新行的最左側也就是開頭處;\r則是將光標移動到當前行的開頭處。

      所以單個進度條的繪制過程一共只要兩步:

      1. 根據進度計算出當前進度條的樣子,然后用打印函數輸出,注意不能輸出換行符\n
      2. 輸出\r讓光標回到行首,等待一段時間,重復步驟1,新的輸出內容會覆蓋掉老的。
      3. 進度到了100%之后就可以輸出一個換行符\n結束進度條的打印了。

      最關鍵的地方也只有一處,新的輸出內容的長度要大于或者等于老內容,否則老內容會殘留在終端里。

      人眼的要求很低,所以你甚至可以不必做到每秒xx次刷新,只要在一秒或幾秒里更新幾次就能讓人覺得你的進度條動起來了。

      所以一個最簡單的例子可以是這樣的:

      package main
      
      import (
      	"bytes"
      	"fmt"
      	"time"
      )
      
      const width = 50
      
      func main() {
      	bar := bytes.Repeat([]byte{' '}, width)
      	fmt.Println()
      	for i := range 50 {
      		bar[i] = '='
      		fmt.Printf("[%s] % 3d%%\r", bar, (i+1)*2)
      		time.Sleep(100 * time.Millisecond)
      	}
          fmt.Println()
          fmt.Println("end")
      }
      

      這是效果:

      \r有個缺點,它只能回溯當前行,而且這個“行”是以終端顯示為準的——即使你的輸出并沒有包含換行符但它的長度超過了終端顯示的寬度導致需要“折行”,那么新折行出來的那行在終端顯示中會被認為是一個新行,\r只會將光標放到這個新行的開頭。

      其實我最開始想利用折行加\r字符實現多行進度條,但很快就發現這條路是走不通的。顯然pacman并沒有使用\r或者說它還利用了一些其他的東西。

      看源代碼是最快的,而且簡單搜索一下“progressbar”很快就能找到答案。我就不賣關子了,pacman實現多行進度條效果是利用了ASNI轉義序列

      ANSI轉義序列(ANSI escape sequences)是一種帶內信號的轉義序列標準,用于控制視頻文本終端上的光標位置、顏色和其他選項。在文本中嵌入確定的字節序列,大部分以ESC轉義字符和"["字符開始,終端會把這些字節序列解釋為相應的指令,而不是普通的字符編碼。

      簡單的說,轉義序列就像一些命令,可以控制光標和終端的各種行為。

      具體格式是:轉義序列開始字符參數1;參數2;...;參數N命令。我們最常見的轉義序列是顏色控制,讓終端里的文字變成紅色:\033[0;31m。其中\033[是轉義序列的開始標志,0;31是命令m的兩個參數,參數之間用空格分隔,最后一個參數緊貼著命令。

      轉義序列的支持程度要看終端和終端模擬器,好消息是我們需要用到的轉義序列的被廣泛支持的,我們要用它們來在行與行之間移動光標并繪制內容。

      轉義序列支持光標上下左右移動還支持直接清除整行的內容,這使得我們可以將終端當成一個畫布:每個字符的位置相當于畫布上的一個像素點(因此使用等寬字體效果顯示會更好),坐標原點是程序運行開始后光標所在的位置,根據這個原點可以簡單構建出一個平面坐標系,我們可以用一些特殊字符模擬點和線來繪制簡單的圖形。

      我們要用的轉義序列是這些:

      1. \033[nF,將光標向上移動n行
      2. \033[nE,將光標向下移動n行
      3. \033[nC,將光標向后(右)移動n個字符
      4. \033[2K,清除光標所在行的整個內容(2以外的參數可以選擇只清除光標前/后的內容)
      5. 轉義字符之間可以組合使用,比如\033[nE\033[mC表示光標先向下移動n行然后再向右移動m個字符。

      現在你應該明白那個倒計時是怎么畫出來的了,核心技術點就是找到個合適的數字asciiart,然后根據每秒更新的內容在正確的位置上用上面的轉義序列像畫像素點一樣把數字和分隔符畫出來就行了。

      說說其實一句話的事情,但做起來還是比較麻煩的,因為轉義序列用的都是相對坐標,稍微算錯一點相對位置顯示效果就整個完蛋了,我也是調試了三四回才做到正確繪制的:

      func (ar *ASCIIArtCharRender) RenderContent(duration time.Duration) {
      	if len(ar.chars) > 0 {
      		ar.chars = ar.chars[:0]
      	}
      	ar.chars = char.ConvertToChars(duration, char.ASCIIArtChars, ar.chars)
      	for i := 0; i < char.MaxASCIIArtCharHeight(); i++ {
      		util.CursorEraseEntireLine()
      		fmt.Print(ar.chars[0][i])
      		fmt.Print(" ")
      		fmt.Print(ar.chars[1][i])
      		fmt.Print("  ")
      		fmt.Print(char.ASCIIArtChars[char.ASCIIArtColonIdx][i])
      		fmt.Print("  ")
      		fmt.Print(ar.chars[2][i])
      		fmt.Print(" ")
      		fmt.Print(ar.chars[3][i])
      		fmt.Print("  ")
      		fmt.Print(char.ASCIIArtChars[char.ASCIIArtColonIdx][i])
      		fmt.Print("  ")
      		fmt.Print(ar.chars[4][i])
      		fmt.Print(" ")
      		fmt.Print(ar.chars[5][i])
      		fmt.Print("\n")
      	}
      }
      
      func (ar *ASCIIArtCharRender) RenderFlashing() {
      	util.CursorDownForward(1, 3+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
      	fmt.Print(" ")
      	util.CursorForward(3 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 3)
      	fmt.Print(" ")
      	util.CursorDownForward(1, 2+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
      	fmt.Print("   ")
      	util.CursorForward(2 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 2)
      	fmt.Print("   ")
      
      	util.CursorDownForward(2, 3+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
      	fmt.Print(" ")
      	util.CursorForward(3 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 3)
      	fmt.Print(" ")
      	util.CursorDownForward(1, 2+len(ar.chars[0][0])+1+len(ar.chars[1][0]))
      	fmt.Print("   ")
      	util.CursorForward(2 + len(ar.chars[2][0]) + 1 + len(ar.chars[3][0]) + 2)
      	fmt.Print("   ")
      	// move to bottom
      	util.CursorDown(1)
      }
      

      第一個函數是繪制時間用的數字的,為了簡單我已經提前把數字的asciiart保存進了二維數組并且做到了等高,這樣畫的時候只要知道需要什么數字就行,剩下的就是逐行輸出“像素點”。

      第二個函數是用來繪制電子時鐘數字分隔符的閃爍效果的,這個看上去就更亂了,因為需要在終端畫布上大范圍移動。

      所以會者不難,純體力活。

      完整的代碼可以在這找到:https://github.com/apocelipes/ascii-count-down,歡迎各位大佬的改進或者功能增強。

      總結

      TUI還是挺有意思的,好玩能學到東西而且很能消磨無聊的時間。

      另外我覺得在之間看源碼對答案之前,可以先自己思考一下并動手做做試驗比如像我那樣最先異想天開用折行去實現多行進度條。這樣雖然浪費了點時間,但可以加深自己對新知識的理解和記憶。

      posted @ 2025-03-05 22:55  apocelipes  閱讀(721)  評論(2)    收藏  舉報
      主站蜘蛛池模板: 天天综合亚洲色在线精品| 野外少妇被弄到喷水在线观看| 国产v亚洲v天堂a无码99| 久热re这里精品视频在线6| 国产自在自线午夜精品| 妖精视频yjsp毛片永久| 亚洲成人高清av在线| 亚洲熟女精品一区二区| 亚洲中文字幕一二三四区| 国产69精品久久久久99尤物| 精品超清无码视频在线观看| 国产一二三四区中| 久久婷婷大香萑太香蕉AV人| 黄大仙区| 国产99视频精品免费视频6| 亚洲国产精品久久电影欧美| 国内精品久久人妻无码不卡| 亚洲18禁一区二区三区| 日韩人妻无码一区二区三区| 粉嫩国产av一区二区三区| 国产中文字幕在线一区| 欧美刺激性大交| 99中文字幕国产精品| 长白| 粉嫩国产一区二区三区在线| 5D肉蒲团之性战奶水欧美| 深夜av在线免费观看| 韩国V欧美V亚洲V日本V| 3d无码纯肉动漫在线观看| 伊人色综合久久天天| 少妇被粗大的猛烈进出69影院一| 国产成人一区二区不卡| 国产极品尤物粉嫩在线观看| 免费国产女王调教在线视频| 亚洲精品一区二区在线播| 欧美一本大道香蕉综合视频| 日韩有码国产精品一区| 国产精品爆乳奶水无码视频免费| 欧美一级高清片久久99| 国产漂亮白嫩美女在线观看| 国产精品多p对白交换绿帽|