7 điều học được khi xây dựng một framework TUI hiện đại
(textualize.io)- Quá trình phát triển Textual cho thấy có thể tạo hoạt ảnh TUI mượt mà ngay cả trên các giao thức terminal cũ, nhưng cần xử lý cẩn thận cách làm mới màn hình và đồng bộ hóa đầu ra
- Để giảm nhấp nháy, điều quan trọng là ghi đè lên nội dung hiện có thay vì xóa màn hình, ghi một lần vào đầu ra chuẩn, và tận dụng giao thức Synchronized Output
- View
dict.items()của Python và@lru_cachehữu ích cho việc tính toán phần thay đổi khi render và tối ưu các hàm nhỏ được gọi với tần suất cao; phần triển khai C của CPython hỗ trợ hiệu năng cache - Các đối tượng gần như bất biến, sơ đồ bằng ký tự hộp Unicode và
fractions.Fractionlần lượt giúp tăng độ chính xác trong suy luận·cache·kiểm thử, hiểu tài liệu và tính toán layout - Emoji trong terminal khó dự đoán do khác biệt về phiên bản Unicode, độ rộng ký tự và cách render nhiều code point; trong thực tế, các Emoji đơn giản ở mức Unicode 9 ổn định hơn
Hoạt ảnh terminal và kiểm soát nhấp nháy
- Các trình giả lập terminal hiện đại hoạt động trên những giao thức cũ, nhưng nhiều bản triển khai dùng các kỹ thuật đồ họa cũng được dùng trong trò chơi điện tử
- Hiệu ứng hình ảnh trong terminal dễ bị nhấp nháy hoặc tearing, nhưng nếu tuân thủ một vài điều kiện thì có thể triển khai hoạt ảnh mượt mà
- Cách đầu tiên để giảm nhấp nháy không phải là “clear rồi draw”, mà là ghi đè lên nội dung hiện có
- Nếu xóa màn hình rồi thêm nội dung mới, người dùng có thể thoáng thấy khung hình trống hoặc khung hình chỉ được vẽ một phần
- Cách thứ hai là ghi nội dung cập nhật ra đầu ra chuẩn bằng một lần write duy nhất
- Nếu gọi
file.writenhiều lần, các cập nhật từng phần có thể bị lộ ra cho người dùng thấy
- Nếu gọi
- Cách thứ ba là dùng giao thức Synchronized Output
- Khi báo cho terminal biết điểm bắt đầu và kết thúc của một frame, terminal có thể tận dụng điều đó để cập nhật không nhấp nháy
- Textual lấy 60fps làm chuẩn; cao hơn mức đó có thể không tạo ra khác biệt dễ nhận thấy
- Không phải hoạt ảnh nào cũng có giá trị như nhau
- Các hiệu ứng như sidebar trượt vào từ bên trái trông đẹp mắt, nhưng có thể không mang lại lợi ích thực tế cho trải nghiệm người dùng
- Cuộn mượt giúp giữ được vị trí hiện tại trong văn bản dài
- Vì lý do này, Textual dự định sẽ có cơ chế tắt một số hoạt ảnh nhất định
Dùng DictView để tìm phần thay đổi khi render
keys()vàitems()củadicttrong Python lần lượt trả vềKeysViewvàItemsView, cung cấp giao diện tương tự tập hợp- Quá trình layout của Textual tạo ra render map, tức ánh xạ giữa Widget và vị trí trên màn hình
- Trong cách làm trước đây, chỉ cần vị trí của một Widget thay đổi thì toàn bộ màn hình cũng bị cập nhật một cách lãng phí
- Dùng hiệu đối xứng của hai
ItemsViewcó thể lấy được các mục mới xuất hiện hoặc đã thay đổi- Phép toán này được xử lý ở mức C
- Textual dùng nó để tìm các vùng màn hình đã thay đổi khi thuộc tính CSS thay đổi, phục vụ cập nhật được tối ưu hóa
lru_cache và tối ưu các hàm nhỏ
@lru_cachetrong thư viện chuẩnfunctoolslà decorator dùng để cache giá trị trả về của hàm- Khi đặt
maxsize, có thể giới hạn tình trạng cache phình ra vô hạn lru_cachecủa CPython dùng phần triển khai C nên nhanh cả khi cache hit lẫn cache miss- Textual có những hàm nhỏ không chậm nhưng được gọi rất thường xuyên, và nhiều hàm trong số đó phù hợp để cache
- Thông thường, đặt
maxsizekhoảng 1000~4000 là đủ để phần lớn các lần gọi trở thành cache hit - Khi dùng cache, cần kiểm tra
cache_info()- Với một cache hiệu quả, có thể kỳ vọng
hitstăng nhanh hơnmisses
- Với một cache hiệu quả, có thể kỳ vọng
- Phương thức
intersection, tính phần giao nhau của hai vùng hình chữ nhật, bản thân khối lượng công việc không lớn nhưng được gọi hàng nghìn lần nên hưởng lợi từ caching
Sự đơn giản mà đối tượng bất biến mang lại
- Python không có đối tượng bất biến theo nghĩa thực sự, nhưng tuple,
NamedTuplevà frozen dataclass mang lại nhiều lợi ích - Ràng buộc không thể thay đổi đối tượng có thể trông như một hạn chế tùy ý, nhưng trong mã thực tế nó thường hữu ích
- Trong Textual, mã dùng đối tượng bất biến dễ suy luận hơn, và cũng thuận lợi cho cache cũng như kiểm thử
- Lý do cốt lõi là việc viết mã không có side effect trở nên dễ hơn
- Khi truyền instance của class vào hàm, khó tránh side effect theo cùng cách như vậy
Nghệ thuật Unicode và tài liệu hóa
- Khi khó giải thích nội dung kỹ thuật chỉ bằng lời, sơ đồ tạo bằng ký tự hộp Unicode giúp ích rất nhiều cho việc hiểu tài liệu
- Trong docstring của Textual, sơ đồ bằng ký tự hộp được dùng để giải thích một phương thức chia một vùng thành bốn vùng con
- Những sơ đồ như vậy không thay thế được docstring được viết tốt, nhưng khi dùng cùng nhau thì có thể tăng mức độ dễ hiểu
- Công cụ dùng để vẽ sơ đồ là monodraw
- Monodraw chỉ dành cho macOS
- Có thể có các lựa chọn thay thế trên nền tảng khác
Giảm lỗi layout bằng fractions.Fraction
- Module
fractionstrong thư viện chuẩn Python đã tồn tại từ Python 2.6 Fractionlà một cách khác để biểu diễn số, và sau khi tạo có thể dùng thay cho float- Số thực dấu phẩy động có những giới hạn đã biết rõ
- Ví dụ,
0.1 + 0.1 + 0.1 == 0.3làFalse
- Ví dụ,
- Trong Textual, lỗi làm tròn của số thực dấu phẩy động trở thành vấn đề trong các layout chia màn hình theo nhiều tỷ lệ
- Trong tình huống một panel chiếm 1/3 chiều rộng màn hình và phần 2/3 còn lại tiếp tục được chia, có thể xuất hiện một khoảng trống rộng một ký tự
- Thay float bằng
Fractioncó thể tránh loại lỗi làm tròn này- Tổng của ba
Fraction(1, 10)bằngFraction(3, 10)
- Tổng của ba
- Trong ví dụ chia 24 ký tự thành 7 phần, phiên bản dùng float bị thiếu một ký tự, còn phiên bản dùng Fraction lấp đầy toàn bộ chiều dài
Khó khăn của Emoji trong terminal
- Hỗ trợ Emoji đã là vấn đề từ những ngày đầu của Rich, và Textual cũng thừa hưởng cùng vấn đề đó
- Trong terminal, một ký tự thường chiếm một hoặc hai ô, và một số ký tự có độ rộng bằng 0
- Ký tự CJK chiếm gấp đôi không gian so với chữ cái phương Tây
- Để thực hiện các định dạng cơ bản như căn giữa hoặc vẽ hộp, cần biết độ rộng terminal của văn bản sẽ được xuất ra
- Vì vậy chỉ dùng
len(text)thì không thể biết văn bản sẽ chiếm bao nhiêu độ rộng trong terminal
- Cơ sở dữ liệu Unicode có ánh xạ giữa ký tự độ rộng đơn và ký tự độ rộng đôi
- Rich và Textual tra cứu cơ sở dữ liệu này cho mọi ký tự được xuất ra
- Công việc này không nhẹ, nhưng có thể làm đủ nhanh bằng kỹ thuật engineering và caching
- Dù Emoji có trong cơ sở dữ liệu Unicode, mỗi bản phát hành Unicode mới lại thêm Emoji mới nên kết quả khó dự đoán
- Khi xuất Emoji mới ra terminal, nó có thể hiển thị rộng một ô hoặc hai ô
- Cũng có thể không được render đúng
- Không có cách tiêu chuẩn để phát hiện ổn định phiên bản Unicode mà trình giả lập terminal đang dùng
- Cũng không có biến môi trường tiêu chuẩn
- Có heuristic xuất một chuỗi nhất định rồi hỏi vị trí con trỏ, nhưng trong thử nghiệm, ngay cả khi tưởng rằng đã biết cơ sở dữ liệu Unicode, terminal vẫn render Emoji theo cách không dự đoán được
- Emoji gồm nhiều code point làm vấn đề phức tạp hơn nữa
- Ví dụ, 👨🏻🦰 gồm 4 code point
- Một số terminal có thể render nó thành 4 ký tự, 2 ký tự, 1 ký tự, rộng một ô hoặc hai ô, hoặc 4 dấu
?
- Về thực tế, dùng Emoji của Unicode 9 tương đối ổn định trên nhiều nền tảng
- Tốt hơn là tránh Emoji mới và các ký tự gồm nhiều code point, dù hiện tại chúng hiển thị tốt trong terminal của bạn
Tuyển dụng tại Textualize
- Textualize đang tuyển dụng để xây dựng một framework TUI có thể thay thế một phần vai trò của trình duyệt
- Có thể xem thông tin tuyển dụng tại Jobs
1 bình luận
Ý kiến trên Hacker News
Thật thú vị là mọi nhà phát triển TUI cuối cùng đều đụng phải Unicode, và việc xử lý đúng ký tự quốc tế cùng emoji lại trở thành một dự án riêng biệt có quy mô tương đương hoặc còn lớn hơn cả dự án TUI ban đầu
Điều tương tự cũng đã xảy ra với rivo/tview, và thông qua gói rivo/uniseg được tạo ra từ đó, tôi nhận ra rằng những người duy trì các thư viện TUI khác cũng gặp đúng vấn đề này
Rốt cuộc, vì độ rộng ký tự không được chuẩn hóa và terminal thì phức tạp, ai cũng phải tự phát minh ra cách giải quyết riêng. Tác giả bài viết chỉ hỗ trợ Unicode 9, trong khi hiện tại Unicode đã là 15.1, nên sớm muộn gì cũng sẽ có phàn nàn rằng một số emoji hoặc ký tự quốc tế nào đó bị render sai, nên khó mà chắc đây có phải lời giải tốt hay không
Tôi đang bảo trì hai thư viện TUI dùng kỹ thuật này, và hỗ trợ emoji gần như là xuất sắc. Một trong số đó cũng dùng thư viện uniseg
https://mitchellh.com/writing/grapheme-clusters-in-terminals
Điều tôi không thích nhất ở textual là nó muốn trở thành React. Tôi hiểu vì sao họ làm vậy, vì đó là framework phổ biến mà nhiều người đã quen, nhưng tôi không nghĩ đó là cách tốt để xây dựng giao diện người dùng
Bản thân thiết kế phản ứng đã là một hướng đi được kiểm chứng, và dựa thiết kế hệ thống vào một cách làm vốn được biết là hoạt động tốt thực ra là cách hay để giảm rủi ro dự án
Nhưng dùng cả một biến thể của CSS thì có vẻ hơi quá. Mô hình React ưu tiên các component được chuẩn hóa, đồng thời ở nhiều khía cạnh cũng đã phá vỡ chính các khái niệm của CSS, và việc các nhà phát triển tùy biến component bằng CSS trông giống hệ quả phụ của quá trình tiến hóa của React hơn là một kiến trúc chính đáng
Lần cuối tôi dùng thì vẫn phải dùng CSS, và không có các component chuẩn đủ tốt nên phải tự làm. Thay vì component trở thành một lớp Python được đóng gói gọn gàng, tài liệu chính thức lại hướng dẫn kiểu tạo component danh sách rồi trang trí nó bằng stylesheet bên ngoài
Trong Python, nên có một cách hiển nhiên để làm một việc, và nếu có thể thì chỉ nên có đúng một cách. Việc bám theo React quá sát khiến nó có vẻ như đang sao chép cả những thói quen xấu lớn nhất của cộng đồng JavaScript
Còn chuyện không có component chuẩn tốt thì có vẻ cũng đã lâu rồi chưa kiểm tra: https://textual.textualize.io/widget_gallery/
Tôi cũng không cần tự tạo component, và CSS thì vốn đã biết rồi. Ngược lại, chính một vài khác biệt về bố cục và những chỗ lệch khỏi CSS mới đôi lúc làm tôi bối rối
Hãy hình dung một component trong cửa sổ GUI bình thường lại chính là một cửa sổ terminal. Tôi muốn biết liệu đã có thứ như vậy chưa, hay là rốt cuộc chỉ nên dùng text view với font đơn cách
TUI này nhìn đẹp đấy, nhưng tôi khó mà hình dung ra tình huống thực tế mình sẽ dùng nó, thậm chí còn trả tiền cho nó. Có thể là vì tôi không ở đúng môi trường phù hợp, nhưng theo kinh nghiệm của tôi thì mọi người либо hài lòng với những công cụ thật tối giản, либо muốn làm người dùng hài lòng ngay bằng GUI
Ví dụ, liên kết YouTube trong bài cho thấy một bảng có ô được tô nổi bật, nhưng tôi không hiểu tại sao phải làm việc đó bằng TUI. Nếu cần di chuyển ô đang được tô nổi bật thì cuối cùng sẽ cần thêm rất nhiều chức năng khác, và rốt cuộc có lẽ sẽ cần một GUI đúng nghĩa
Tuy vậy, tôi cũng không rõ “GUI đúng nghĩa” là gì. Terminal có những tiêu chuẩn mở được dùng rộng rãi, và các giao thức này còn chuẩn hóa, tương thích liên vận hành và được hỗ trợ trên mọi hệ thống hơn bất kỳ framework GUI nào
Nếu thêm vào các yếu tố như video và lấp đầy một vài khoảng trống, thì việc cạnh tranh với trình duyệt hoặc mang toàn bộ trải nghiệm máy tính vào terminal cũng có vẻ hợp lý
Tất nhiên, nếu terminal thêm video thì đó cũng là đang sao chép pipeline render của một “GUI đúng nghĩa”, nhưng đó vẫn là cải thiện trải nghiệm terminal, và nếu thành công lớn thì cũng có thể xem là mang sự đơn giản kiểu UNIX vào GUI
Họ làm việc cực kỳ hiệu quả bằng phím tắt, và mỗi lần ai đó cố thay AS400 bằng một ứng dụng web hiện đại thì năng suất lại giảm đi 100 lần
Tình huống này giống như khác biệt giữa cao thủ Vim/Emacs và một GUI trông bóng bẩy nhưng không có phím tắt
Có thể để người dùng SSH vào server và dùng ứng dụng TUI như một shell, dù tôi cũng thừa nhận đây là một kịch bản hơi gượng ép
Cá nhân tôi thấy Textual hơi lạ khi dùng, nhưng vẫn tốt hơn ncurses. Chỉ là nó không cho ra kết quả tôi muốn. Các ứng dụng TUI kiểu mainframe ngày xưa thì lại cho cảm giác cực kỳ hiệu quả
Đôi khi nó vẫn hoạt động ổn, nhưng chỉ trong những trường hợp hạn chế, chẳng hạn dùng X forwarding trên kết nối băng thông lớn. Trong các trường hợp khác thì GUI trở nên ì ạch hoàn toàn
GUI dựa trên văn bản có vẻ có thể lấy được ưu điểm của cả hai phía
Cách làm kiểu “mẹo đầu tiên là đừng xóa mà hãy ghi đè” cũng chính là cách người ta viết game trước thời DirectX
Ghi trực tiếp vào frame buffer, và thay vì xóa toàn bộ rồi vẽ lại, họ chỉ vẽ lại phần đã thay đổi cùng vùng xung quanh và phía dưới nó. Lý do là không đủ thời gian để cập nhật toàn màn hình đúng lúc
Thứ nhất là ghi vào một buffer khác, rồi khi frame kết thúc thì sao chép toàn bộ buffer đó trong một lần để mỗi pixel chỉ thay đổi đúng một lần. Buffer này thường có thể nằm trong RAM thường thay vì video RAM
Thứ hai là ghi vào một buffer khác trong video RAM, rồi thay đổi thanh ghi phần cứng đồ họa để màn hình tạo pixel từ buffer đó
Mỗi cách có điểm đánh đổi khác nhau. Sao chép cả buffer thì tốn kém, còn đổi thanh ghi địa chỉ thì rẻ. Nhưng chi tiết thanh ghi có thể phụ thuộc phần cứng, và khi đó cũng chưa có framework driver đồ họa tử tế
Để chỉ đơn giản là “lật buffer”, khi render vào buffer ngoài màn hình bạn vẫn phải đẩy pixel vào video RAM, mà theo tôi nhớ thì truy cập video RAM chậm hơn RAM thường. Nó gần như là một cấu trúc NUMA, nên tùy vào việc pixel bị vẽ chồng lại thường xuyên đến mức nào, việc render trong RAM thường rồi sao chép lần cuối có khi vẫn nhanh hơn tổng thể
Tuy vậy, tôi cho rằng khả năng báo cho terminal biết điểm bắt đầu và kết thúc của một frame còn mạnh hơn nhiều
Tôi thực sự không hiểu vì sao các kỹ sư phần mềm lại hứng thú với TUI đến vậy. Tôi thích chương trình dòng lệnh tốt, nhưng TUI thì không hấp dẫn tôi
Hơn nữa, nó còn có chất hacker. Phong cách không bị tách biệt mạnh khỏi nội dung, và các tùy chọn style có thể chỉnh cũng khá hạn chế, nên các ứng dụng TUI thường hội tụ về một kiểu chung. Kiểu đó gợi nhớ đến phim hacker và sci-fi ngầu
Đây cũng không phải lãnh địa mà các nhà thiết kế doanh nghiệp yêu thích, và các công ty cũng không chốt bán hàng cho doanh nghiệp hay người tiêu dùng chỉ dựa vào TUI. Vì không có áp lực thương mại như vậy nên TUI được các nhà phát triển thiết kế cho chính các nhà phát triển
Vì thế, meme TUI tự củng cố chính nó. Các nhà phát triển nhìn thấy và dùng TUI thường cảm nhận đây là công cụ được làm với nhà phát triển trong đầu, rồi lại muốn tự tạo TUI của riêng mình
Đa nền tảng gần như miễn phí, không cần tạo GUI hoàn hảo đến từng pixel, mà thực ra cũng không thể, và cũng không cần icon hay đồ họa. Vì bạn vốn không thể hiển thị đồ họa
Bạn có thể tiếp tục dùng terminal emulator với phông chữ mình thích, đồng thời vẫn có một giao diện kiểu GUI
Tất cả các ứng dụng TUI tôi từng dùng đều có độ phản hồi thấp. Ứng dụng GUI nhìn chung cũng nhanh, nhưng những ứng dụng chậm đến mức gây bực bội mà tôi từng gặp thì đều là GUI
Có ascii-draw là một lựa chọn thay thế cho Monodraw
https://github.com/Nokse22/ascii-draw
Hầu như mọi thứ tôi thấy sau TurboVision đều có lấy cảm hứng từ nó, nhưng thực tế thì độ hoàn thiện còn thiếu. Khi thực sự dùng thử các toolkit, chúng trông dễ thương đấy nhưng lại cho cảm giác như còn dang dở
Nói cách khác, cũng có thể nói rằng nhiều framework TUI tôi từng thấy là những framework “mang tính áp đặt quan điểm”
Có lẽ là do tôi chậm hiểu và không sáng tạo lắm, nhưng từ sau khi chuyển từ DOS sang Linux vào thập niên 90, tôi chưa từng có lúc nào làm việc hiệu quả như thời còn làm với B800
Có thể là tôi đã mất rất lâu mới hiểu được sự cần thiết của double buffering, sự khác biệt giữa text mode cục bộ truy cập trực tiếp và terminal, rồi cả escape sequence. Dù vậy, mỗi lần tôi định tự làm gì đó với ncurses thì cảm giác không thỏa mãn rất rõ rệt lại khiến tôi gần như bỏ cuộc. Nó hoàn toàn khác với những gì có thể làm chỉ với sự lý tưởng đơn giản của B800
Không biết có phải tầm nhìn của tôi bị che khuất cỡ cả một thành phố không, nhưng mọi framework GUI sau message pump đều cực kỳ khó hiểu với tôi
Đặc biệt, tôi không hiểu các framework hướng đối tượng thực sự giải quyết vấn đề UI đa luồng như thế nào. Với Win32, tôi đặt renderer chính vào một thread, các renderer phụ thì dựng model ở bên cạnh rồi chỉ cập nhật phần khác biệt. Mã nguồn cũng không hề phình to đến mức không kiểm soát nổi
Nếu đã chạy kitty thì bạn có thể làm được nhiều hơn thế rất nhiều: https://m.youtube.com/watch?v=ft1Q-DwGWIs
https://notcurses.com/
Cả hai đều có thể dùng cùng một giao thức để hiển thị đồ họa. Nếu thực sự cần đồ họa thì terminal hầu như không phải là lời giải đúng. Tuy vậy, với những thứ nhỏ như biểu tượng thì đôi khi vẫn hữu ích
Điều thú vị là họ đang tuyển dụng. Tôi tò mò họ định kiếm tiền bằng framework TUI như thế nào
Nhân tiện, tôi đã đánh giá khoảng 12 thư viện TUI có thể biên dịch được, và FTXUI là cái dễ dùng và ổn định nhất
https://github.com/ArthurSonzogni/FTXUI
Đây là công cụ rất phù hợp để tạo các dashboard tương tác hỗ trợ cả bàn phím lẫn chuột