1 điểm bởi GN⁺ 1 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Giả định rằng ứng dụng terminal vốn dĩ dễ tiếp cận chỉ vì dựa trên văn bản bị phá vỡ trong các TUI hiện đại; các framework như Ink, Bubble Tea, tcell có thể tạo ra môi trường còn thù địch hơn với người dùng trình đọc màn hình
  • CLI xuất ra dưới dạng luồng tuyến tính của stdin/stdout, nơi nội dung tích lũy theo thời gian; còn TUI xử lý terminal như một lưới 2D dựa trên các ô ký tự, khiến trình đọc màn hình khó theo dõi luồng hơn
  • gemini-cli có thể khiến Ink liên tục vẽ lại cây component React để khớp với lưới terminal, di chuyển con trỏ giữa spinner, bộ đếm thời gian và lịch sử hội thoại, từ đó gây đọc lặp lại, crash và độ trễ nhập liệu cho Speakup và NVDA
  • Những công cụ cũ như nano, vim, menuconfig, Irssi giảm nhiễu cập nhật tọa độ và hạn chế can thiệp vào dòng nhập bằng cách ẩn con trỏ, giữ tiêu điểm một cột duy nhất và tận dụng vùng cuộn của VT100
  • Để tạo công cụ terminal có khả năng truy cập tốt, cần tránh các framework UI khai báo xử lý terminal như canvas và việc vẽ lại quá mức, đồng thời đảm bảo hành vi gần với luồng CLI đơn giản, tuyến tính

Hiểu lầm rằng “vì là văn bản nên sẽ dễ tiếp cận”

  • Giả định rằng các ứng dụng chạy trong terminal vốn dĩ có khả năng truy cập tốt là điều không phù hợp với môi trường sử dụng thực tế
  • Kỳ vọng rằng trình đọc màn hình có thể dễ dàng diễn giải văn bản ASCII thô vì không có đồ họa, DOM phức tạp hay canvas WebGL bị phá vỡ trong TUI hiện đại
  • Các framework terminal UI như Ink (JS/React), Bubble Tea (Go), tcell cố gắng cải thiện trải nghiệm lập trình viên (DX), nhưng với người dùng khiếm thị, chúng có thể tạo ra môi trường còn thù địch hơn
  • Trong nhiều trường hợp, TUI hiện đại còn tệ hơn về mặt khả năng truy cập so với một giao diện đồ họa được triển khai kém

Khác biệt cấu trúc giữa CLI và TUI

  • CLI: luồng tuyến tính

    • CLI hoạt động dựa trên stdin/stdout; khi nhập lệnh, kết quả được thêm xuống dưới và con trỏ đi xuống
    • Vì đầu ra có tính tuyến tính và tích lũy theo thời gian, nó phù hợp với các trình đọc màn hình ở cấp kernel như Speakup
  • TUI: lưới 2D

    • TUI không xem cửa sổ terminal là luồng văn bản mà là một lưới 2D, nơi từng ô ký tự được dùng như pixel
    • Khi từ bỏ dòng chảy theo thời gian để ưu tiên bố cục không gian, nó tạo ra cấu trúc mà trình đọc màn hình khó bám theo

Những vấn đề bộc lộ trong gemini-cli

  • gemini-cli là công cụ được viết bằng Node.js và framework Ink, bề ngoài trông như một giao diện chat đơn giản
  • Nhưng bên trong, Ink cố gắng điều chỉnh cây component React để khớp với lưới terminal
  • Khi dùng với Speakup (Linux) hoặc NVDA (Windows), ứng dụng không chỉ đơn thuần thất bại mà còn liên tục dội nội dung để trình đọc màn hình đọc lên
  • Màn hình vận hành như một canvas phản ứng

    • Vì framework xử lý màn hình như một canvas phản ứng, mọi cập nhật đều kích hoạt vẽ lại
    • Khi AI đang “suy nghĩ”, để cập nhật bộ đếm hay spinner, nó di chuyển con trỏ phần cứng tới vị trí bộ đếm, ghi thời gian mới rồi đưa con trỏ về vị trí ban đầu
    • Với người dùng nhìn thấy màn hình, đó là hành vi thoáng qua; nhưng với người dùng trình đọc màn hình, nó lặp đi lặp lại như “Responding... Time elapsed 1s... Responding... Time elapsed 2s...”
    • Khi con trỏ tức thời nhảy giữa chỉ báo trạng thái, spinner và lịch sử hội thoại, Speakup sẽ cố đọc nội dung nằm dưới con trỏ tại thời điểm đó
    • Kết quả là cập nhật bộ đếm và các mảnh hội thoại bị trộn lẫn, khiến rất khó tập trung vào nội dung thực sự đang nhập
  • Sự bất ổn với NVDA và thao tác dán

    • Trên Windows, khi mở terminal bằng NVDA, SSH vào máy Linux rồi gắn vào một session screen và dán văn bản, NVDA có thể crash ngay lập tức hoặc khiến cả hệ thống trở nên rất bất ổn
    • Mỗi lần nhập ký tự hay dán văn bản, trạng thái ứng dụng thay đổi và framework kết luận rằng phải render lại giao diện
    • Nếu lịch sử hội thoại nằm trong state, nó sẽ cố vẽ lại hoặc tính toán lại bố cục của hàng nghìn dòng văn bản ngay lập tức
    • Càng nhiều tin nhắn trong hội thoại, vấn đề này càng xảy ra thường xuyên hơn
    • Ngay cả tổ hợp Insert+5, vốn dùng để tránh thông báo nội dung động, cũng không thể né được vấn đề này
  • Vòng lặp độ trễ nhập liệu

    • Khi các framework như Ink chạy trong môi trường đơn luồng như Node.js, hiệu năng sẽ giảm mạnh khi lịch sử ngày càng dài
    • Nếu dán một khối văn bản lớn, nó phải tính diff cho hàng nghìn dòng
    • Hệ thống bận tính toán cách vẽ lại màn hình nên việc xử lý nhập liệu bị chậm lại
    • Chỉ cần nhấn một phím cũng có thể phải đợi tới 10 giây trước khi ký tự hiện lại

Vì sao các công cụ cũ vẫn hoạt động được

  • Không phải các công cụ như nano, vim, menuconfig luôn hoàn hảo về khả năng truy cập nên mới được dùng
  • Điểm cốt lõi là chúng có thể ẩn hoàn toàn con trỏ hoặc giảm nhiễu phát sinh từ việc theo dõi vị trí con trỏ
  • nanovim: ẩn con trỏ

    • Nếu chạy nano với tùy chọn hiển thị vị trí con trỏ như --constantshow, hoặc dùng vim mà không có cấu hình thích hợp, khả năng sử dụng có thể bị phá vỡ
    • Khi con trỏ hiển thị và việc theo dõi được bật, Speakup sẽ ưu tiên thông báo cập nhật vị trí con trỏ hơn là echo ký tự
    • Khi người dùng nhập “a”, thay vì nghe “a” họ sẽ nghe “Column 2”; nhập “b” thì nghe “Column 3”
    • Các công cụ cũ này có thể được cấu hình để hạn chế cập nhật con trỏ thị giác hoặc thanh trạng thái, nhờ đó buộc trình đọc màn hình dựa vào luồng ký tự nhập thay vì các cập nhật tọa độ
    • Các framework hiện đại thường không cung cấp chế độ “no-cursor” hay “headless”, mà mặc định cho rằng con trỏ thị giác là bắt buộc
  • menuconfig: tiêu điểm một cột duy nhất

    • menuconfig của nhân Linux hoạt động được vì nó duy trì nghiêm ngặt tiêu điểm một cột duy nhất
    • Dù có viền và tiêu đề, vùng hoạt động vẫn là một danh sách dọc, và con trỏ được cố định trong danh sách đó
    • Con trỏ không chạy xuống góc phải dưới để cập nhật đồng hồ rồi lại nhảy lên góc trái trên để cập nhật tiêu đề
    • Độ phức tạp không gian được giữ thấp nên trình đọc màn hình không bị lạc hướng
  • Irssi: tận dụng vùng cuộn

    • Irssi không vô tình mà dễ tiếp cận; đó là một công cụ chat hơn 20 năm nay đã tận dụng vùng cuộn của VT100 thông qua engine render tùy biến
    • Khi có tin nhắn mới đến, nó ra lệnh cho terminal driver “hãy đặt từ dòng 1 đến dòng 23 làm vùng cuộn”
    • Sau đó nó gửi lệnh “cuộn lên”, terminal sẽ đẩy nội dung lên trên rồi vẽ văn bản mới ở cuối vùng đó
    • Cách này giảm can thiệp vào dòng nhập xuống mức tối thiểu
    • Thay vì tự tay ghi lại mọi ký tự trên màn hình, nó dựa vào khả năng phần cứng của terminal
    • Các framework hiện đại bỏ qua những khả năng phần cứng như vậy và chọn cách tính diff trạng thái màn hình rồi ghi lại ký tự, điều này tốn chi phí tính toán hơn và thù địch với khả năng truy cập

Vấn đề trong cách xử lý issue của gemini-cli

  • Google và những người bảo trì gemini-cli có vẻ như quan tâm tới khả năng truy cập, nhưng trong kho mã, các hồi quy nghiêm trọng về khả năng truy cập vẫn bị bỏ mặc
  • Các hồi quy về khả năng truy cập như Issue #3435 và Issue #11305 không có thảo luận, lộ trình hay bản sửa lỗi nào
  • Issue #1553 từng là issue để theo dõi những thất bại về khả năng truy cập như vậy, nhưng cũng không được giải quyết và đã bị bot tự động đóng
  • Bot kết thúc issue bằng lời nhắn chung chung rằng do lâu không có hoạt động và để quản lý backlog nên issue bị đóng
  • Việc đóng các báo cáo khả năng truy cập chỉ vì người bảo trì không đụng tới chúng trong nhiều tháng không phải là dọn dẹp mà giống như che giấu bằng chứng
  • Nó phát đi tín hiệu rằng nếu một lỗi bị phớt lờ đủ lâu thì coi như nó không còn tồn tại, trong khi phần mềm thực tế vẫn không thể dùng được với người dùng khiếm thị
  • Chỉ số “Closed Issues” của dự án có thể đẹp hơn, nhưng vấn đề khả năng truy cập thì không được giải quyết

Kết luận để xây dựng công cụ terminal có khả năng truy cập tốt

  • Nếu thực sự coi trọng khả năng truy cập trong ứng dụng terminal, cần ngừng dùng các framework UI khai báo xem terminal như một canvas
  • Stack TUI “hiện đại” được tối ưu để giúp lập trình viên viết code theo kiểu React dễ hơn, nhưng đánh đổi bằng khả năng render văn bản hiệu quả cho máy
  • Nếu ứng dụng không bảo đảm cho người dùng có thể ẩn con trỏ, hoặc phụ thuộc vào việc vẽ lại quá mức để hiển thị spinner và bộ đếm thời gian, thì đó là một công cụ không thể truy cập được
  • Với người dùng khiếm thị, một luồng CLI đơn giản và tuyến tính tốt hơn nhiều so với một TUI “thông minh” nhưng chậm, liên tục tuôn nội dung để đọc và làm con trỏ nhảy khắp màn hình

1 bình luận

 
Ý kiến trên Lobste.rs
  • Bài này, cũng như nhiều bài blog khác, có mùi viết với AI hỗ trợ rất rõ
    LLM rất thích kiểu tiêu đề như thế này: “The Architectural Flaw”, “The Lag Loop”, “Why The ‘Old Guard’ Works”, “The Lost Art of Scrolling Regions”, “The ‘Stale Bot’ excuse: A Case Study in Neglect”

    • Ngay từ tiêu đề đã đọc như bài sản xuất hàng loạt bằng AI. Chủ đề thì bản thân nó mới mẻ nên có lẽ báo spam là hơi quá tay, nhưng 1) tôi không chịu nổi cái cùng một giọng văn lặp đi lặp lại ở khắp nơi nữa và 2) điều đó còn khiến tôi nghi ngờ cả độ chính xác của nội dung
      Nó đã có thể là một bài blog rất hay, nhưng nếu trông như tác giả chỉ ném dàn ý vào ChatGPT rồi thôi thì cả độc giả lẫn tác giả đều thiệt
    • Một trong những đặc điểm tôi ghét nhất ở văn phong LLM là viết kiểu “The <một khái niệm hoàn toàn chưa từng được xác lập>” để làm như đó là một khái niệm chính thức
      Gọi một vấn đề rất đặc thù và chỉ xảy ra một lần là vấn đề “kinh điển” cũng y hệt như vậy
  • Thật sự rất buồn. Tóm lại là vẫn có những TUI có khả năng tiếp cận như Irssi, nhưng các framework TUI hiện đại lại phớt lờ những tiền lệ đó và phụ thuộc vào diff dạng lưới cùng di chuyển con trỏ
    Công cụ đọc màn hình sẽ đọc nội dung tại vị trí con trỏ khi con trỏ di chuyển, nên kết quả là mọi thứ bị đảo lộn hoặc tạo ra một đống spam đọc thành tiếng khổng lồ

  • Tôi nghi ngờ phần giải thích kỹ thuật ở đây có hoàn toàn chính xác hay không
    Đặc biệt là Ink trong thời gian dài hoàn toàn không hỗ trợ render tăng dần, và phần lớn ứng dụng dùng Ink đến giờ vẫn chưa bật nó. Ngay cả render tăng dần đó cũng là theo dòng, nên nó không thật sự đưa con trỏ tới vị trí timer như mô tả
    Gemini CLI muốn bật render tăng dần thì cần dùng alternate buffer, và điều này sẽ bị vô hiệu hóa khi bật chế độ thân thiện với trình đọc màn hình tích hợp. Tài liệu về tùy chọn liên quan ở đây
    Nói thêm thì rich/textual của Python dù chạy trên một ngôn ngữ chậm hơn và chủ yếu đơn luồng vẫn thường nhanh hơn Ink rất nhiều. Việc diff hàng nghìn dòng không nhất thiết phải chậm đến mức đó, càng không phải mất tới 10 giây
    Tôi không nghi ngờ rằng trải nghiệm người dùng là bực bội và hỏng hóc, nhưng nguyên nhân chính xác được nêu ra có vẻ có thể là do LLM bịa ra hoặc dựa trên thông tin chưa đầy đủ. Render tăng dần của Ink, ngay cả khi được bật, cũng không hoạt động như bài viết mô tả
    Trên thực tế, có lẽ việc vẽ lại toàn màn hình mới là thứ làm trình đọc màn hình bối rối, còn việc vẽ lại theo dòng thì lại khiến nó đọc lại những mảnh văn bản rời rạc ngẫu nhiên không liên quan tới thay đổi

  • Chỉ đổ lỗi cho TUI thì không công bằng
    Vấn đề thật sự là hỗ trợ khả năng tiếp cận của gần như toàn bộ stack đều tệ hại
    Thứ nhất, phần lớn terminal emulator dùng render bằng GPU hoàn toàn không dùng API khả năng tiếp cận do hệ thống cung cấp. Khi văn bản được render bằng GPU, công cụ hỗ trợ tiếp cận không thể “đọc” được và nó chỉ trông như một hình ảnh. Kitty, Alacritty, WezTerm đều thuộc nhóm này. Terminal Ghostty của tôi thì trên macOS có thể đọc qua API khả năng tiếp cận, iTerm2 và Terminal.app cũng vậy
    Thứ hai, hoàn toàn không có terminal sequence hay chuyển động chuẩn nào để TUI truyền thông tin khả năng tiếp cận cho terminal emulator. Cần một thứ tương đương chú thích kiểu ARIA cho ô terminal, đoạn chạy, vùng, nhưng chẳng có nỗ lực nào như vậy cả. Dù TUI có xử lý con trỏ tốt thì trong nhiều trường hợp sử dụng vấn đề vẫn sẽ nổ ra
    Ví dụ, ở Ghostty tôi đã làm việc để tích hợp OSC133 với API khả năng tiếp cận nhằm phơi bày từng shell prompt, input và command thành các phần tử có ý nghĩa về mặt cấu trúc chứ không chỉ là hộp văn bản đơn thuần. Điều đó cho thấy đặc tả terminal, TUI và terminal emulator phải ăn khớp với nhau
    Cả stack đều mục ruỗng, và cũng gần như không có ai thực sự muốn sửa nó. Bản thân tôi cũng chỉ cố hết sức trong giới hạn thời gian, nhưng đây là một chủ đề khổng lồ còn đòi hỏi cả chính trị trong hệ sinh thái nên rất khó gánh nổi
    Thêm nữa, một thực tế vừa thú vị vừa đáng sợ là AI đang giúp cải thiện khả năng tiếp cận ở đây. Nhiều công cụ AI dùng hoặc lạm dụng API khả năng tiếp cận để đọc danh sách cửa sổ và thực hiện nhập liệu. Vì thế ngày càng nhiều ứng dụng bắt đầu coi trọng tích hợp khả năng tiếp cận hơn nhiều vì các trường hợp sử dụng AI

    • Terminal thật sự đang dần trở thành một trình duyệt nhỏ
  • Tôi bực mỗi ngày vì Claude Code và gemini-cli không dựa trên readline
    Họ có thêm vào vài phím bấm tương tự, nhưng phần đuôi dài của những phím tắt readline quen thuộc thì lại thiếu
    Anthropic hoàn toàn có thể thừa nhận rằng quyết định “phải làm nó giống phát triển web” là một sai lầm rồi bắt đầu lại bằng readline
    Suy nghĩ rằng trải nghiệm phát triển quen thuộc với lập trình viên làm ra công cụ quan trọng hơn trải nghiệm người dùng quen thuộc với người sử dụng công cụ là sai

    • Theo tôi hiểu thì một phần lớn của vấn đề là Ink về cơ bản chỉ hài lòng làm backend render và không cung cấp widget nhập liệu
      Thực tế cũng gần như không có giải pháp bên thứ ba nổi tiếng nào được duy trì tốt. Nếu cần một ô nhập linh hoạt thì bạn phải tự làm từ đầu
      Điều này đối lập với widget Input rất tốt của Textual hay OpenTUI là một thư viện khác trong hệ sinh thái JS
    • readline chẳng phải dùng giấy phép GNU sao? Cuối cùng đã có ai làm ra phiên bản không phải GPL rồi à?
      Tôi không thích LLM nên UI tệ với cá nhân tôi lại là một điểm cộng, nhưng có lẽ cũng có lý do khiến họ không dùng readline
  • Các trình soạn thảo terminal như kakoune, helix có lẽ sẽ khó vượt qua tiêu chuẩn khả năng tiếp cận trừ khi dùng mẹo “ẩn con trỏ”
    Dù vậy, có lẽ chúng vẫn không thể tiếp cận tốt bằng VS Code
    Ngoài VS Code ra thì còn IDE-lite hay IDE đa nền tảng nào có khả năng tiếp cận tốt không? Tôi không thích thái độ ngày càng thù địch của VS Code. Có thể là IDE của JetBrains

    • emacspeak, và nó cung cấp giao diện rất tốt về mặt khả năng tiếp cận cho Emacs
      Điểm trừ là Emacs bản thân nó đa nền tảng, nhưng emacspeak có thể phụ thuộc phần nào vào Linux vì TTS. Hoặc cũng có thể không. Tôi chưa từng thử trên Windows
    • Trước hết phải xác định là khả năng tiếp cận cho ai. Nếu là cho người khiếm thính thì cần cung cấp chữ viết bằng ngôn ngữ địa phương và ngôn ngữ ký hiệu địa phương. Nếu ở Mỹ thì thường là ASL
      Nếu là khả năng tiếp cận cho người khiếm thị thì cần emacspeak hoặc công cụ hỗ trợ tiếp cận dành cho người khiếm thị của nền tảng đó
      Khả năng tiếp cận là một phổ, không phải một ô checkbox
  • Links có hẳn chế độ terminal chữ nổi Braille, trong đó các thành phần GUI giả được đổi thành menu toàn màn hình đơn giản hơn và điều hướng bằng phím mũi tên cũng chuyển sang theo từng dòng
    Một ví dụ thú vị khác là edbrowse. Đây là trình duyệt chế độ văn bản do Karl Dahlke, một người khiếm thị, tạo ra; khác với các trình duyệt web văn bản phổ biến hơn, nó không dùng TUI mà dùng giao diện dòng lệnh kiểu ed

  • Nếu là framework Ink thì đó có thể là lý do CLI cứ ăn 100% CPU rồi treo mãi trong khi liên tục vẽ lại lịch sử chat dài. Thật đáng tiếc