分类 技术 下的文章

快速入门使用 Textual

Python 在 Linux 上有像 TkInter 这样的优秀 GUI(图形用户界面)开发库,但如果你不能运行图形应用程序怎么办?

文本终端,并非只在 Linux 上有,而且 BSD 和其它的出色的类 Unix 操作系统上也有。如果你的代码是用 Python 编写的,你应该使用 Textual 来帮助你编写 TUI(文本用户界面)。在这个快速介绍中,我将向你展示两个你可以用 Textual 做的示例,并且介绍它未来可能的应用方向。

所以 Textual 是什么?

Textual 是一个为 Python 构建的快速应用程序开发框架,由 Textualize.io 构建。它可以让你用简单的 Python API 构建复杂的用户界面,并运行在终端或网络浏览器上!

你需要的跟进这个教程的工具

你需要有以下条件:

  1. 具备基础的编程经验,最好熟练使用 Python。
  2. 理解基础的面向对象概念,比如类和继承。
  3. 一台安装了 Linux 与 Python 3.9+ 的机器
  4. 一款好的编辑器(Vim 或者 PyCharm 是不错的选择)

我尽可能简单化代码,以便你能轻松理解。此外,我强烈建议你下载代码,或至少按照接下来的说明安装相关程序。

安装步骤

首先创建一个虚拟环境:

python3 -m venv ~/virtualenv/Textualize

现在,你可以克隆 Git 仓库并创建一个可以编辑的发布版本:

. ~/virtualenv/Textualize/bin/activate
pip install --upgrade pip
pip install --upgrade wheel
pip install --upgrade build
pip install --editable .

或者直接从 Pypi.org 安装:

. ~/virtualenv/Textualize/bin/activate
pip install --upgrade KodegeekTextualize

我们的首个程序:日志浏览器

这个 日志浏览器 就是一款简单的应用,能执行用户 PATH 路径上的一系列 UNIX 命令,并在任务执行完毕后捕获输出。

以下是该应用的代码:

import shutil
from textual import on
from textual.app import ComposeResult, App
from textual.widgets import Footer, Header, Button, SelectionList
from textual.widgets.selection_list import Selection
from textual.screen import ModalScreen
# Operating system commands are hardcoded
OS_COMMANDS = {
    "LSHW": ["lshw", "-json", "-sanitize", "-notime", "-quiet"],
    "LSCPU": ["lscpu", "--all", "--extended", "--json"],
    "LSMEM": ["lsmem", "--json", "--all", "--output-all"],
    "NUMASTAT": ["numastat", "-z"]
}

class LogScreen(ModalScreen):
    # ... Code of the full separate screen omitted, will be explained next
    def __init__(self, name = None, ident = None, classes = None, selections = None):
        super().__init__(name, ident, classes)
        pass

class OsApp(App):
    BINDINGS = [
        ("q", "quit_app", "Quit"),
    ]
    CSS_PATH = "os_app.tcss"
    ENABLE_COMMAND_PALETTE = False  # Do not need the command palette

    def action_quit_app(self):
        self.exit(0)

    def compose(self) -> ComposeResult:
        # Create a list of commands, valid commands are assumed to be on the PATH variable.
        selections = [Selection(name.title(), ' '.join(cmd), True) for name, cmd in OS_COMMANDS.items() if shutil.which(cmd[0].strip())]
        yield Header(show_clock=False)
        sel_list = SelectionList(*selections, id='cmds')
        sel_list.tooltip = "Select one more more command to execute"
        yield sel_list
        yield Button(f"Execute {len(selections)} commands", id="exec", variant="primary")
        yield Footer()

    @on(SelectionList.SelectedChanged)
    def on_selection(self, event: SelectionList.SelectedChanged) -> None:
        button = self.query_one("#exec", Button)
        selections = len(event.selection_list.selected)
        if selections:
            button.disabled = False
        else:
            button.disabled = True
        button.label = f"Execute {selections} commands"

    @on(Button.Pressed)
    def on_button_click(self):
        selection_list = self.query_one('#cmds', SelectionList)
        selections = selection_list.selected
        log_screen = LogScreen(selections=selections)
        self.push_screen(log_screen)

def main():
    app = OsApp()
    app.title = f"Output of multiple well known UNIX commands".title()
    app.sub_title = f"{len(OS_COMMANDS)} commands available"
    app.run()

if __name__ == "__main__":
    main()

现在我们逐条梳理一下程序的代码:

  1. 每个应用都扩展自 App 类。其中最重要的有 composemount 等方法。但在当前应用中,我们只实现了 compose
  2. compose 方法中,你会返回一系列 组件 Widget ,并按顺序添加到主屏幕中。每一个组件都有定制自身外观的选项。
  3. 你可以设定单字母的 绑定 binding ,比如此处我们设定了 q 键来退出应用(参见 action_quit_app 函数和 BINDINGS 列表)。
  4. 利用 SelectionList 组件,我们展示了待运行的命令列表。然后,你可以通过 @on(SelectionList.SelectedChanged) 注解以及 on_selection 方法告知应用获取所选的内容。
  5. 对于无选定元素的应对很重要,我们会根据运行的命令数量来决定是否禁用 “exec” 按钮。
  6. 我们使用类似的监听器( @on(Button.Pressed) )来执行命令。我们做的就是将我们的选择送到一个新的屏幕,该屏幕会负责执行命令并收集结果。

你注意到 CSS_PATH = "os_app.tcss" 这个变量了吗?Textual 允许你使用 CSS 来控制单个或多个组件的外观(色彩、位置、尺寸):

Screen {
        layout: vertical;
}

Header {
        dock: top;
}

Footer {
        dock: bottom;
}

SelectionList {
        padding: 1;
        border: solid $accent;
        width: 1fr;
        height: 80%;
}

Button {
        width: 1fr
}

引自 Textual 官方网站:

Textual 中使用的 CSS 是互联网上常见 CSS 的简化版本,容易上手。

这真是太棒了,只需要用一哥独立的 样式表,就可以轻松调整应用的样式。

好,我们现在来看看如何在新屏幕上展示结果。

在新屏幕上展示结果

以下是在新屏幕上处理输出的代码:

import asyncio
from typing import List
from textual import on, work
from textual.reactive import reactive
from textual.screen import ModalScreen
from textual.widgets import Button, Label, Log
from textual.worker import Worker
from textual.app import ComposeResult

class LogScreen(ModalScreen):
    count = reactive(0)
    MAX_LINES = 10_000
    ENABLE_COMMAND_PALETTE = False
    CSS_PATH = "log_screen.tcss"

    def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            selections: List = None
    ):
        super().__init__(name, ident, classes)
        self.selections = selections

    def compose(self) -> ComposeResult:
        yield Label(f"Running {len(self.selections)} commands")
        event_log = Log(
            id='event_log',
            max_lines=LogScreen.MAX_LINES,
            highlight=True
        )
        event_log.loading = True
        yield event_log
        button = Button("Close", id="close", variant="success")
        button.disabled = True
        yield button

    async def on_mount(self) -> None:
        event_log = self.query_one('#event_log', Log)
        event_log.loading = False
        event_log.clear()
        lst = '\n'.join(self.selections)
        event_log.write(f"Preparing:\n{lst}")
        event_log.write("\n")

        for command in self.selections:
            self.count += 1
            self.run_process(cmd=command)

    def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
        if self.count == 0:
            button = self.query_one('#close', Button)
            button.disabled = False
        self.log(event)

    @work(exclusive=False)
    async def run_process(self, cmd: str) -> None:
        event_log = self.query_one('#event_log', Log)
        event_log.write_line(f"Running: {cmd}")
        # Combine STDOUT and STDERR output
        proc = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT
        )
        stdout, _ = await proc.communicate()
        if proc.returncode != 0:
            raise ValueError(f"'{cmd}' finished with errors ({proc.returncode})")
        stdout = stdout.decode(encoding='utf-8', errors='replace')
        if stdout:
            event_log.write(f'\nOutput of "{cmd}":\n')
            event_log.write(stdout)
        self.count -= 1

    @on(Button.Pressed, "#close")
    def on_button_pressed(self, _) -> None:
        self.app.pop_screen()

你会注意到:

  1. LogScreen 类扩展自 ModalScreen 类, 该类负责处理模态模式的屏幕。
  2. 这个屏幕同样有一个 compose 方法,我们在这里添加了组件以展示 Unix 命令的内容。
  3. 我们创建了一个叫做 mount 的新方法。一旦你用 compose 编排好组件,你就可以运行代码来获取数据,并再进一步定制它们的外观。
  4. 我们使用 asyncio 运行命令,这样我们就能让 TUI 主工作线程在每个命令的结果出来时就及时更新内容。
  5. 对于“工作线程”,请注意 run_process 方法上的 @work(exclusive=False) 注解,该方法用于运行命令并捕获 STDOUT + STDERR 输出。使用 工作线程 来管理并发并不复杂,尽管它们在手册中确实有专门的章节。这主要是因为运行的外部命令可能会执行很长时间。
  6. run_process 中,我们通过调用 write 以命令的输出内容来更新 event_log
  7. 最后,on_button_pressed 把我们带回到前一屏幕(从堆栈中移除屏幕)。

这个小应用向你展示了如何一份不到 200 行的代码来编写一个简单的前端,用来运行非 Python 代码。

现在我们来看一个更复杂的例子,这个例子用到了我们还未探索过的 Textual 的新特性。

示例二:展示赛事成绩的表格

通过 Textual 创建的表格应用

本示例将展示如何使用 DataTable 组件在表格中展示赛事成绩。你能通过这个应用实现:

  • 通过列来排序表格
  • 选择表格中的行,完整窗口展示赛事细节,我们将使用我们在日志浏览器中看到的 “推送屏幕” 技巧。
  • 能进行表格搜索,查看选手详情,或执行其他操作如退出应用。

下面,我们来看看应用代码:

#!/usr/bin/env python
"""
Author: Jose Vicente Nunez
"""
from typing import Any, List

from rich.style import Style
from textual import on
from textual.app import ComposeResult, App
from textual.command import Provider
from textual.screen import ModalScreen, Screen
from textual.widgets import DataTable, Footer, Header

MY_DATA = [
    ("level", "name", "gender", "country", "age"),
    ("Green", "Wai", "M", "MYS", 22),
    ("Red", "Ryoji", "M", "JPN", 30),
    ("Purple", "Fabio", "M", "ITA", 99),
    ("Blue", "Manuela", "F", "VEN", 25)
]

class DetailScreen(ModalScreen):
    ENABLE_COMMAND_PALETTE = False
    CSS_PATH = "details_screen.tcss"

    def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            row: List[Any] | None = None,
    ):
        super().__init__(name, ident, classes)
        # Rest of screen code will be show later

class CustomCommand(Provider):

    def __init__(self, screen: Screen[Any], match_style: Style | None = None):
        super().__init__(screen, match_style)
        self.table = None
        # Rest of provider code will be show later

class CompetitorsApp(App):
    BINDINGS = [
        ("q", "quit_app", "Quit"),
    ]
    CSS_PATH = "competitors_app.tcss"
    # Enable the command palette, to add our custom filter commands
    ENABLE_COMMAND_PALETTE = True
    # Add the default commands and the TablePopulateProvider to get a row directly by name
    COMMANDS = App.COMMANDS | {CustomCommand}

    def action_quit_app(self):
        self.exit(0)

    def compose(self) -> ComposeResult:
        yield Header(show_clock=True)

        table = DataTable(id=f'competitors_table')
        table.cursor_type = 'row'
        table.zebra_stripes = True
        table.loading = True
        yield table
        yield Footer()

    def on_mount(self) -> None:
        table = self.get_widget_by_id(f'competitors_table', expect_type=DataTable)
        columns = [x.title() for x in MY_DATA[0]]
        table.add_columns(*columns)
        table.add_rows(MY_DATA[1:])
        table.loading = False
        table.tooltip = "Select a row to get more details"

    @on(DataTable.HeaderSelected)
    def on_header_clicked(self, event: DataTable.HeaderSelected):
        table = event.data_table
        table.sort(event.column_key)

    @on(DataTable.RowSelected)
    def on_row_clicked(self, event: DataTable.RowSelected) -> None:
        table = event.data_table
        row = table.get_row(event.row_key)
        runner_detail = DetailScreen(row=row)
        self.show_detail(runner_detail)

    def show_detail(self, detailScreen: DetailScreen):
        self.push_screen(detailScreen)

def main():
    app = CompetitorsApp()
    app.title = f"Summary".title()
    app.sub_title = f"{len(MY_DATA)} users"
    app.run()

if __name__ == "__main__":
    main()

有哪些部分值得我们关注呢?

  1. compose 方法中添加了 表头,“命令面板” 就位于此处,我们的表格(DataTable)也在这里。表格数据在 mount 方法中填充。
  2. 我们设定了预期的绑定(BINDINGS),并指定了外部的 CSS 文件来设置样式(CSS_PATH)。
  3. 默认情况下,我们无需任何设置便能使用 命令面板,但在此我们显式启用了它(ENABLE_COMMAND_PALETTE = True)。
  4. 我们的应用有一个自定义表格搜索功能。当用户输入一名选手的名字后,应用会显示可能的匹配项,用户可以点击匹配项查看该选手的详细信息。这需要告诉应用我们有一个定制的命令提供者(COMMANDS = App.COMMANDS | {CustomCo_ mmand}),即类 CustomCommand(Provider)
  5. 如果用户点击了表头,表格内容会按照该列进行排序。这是通过 on_header_clicked 方法实现的,该方法上具有 @on(DataTable.HeaderSelected) 注解。
  6. 类似地,当选中表格中的一行时, on_row_clicked 方法会被调用,这得益于它拥有 @on(DataTable.RowSelected) 注解。当方法接受选中的行后,它会推送一个新的屏幕,显示选中行的详细信息(class DetailScreen(ModalScreen))。

现在,我们详细地探讨一下如何显示选手的详细信息。

利用多屏展示复杂视图

当用户选择表格中的一行,on_row_clicked 方法就会被调用。它收到的是一个 DataTable.RowSelected 类型的事件。从这里我们会用选中的行的内容构建一个 DetailScreen(ModalScreen) 类的实例:

from typing import Any, List
from textual import on
from textual.app import ComposeResult
from textual.screen import ModalScreen
from textual.widgets import Button, MarkdownViewer

MY_DATA = [
    ("level", "name", "gender", "country", "age"),
    ("Green", "Wai", "M", "MYS", 22),
    ("Red", "Ryoji", "M", "JPN", 30),
    ("Purple", "Fabio", "M", "ITA", 99),
    ("Blue", "Manuela", "F", "VEN", 25)
]

class DetailScreen(ModalScreen):
    ENABLE_COMMAND_PALETTE = False
    CSS_PATH = "details_screen.tcss"

    def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            row: List[Any] | None = None,
    ):
        super().__init__(name, ident, classes)
        self.row: List[Any] = row

    def compose(self) -> ComposeResult:
        self.log.info(f"Details: {self.row}")
        columns = MY_DATA[0]
        row_markdown = "\n"
        for i in range(0, len(columns)):
            row_markdown += f"* **{columns[i].title()}:** {self.row[i]}\n"
        yield MarkdownViewer(f"""## User details:
        {row_markdown}
        """)
        button = Button("Close", variant="primary", id="close")
        button.tooltip = "Go back to main screen"
        yield button

    @on(Button.Pressed, "#close")
    def on_button_pressed(self, _) -> None:
        self.app.pop_screen()

这个类的职责很直接:

  1. compose 方法取得此行数据,并利用一个 支持 Markdown 渲染的组件 来展示内容。它的便利之处在于,它会为我们自动生成一个内容目录。
  2. 当用户点击 “close” 后,方法 on_button_pressed 会引导应用回到原始屏幕。注解 @on(Button.Pressed, "#close") 用来接收按键被点击的事件。

最后,我们来详细讲解一下那个多功能的搜索栏(也叫做命令面板)。

命令面板的搜索功能

任何使用了表头的 Textual 应用都默认开启了 命令面板。有意思的是,你可以在 CompetitorsApp 类中添加自定义的命令,这会增加到默认命令集之上:

COMMANDS = App.COMMANDS | {CustomCommand}

然后是执行大部分任务的 CustomCommand(Provider) 类:

from functools import partial
from typing import Any, List
from rich.style import Style
from textual.command import Provider, Hit
from textual.screen import ModalScreen, Screen
from textual.widgets import DataTable
from textual.app import App

class CustomCommand(Provider):

    def __init__(self, screen: Screen[Any], match_style: Style | None = None):
        super().__init__(screen, match_style)
        self.table = None

    async def startup(self) -> None:
        my_app = self.app
        my_app.log.info(f"Loaded provider: CustomCommand")
        self.table = my_app.query(DataTable).first()

    async def search(self, query: str) -> Hit:
        matcher = self.matcher(query)

        my_app = self.screen.app
        assert isinstance(my_app, CompetitorsApp)

        my_app.log.info(f"Got query: {query}")
        for row_key in self.table.rows:
            row = self.table.get_row(row_key)
            my_app.log.info(f"Searching {row}")
            searchable = row[1]
            score = matcher.match(searchable)
            if score > 0:
                runner_detail = DetailScreen(row=row)
                yield Hit(
                    score,
                    matcher.highlight(f"{searchable}"),
                    partial(my_app.show_detail, runner_detail),
                    help=f"Show details about {searchable}"
                )

class DetailScreen(ModalScreen):
        def __init__(
            self,
            name: str | None = None,
            ident: str | None = None,
            classes: str | None = None,
            row: List[Any] | None = None,
    ):
        super().__init__(name, ident, classes)
        # Code of this class explained on the previous section

class CompetitorsApp(App):
    # Add the default commands and the TablePopulateProvider to get a row directly by name
    COMMANDS = App.COMMANDS | {CustomCommand}
    # Most of the code shown before, only displaying relevant code
    def show_detail(self, detailScreen: DetailScreen):
        self.push_screen(detailScreen)
  1. 所有继承自 Provider 的类需实现 search 方法。在我们的例子中,我们还覆盖了 startup 方法,为了获取到我们应用表格(和其内容)的引用,这里使用到了 App.query(DataTable).first()。在类的生命周期中, startup 方法只会被调用一次。
  2. search 方法内,我们使用 Provider.matcher 对每个表格行的第二列(即名字)进行模糊搜索,以与用户在 TUI 中输入的词条进行比较。matcher.match(searchable) 返回一个整型的评分,大于零说明匹配成功。
  3. search 方法中,如果评分大于零,则返回一个 Hit 对象,以告知命令面板搜索查询是否成功。
  4. 每个 Hit 都有以下信息:评分(用于在命令面板中对匹配项排序)、高亮显示的搜索词、一个可调用对象的引用(在我们的案例中,它是一个可以将表格行推送到新屏幕的函数)。
  5. Provider 类的所有方法都是异步的。这使你能释放主线程,只有当响应准备好后才返回结果,这个过程不会冻结用户界面。

理解了这些信息,我们就可以现在展示赛手的详细信息了。

尽管这个架构的追踪功能相对直观,但是组件间传递的消息复杂性不可忽视。幸运的是,Textual 提供了有效的调试工具帮助我们理解背后的工作原理。

Textual 应用的问题排查

对于 Python 的 Textual 应用进行 调试 相较而言更具挑战性。这是因为其中有一些操作可能是异步的,而在解决组件问题时设置断点可能颇为复杂。

根据具体情况,你可以使用一些工具。但首先,确保你已经安装了 textual 的开发工具:

pip install textual-dev==1.3.0

确保你能捕捉到正确的按键

不确定 Textual 应用是否能捕捉到你的按键操作?运行 keys 应用:

textual keys

这让你能够验证一下你的按键组合,并确认在 Textual 中产生了哪些事件。

图片比千言万语更直观

如果说你在布局设计上遇到了问题,想向他人展示你当前的困境,Textual 为你的运行应用提供了截图功能:

textual run --screenshot 5 ./kodegeek_textualize/log_scroller.py

就像你所看到的,我是通过这种方式为这篇教程创建了插图。

捕获事件并输出定制消息

在 Textual 中,每一个应用实例都有一个日志记录器,可以使用如下方式访问:

my_app = self.screen.app
my_app.log.info(f"Loaded provider: CustomCommand")

想要查看这些消息,首先需要开启一个控制台:

. ~/virtualenv/Textualize/bin/activate
textual console

然后在另一个终端运行你的应用程序:

. ~/virtualenv/Textualize/bin/activate
textual run --dev ./kodegeek_textualize/log_scroller.py

在运行控制台的终端中,你可以看到实时的事件和消息输出:

▌Textual Development Console v0.46.0
▌Run a Textual app with textual run --dev my_app.py to connect.
▌Press Ctrl+C to quit.
─────────────────────────────────────────────────────────────────────────────── Client '127.0.0.1' connected ────────────────────────────────────────────────────────────────────────────────
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2188
Connected to devtools ( ws://127.0.0.1:8081 )
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2192
---
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2194
driver=<class 'textual.drivers.linux_driver.LinuxDriver'>
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2195
loop=<_UnixSelectorEventLoop running=True closed=False debug=False>
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2196
features=frozenset({'debug', 'devtools'})
[20:29:43] SYSTEM                                                                                                                                                                 app.py:2228
STARTED FileMonitor({PosixPath('/home/josevnz/TextualizeTutorial/docs/Textualize/kodegeek_textualize/os_app.tcss')})
[20:29:43] EVENT

此外,以开发者模式运行的另一大好处是,如果你更改了 CSS,应用会尝试重新渲染,而无需重启程序。

如何编写单元测试

为你全新开发的 Textual 应用编写 单元测试,应该如何操作呢?

官方文档 展示了几种用于测试我们应用的方式。

我将采用 unittest 进行测试。为了处理异步例程,我们会需要特别的类 unittest.IsolatedAsyncioTestCase

import unittest
from textual.widgets import Log, Button
from kodegeek_textualize.log_scroller import OsApp

class LogScrollerTestCase(unittest.IsolatedAsyncioTestCase):
    async def test_log_scroller(self):
        app = OsApp()
        self.assertIsNotNone(app)
        async with app.run_test() as pilot:
            # Execute the default commands
            await pilot.click(Button)
            await pilot.pause()
            event_log = app.screen.query(Log).first()  # We pushed the screen, query nodes from there
            self.assertTrue(event_log.lines)
            await pilot.click("#close")  # Close the new screen, pop the original one
            await pilot.press("q")  # Quit the app by pressing q


if __name__ == '__main__':
    unittest.main()

现在让我们详细看看 test_log_scroller 方法中的操作步骤:

  1. 通过 app.run_test() 获取一个 Pilot 实例。然后点击主按钮,运行包含默认指令的查询,随后等待所有事件的处理。
  2. 从我们新推送出的屏幕中获取 Log,确保我们已获得几行返回的内容,即它并非空的。
  3. 关闭新屏幕并重新呈现旧屏幕。
  4. 最后,按下 q,退出应用。

可以测试表格吗?

import unittest
from textual.widgets import DataTable, MarkdownViewer
from kodegeek_textualize.table_with_detail_screen import CompetitorsApp


class TableWithDetailTestCase(unittest.IsolatedAsyncioTestCase):
    async def test_app(self):
        app = CompetitorsApp()
        self.assertIsNotNone(app)
        async with app.run_test() as pilot:

            """
            Test the command palette
            """
            await pilot.press("ctrl+\\")
            for char in "manuela".split():
                await pilot.press(char)
            await pilot.press("enter")
            markdown_viewer = app.screen.query(MarkdownViewer).first()
            self.assertTrue(markdown_viewer.document)
            await pilot.click("#close")  # Close the new screen, pop the original one

            """
            Test the table
            """
            table = app.screen.query(DataTable).first()
            coordinate = table.cursor_coordinate
            self.assertTrue(table.is_valid_coordinate(coordinate))
            await pilot.press("enter")
            await pilot.pause()
            markdown_viewer = app.screen.query(MarkdownViewer).first()
            self.assertTrue(markdown_viewer)
            # Quit the app by pressing q
            await pilot.press("q")


if __name__ == '__main__':
    unittest.main()

如果你运行所有的测试,你将看到如下类似的输出:

(Textualize) [josevnz@dmaf5 Textualize]$ python -m unittest tests/*.py
..
----------------------------------------------------------------------
Ran 2 tests in 2.065s

OK

这是测试 TUI 的一个不错的方式,对吧?

打包 Textual 应用

打包 Textual 应用与打包常规 Python 应用并没有太大区别。你需要记住,需要包含那些控制应用外观的 CSS 文件:

. ~/virtualenv/Textualize/bin/activate
python -m build
pip install dist/KodegeekTextualize-*-py3-none-any.whl

这个教程的 pyproject.toml 文件是一个打包应用的良好起点,告诉你需要做什么。

[build-system]
requires = [
    "setuptools >= 67.8.0",
    "wheel>=0.42.0",
    "build>=1.0.3",
    "twine>=4.0.2",
    "textual-dev>=1.2.1"
]
build-backend = "setuptools.build_meta"

[project]
name = "KodegeekTextualize"
version = "0.0.3"
authors = [
    {name = "Jose Vicente Nunez", email = "[email protected]"},
]
description = "Collection of scripts that show how to use several features of textualize"
readme = "README.md"
requires-python = ">=3.9"
keywords = ["running", "race"]
classifiers = [
    "Environment :: Console",
    "Development Status :: 4 - Beta",
    "Programming Language :: Python :: 3",
    "Intended Audience :: End Users/Desktop",
    "Topic :: Utilities"
]
dynamic = ["dependencies"]

[project.scripts]
log_scroller = "kodegeek_textualize.log_scroller:main"
table_detail = "kodegeek_textualize.table_with_detail_screen:main"

[tool.setuptools]
include-package-data = true

[tool.setuptools.packages.find]
where = ["."]
exclude = ["test*"]

[tool.setuptools.package-data]
kodegeek_textualize = ["*.txt", "*.tcss", "*.csv"]
img = ["*.svg"]

[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}

未来计划

这个简短的教程只覆盖了 Textual 的部分方面。还有很多需要探索和学习的内容:

  • 强烈建议你查看 官方教程。有大量的示例和指向参考 API 的链接。
  • Textual 可以使用来自 Rich 项目的组件,这个项目是一切的起源。我认为其中一些甚至可能所有这些组件在某些时候都会合并到 Textual 中。Textual 框架对于使用高级 API 的复杂应用更能胜任,但 Rich 也有很多漂亮的功能。
  • 创建你自己的组件!同样,在设计 TUI 时,拿一张纸,画出你希望这些组件如何布局的,这会为你后期省去很多时间和麻烦。
  • 调试 Python 应用可能会有点复杂。有时你可能需要 混合使用不同的工具 来找出应用的问题所在。
  • 异步 IO 是一个复杂的话题,你应该 阅读开发者文档 来了解更多可能的选择。
  • Textual 被其他项目所使用。其中一个非常易于使用的项目是 Trogon它会让你的 CLI 可以自我发现
  • Textual-web 是个很有前景的项目,能让你在浏览器上运行 Textual 应用。尽管它不如 Textual 成熟,但它的进化速度非常快。
  • 最后,查看这些外部项目。在项目组合中有许多有用的开源应用。

(题图:DA/f11b0eb0-8e16-4cbe-986a-0fe978f6732a)


via: https://fedoramagazine.org/crash-course-on-using-textual/

作者:Jose Nunez 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

下面是一些极好的建议来让你的 Linux 系统游戏体验更上一层楼。

多亏了各种新工具的诞生和 Linux 发行版在用户体验上的改进,目前已有成千上万的游戏可以在 Linux 系统中运行。

无论你是使用主流的 Linux 发行版还是 专为游戏设计的 Linux 发行版,你都能在 Linux 中享受游戏的乐趣。

然而,为了得到流畅的游戏体验,有一些你需要遵循的工具,技巧和方法。在这篇文章中,我会为你详细介绍一些。

1、选择合适的电子游戏

最首要的一步就是找到一款你热爱,且在 Linux 上运行流畅的游戏。

假设你购买或下载了一款最初为 Windows 系统制作的游戏,可能不论你使用何种工具,都无法在 Linux 系统中成功运行。而你最终只会挫败地想 “Linux 不适合游戏” ?

那么,该如何避免这类问题呢?

首先,你需要查阅你想要购买或下载的游戏可以在哪些平台上运行。如果游戏支持 Linux 或 Steam OS 系统,那就没问题了。

你还可以查看该游戏是否已经被添加到 Steam Deck 验证列表 中。

无论哪种情况,你都可以逛一逛 ProtonDB,在那儿搜索你所关注的游戏,看看其他用户对其的评价和打分。如果大部分评论听起来都令人信服,或者说这款游戏很值得尝试,那你就可以放心购买或下载了。

2、不要选择知名度较低的发行版

如果你希望得到社区或者使用 Linux 的朋友们的支持与帮助,请使用那些并非某某人业余项目的 Linux 发行版。

我建议你从 最佳 Linux 发行版 中作出选择。

最好的是,选择一款 长期支持版 以确保稳定的使用体验。

3、别为 Linux 游戏搭建顶级配置 PC

我知道听起来有点令人失望。但为了确保最大的兼容性以及无忧的游戏体验,最理想的状况是选用前一代的硬件而不是最新的。

利用这种方式,你还可以享受到大额的折扣,省下大笔钱!别忘了,在决定购买前,还可参阅其他 PC 硬件狂热爱好者的评论。

这个建议尤其适用于新硬件,那些刚刚发布没几个月的。

当然,你可以冒险尝试最新的硬件组件。但游戏可能会崩溃,或以其他方式出问题。而且,你也可能无法获取到关于这些硬件组件的可靠评论,从而做出明智的选择。

4、开启 Steam Play

如果你一直都是 Linux 原生游戏的粉丝,你可能想要 安装 Steam 游戏商店 并开启 Proton 兼容层,在 Linux 上运行那些仅供 Windows 系统的游戏。

你可以参考我前面提到的 ProtonDB 或者 Steam Deck 的验证列表,来确认该游戏是否能在 Linux 上流畅运行。

在安装了 Steam 以后,如果你想 开启 Steam Play,可以参照我们的指南操作。

5、没有 Steam?不必担忧!

尽管 Steam 提供了跨平台的卓越顺畅的游戏体验。

但如果你不想仅限于 Steam,还想要能访问像 Epic 游戏商店这样的商店中的游戏,你可以考虑安装 Lutris 这样的工具。

我们为你准备了一份详尽的指南,来帮助你 在 Linux 下使用 Epic 游戏商店,并学习使用此工具(和其他替代工具)。

不用担心,如果你偏爱 GOG 的无数字版权管理(DRM)游戏,我们同样为你准备了指南,指导你使用 Lutris 这样的工具在 Linux 下玩 GOG 游戏

6、使用 MangoHud 跟踪游戏性能

你是否希望在游戏中同时监控你的硬件性能和游戏性能?

借助 MangoHud,你可以得到一个信息层,显示 FPS、CPU/GPU 的温度、显存使用情况等信息,这与在 Windows 下利用英伟达 GeForce Experience 或者 MSI Afterburner 的效果相似。

安装十分简单,对于像 Fedora、Debian 和 Arch 这样的发行版,提供了 Flatpak 和其他形式的二进制文件。你可以在它的 GitHub 页面 上了解更多关于它的使用方法。

7、已经买好游戏硬件了吗?马上进行配置!

可惜的是,并不是所有的发烧友级游戏硬件都能在 Linux 下进行详细调整,至少,不能和在 Windows 下一样。

例如,你可以使用图形界面工具 piper 进行游戏鼠标的配置

同理,如果你想要控制或者 调整 Razer 设备上的灯光,你可以在这里找到我们的快速教程进行学习。

不只是设备,需要监控你的 PC 中的一体式水冷(AIO)或者其他散热硬件吗?你可以试试 CoolerControl(以前称为 Coolero)。

监控组件的温度对于获得流畅的游戏体验至关重要。因此,你可以选择工具如 CoolerControl 和 MangoHud,或者手动监控你的系统资源。

结束语

除了上述的所有建议,你还应该微调游戏内的设置以在你的系统上获得沉浸式体验。当然,这依赖于个人用户的偏好,所以没有通用的解决方案。

别忘了,如果你是新手,那么不妨读一读我们的 Linux 游戏指南

? 你最喜欢用什么工具来提升你的 Linux 游戏体验?你有想要添加到这个列表的建议吗?请随时在评论中让我们知道你的想法。

(题图:DA/68d71173-992a-423a-bd95-a6e2f64bf254)


via: https://itsfoss.com/linux-gaming-tips/

作者:Ankush Das 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你无需在 Wayland 上放弃使用 Shutter!

哎,我一直在担心有一天我得切换到只支持 Wayland 的 Linux 发行版,结果发现 Shutter 不工作。

它是 Linux 中用于捕捉和编辑 屏幕截图的最佳工具 之一,我每天都会使用它来为这里和其他地方的工作捕捉各种截图。

我知道 GNOME 的屏幕截图工具已经有了很大的进步,但是就像人们所说的,“旧习难改”。所以,当我发现有一种方法可以在所有的 Wayland 上运行 Shutter 时,我自然是相当兴奋。

请跟随我一起了解这个精彩的开源项目。这个项目由一位热心的 Shutter 爱好者发起,他的目标就是让 Shutter 能在配备 Wayland 的发行版上顺利运行。

在 Wayland 上使用 Shutter:有何期待?

这个项目由来自意大利的 IT 开发者 Maurizio 发起,他热衷于 Linux,因为他不能接受 Shutter 无法在他的 Ubuntu 系统上正常运行,因此这个项目应运而生。

因此,他复刻了 Shutter 的代码仓库,并利用 GNOME 的屏幕截图工具 的命令行工具进行开发,同时尽力保持用户界面和以往的操作习惯不变

如下图所示,这个项目与你从 Shutter 那里期待的体验几乎无二,你可以在界面上发现所有熟悉的选项。在运行着 Ubuntu 23.10 的虚拟机上用这个工具截图时,我并未感到有什么两样。

我还发现,按只截图窗口或选取特定区域也变得容易了。只是那个通常用来截图特定窗口或桌面的下拉菜单似乎并未起到应有的功能,反而变得有点挪作他用。

? 这是我在 Ubuntu 23.10 的 Wayland 上进行选区捕捉的结果。

如果你想截图你系统中特定的桌面空间,你需要切换到那个桌面,使 Shutter 转到同一屏幕,然后使用“ 桌面 Desktop ”选项进行截图。

对于只截图窗口也是类似,只要将应用和 Shutter 切到同一屏幕,使用“ 窗口 Window ”选项就能够截取窗口的截图。

至此已经足够了。我真心期待 Shutter 的开发更上层楼,希望我们能看到此类改进被融入原有的项目中。

? 如何下载运行于 Wayland 的 Shutter?

在开始下载和安装之前,请确保你已经彻底卸载了你之前安装的任何版本的 Shutter。

接下来,你有两种获取这个 Shutter 变体的方法可选。第一种方法是直接访问它的 GitHub 仓库,下载提供的 “.deb” 文件。

在 Wayland 上的 Shutter(GitHub)

而我更为推荐的是第二种方法,尤其适用于 Ubuntu 23.10 或者更高的版本,因为它可以自动处理所有的依赖关系。你只需要运行以下的命令即可:

setfacl -m u:_apt:rx .
wget https://github.com/mvivarelli/shutter-on-wayland/raw/master/shutter-on-wayland_0.99.4-6_all.deb
sudo apt -f install ./shutter-on-wayland_0.99.4-6_all.deb

如果安装后你在启动 Shutter 时遇到了卡顿或者延迟的问题,那么你可以通过重启你的系统来解决此类问题。

? 对于这个项目,你有什么样的想法呢?你会考虑使用它吗?


via: https://news.itsfoss.com/shutter-wayland-linux/

作者:Sourav Rudra 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

大家好!某天,我突发奇想 —— 是否能把 Git 存储库制作成一个 FUSE 文件系统,然后把所有的提交记录做成文件夹呢?答案是肯定的!有 giblefsGitMounter 和用于 Plan 9 号的 git9

但在 Mac 上使用 FUSE 实在很烦人 —— 你需要安装一个内核扩展,但由于安全的原因,Mac OS 上安装内核扩展看起来越来越难了。此外,我还有一些想法,希望能用与这些项目不同的方式来组织文件系统。

因此,我想在 Mac OS 上尝试 FUSE 以外的挂载文件系统的方法会很有趣,因此我创建了一个名为 git-commit-folders 的项目来做这个事。它可以同时使用 FUSE 和 NFS(至少在我的电脑上),WebDav 的实现起来还有点问题。

这个项目很有实验性(我不确定这究竟是一个有用的软件,还是一个思考 Git 如何工作的有趣玩具),但写起来很有趣,我自己也很喜欢在小型存储库中使用它,下面是我在写这个项目时遇到的一些问题。

目标:像文件夹一样显示提交记录

我做这个事的主要目的是给大家一些启发:Git 核心是如何运行的。总结来说,Git 提交记录实际上和文件夹非常类似 —— 每个 Git 提交都包含一个目录,其中 列出了文件,这个目录也可以有子目录,依此类推。

只是为了节省磁盘空间,Git 提交实际上并不是以文件夹的形式实现的。

而在 git-commit-folders,所有的提交记录实际上看起来就是一个文件夹,如果你想浏览历史提交记录,你可以像浏览文件系统一样浏览它们!例如如果你像查看我的博客的初始提交记录,你可以如下操作:

$ ls commits/8d/8dc0/8dc0cb0b4b0de3c6f40674198cb2bd44aeee9b86/
README

其他之后的提交记录,如下:

$ ls /tmp/git-homepage/commits/c9/c94e/c94e6f531d02e658d96a3b6255bbf424367765e9/
_config.yml  config.rb  Rakefile  rubypants.rb  source

分支是符号链接

通过 git-commit-folders 挂载的文件系统中,提交是唯一真正的文件夹 —— 其他一切(分支、标签等)都是提交记录的符号链接。这反映了 Git 底层的工作方式。

$ ls -l branches/
lr-xr-xr-x 59 bork bazil-fuse -> ../commits/ff/ff56/ff563b089f9d952cd21ac4d68d8f13c94183dcd8
lr-xr-xr-x 59 bork follow-symlink -> ../commits/7f/7f73/7f73779a8ff79a2a1e21553c6c9cd5d195f33030
lr-xr-xr-x 59 bork go-mod-branch -> ../commits/91/912d/912da3150d9cfa74523b42fae028bbb320b6804f
lr-xr-xr-x 59 bork mac-version -> ../commits/30/3008/30082dcd702b59435f71969cf453828f60753e67
lr-xr-xr-x 59 bork mac-version-debugging -> ../commits/18/18c0/18c0db074ec9b70cb7a28ad9d3f9850082129ce0
lr-xr-xr-x 59 bork main -> ../commits/04/043e/043e90debbeb0fc6b4e28cf8776e874aa5b6e673
$ ls -l tags/
lr-xr-xr-x - bork 31 Dec  1969 test-tag -> ../commits/16/16a3/16a3d776dc163aa8286fb89fde51183ed90c71d0

这个并不能完全呈现 Git 的所有工作机理(相比简单的类似文件夹的提交,还有很多复杂的细节),但是我希望大家对“每个提交如同一个文件夹,里面有你的旧版本代码”有一个直观的认识。

这么做有什么好处呢?

在我深入介绍它的实现之前,我想说下为什么把 Git 提交记录变成拥有文件夹的文件系统很有用。我的很多项目最终都没有真正使用过(比如 dnspeep),但我发现自己在做这个项目的时候确实使用到了一些。

目前为止我发现主要用处是:

  • 查找已经删除的函数 - 可以用 grep someFunction branch_histories/main/*/commit.go 查找它的旧版本
  • 快速查看其他分支的一个文件并从其拷贝一行,如 vim branches/other-branch/go.mod
  • 在每个分支中搜索某个函数,如 grep someFunction branches/*/commit.go

所有这些操作都通过提交记录的符号链接,来替代提交记录的直接引用。

这些都不是最有效的方法(你可以用 git showgit log -S 或者 git grep 来完成类似操作),但是对我个人来说,我经常忘记 Git 语法,而浏览文件系统对我来说更简单。git worktree 还允许你同时签出多个分支,但对我来说,为了看一个文件而设置整个工作树感觉很奇怪。

接下来我想谈谈我遇到的一些问题。

问题 1: 用 WebDav 还是 NFS?

Mac OS 原生支持的两个文件系统是 WebDav 和 NFS。我说不出那个更新容易实现,所以我就索性尝试两个都支持。

起初,WebDav 的实现看起来更容易一些,在 golang.org/x/net 上有一个 WebDav 实现,这个很好配置。

但这个实现不支持符号链接,我想可能原因是它用的是 io/fs 接口,而 io/fs 还不支持 符号链接。不过看起来正在进行中。所以我放弃了 WebDav,而决定重点放在 NFS 实现上了,用 go-nfs NFSv3 的库文件来实现。

有人也提到了 Mac 上的 FileProvider,我还没有深入了解这个。

问题 2: 如何确保所有的实现保持一致?

我已经实现了三个不同的文件系统(FUSE、NFS 和 WebDav),但对我来说还是没搞清楚如何避免大量的重复代码。

我的朋友 Dave 建议写一个核心实现,然后写一个适配器(如 fuse2nfsfuse2dav)来转换成 NFS 和 WebDav 版本。这个看起来需要我着手实现三个文件系统的接口:

  • 对应 FUSE 的 fs.FS
  • 对应 NFS 的 billy.Filesystem
  • 对应 WebDav 的 webdav.Filesystem

因此我把所有的核心逻辑放到 fs.FS 接口上,然后写两个函数:

  • func Fuse2Dav(fs fs.FS) webdav.FileSystem
  • func Fuse2NFS(fs fs.FS) billy.Filesystem

所有的文件系统都比较类似,因此转换起来不是很难,但就是有大量的烦人的问题需要修复。

问题 3: 我不想罗列所有的提交记录怎么办

一些 Git 存储库有成千上万的提交记录。我的第一个想法是如何让 commits/ 看起来是空的,这样就可以如下展示:

$ ls commits/
$ ls commits/80210c25a86f75440110e4bc280e388b2c098fbd/
fuse  fuse2nfs  go.mod  go.sum  main.go  README.md

因此所有的提交记录可以直接查看,但是又不能罗列它们。这个对文件系统是一个奇怪的事情,实际上 FUSE 可以做到。但我在 NFS 上无法实现。我认为这里的原因是,如果你告诉 NFS 某个目录是空的,它就会认为该目录实际上是空的,这是合理的。

我们最终是这样处理的:

  • 按照 .git/objects 的方式,以前两个字符组织管理提交记录(因此 ls commits 会显示 0b 03 05 06 07 09 1b 1e 3e 4a),但这样做会分为两层,这样 18d46e76d7c2eedd8577fae67e3f1d4db25018b0 则为 commits/18/18df/18d46e76d7c2eedd8577fae67e3f1d4db25018b0
  • 开始只罗列一次所有的已经打包的提交哈希,将它们缓存在内存中,然后后面仅更新稀疏对象。主要思路是版本库中几乎所有的提交都应该打包,而且 Git 不会经常重新打包提交

这个看起来在拥有百万提交记录的 Linux 内核的 Git 存储库上似乎效果不错。在我的机器上实测它初始化大概需要一分钟,之后只需快速增量更新即可。

每个提交哈希只有 20 个字节,因此缓存 1 百万个提交哈希也不是很大,大约 20MB。

我认为更聪明的做法是延迟加载提交列表 —— Git 会按提交 ID 对其打包文件进行排序,所以你可以很容易地进行二叉树搜索,找到所有以 1b1b8c 开始的提交。我用的 Git 库 对此并不支持,因为罗列出来 Git 存储库所有的提交记录确实一个奇怪的事情。我花了 几天时间 尝试实现它,但没有达到我想要的性能,所以就放弃了。

问题 4: 不是目录

我常遇到下面这个错误:

"/tmp/mnt2/commits/59/59167d7d09fd7a1d64aa1d5be73bc484f6621894/": Not a directory (os error 20)

这起初真的把我吓了一跳,但事实证明,这只是表示在列出目录时出现了错误,而 NFS 库处理该错误的方式就是显示 “Not a directory”(不是目录)。这个错误遇到了很多次,我需要每次跟踪这个错误的根源。

有很多类似错误。我也遇到 cd: system call interrupted,令人沮丧的是,但最终也只是程序中的其他错误。

我意识到终极大法是用 Wireshark 查看 NFS 发送和接受的数据包,很多问题便可迎刃而解。

问题 5: inode 编号

在开始的时候我不小心将所有的文件夹的 inode 设为 0。这很糟糕,因为如果在每个目录的 inode 都为 0 的目录上运行查找,它就会抱怨文件系统循环并放弃,这个也是符合逻辑的。

我通过定义一个 inode(string) 来修复这个问题,通过散列字符串来获取 inode 编号,并使用树 ID / blob ID 作为散列字符串。

问题 6: 过期文件句柄

我一直遇到这个“Stale NFS file handle”(过期文件句柄)错误。问题是,我需要获取未知的 64 字节 NFS “文件句柄”,并将其映射到正确的目录。

我使用的 NFS 库的工作方式是为每个文件生成一个文件句柄,并通过固定大小的缓存来缓存这些引用。这对小型存储库来说没问题,但是如果对于拥有海量的文件的存储库来说,由于缓存就会溢出,就会导致“stale file handle” 错误。

这仍然是个问题,我不知道如何解决。我不明白真正的 NFS 服务器是如何做到这一点的,也许它们只是有一个非常大的缓存?

NFS 文件句柄占用 64 个字节(不是比特),确实很大,所以很多时候似乎可以将整个文件路径编码到句柄中,根本不需要缓存。也许我会在某个时候尝试实现这一点。

问题 7: 分支历史

branch_histories/ 目录目前仅罗列对应分支的最近 100 个提交记录。我不知道该怎么做,如果能以某种方式列出分支的全部历史就更好了。也许我可以使用 commits/ 目录中类似的子文件夹技巧。

问题 8: 子模块

Git 存储库有时包含了子模块。由于目前我对子模块的理解还不深入,我先忽略它吧。因此这个算是一个问题。

问题 9: NFSv4 是否更好?

我构建这个项目使用的是 NFSv3 库,因为我当时只能找到一个 NFSv3 的 Go 库文件。可当我搞完的时候才发现了一个名叫 buildbarn 的项目里有 NFSv4 服务器。有没有可能用它会更好一些?

我不知道这样做有什么问题,或者用 NFSv4 有哪些优点?我还有点不确定是否要使用 buildbarn NFS 库,因为不清楚他们是否希望其他人使用它。

就这些吧

之前已经解决了很多问题我都忘记了,这是我目前能回想起来的。我未来有可能解决或根本解决不了 NFS 的“过期文件句柄” 错误,或者“在 Linux 内核的存储库上启动需要 1 分钟”的问题,就这样吧。

感谢我的朋友 vasi,他给我了很多文件系统方面的帮助。

(题图:DA/d22b1c01-e80a-4529-b88a-419ceef74b5e)


via: https://jvns.ca/blog/2023/12/04/mounting-git-commits-as-folders-with-nfs/

作者:Julia Evans 选题:lujun9972 译者:guevaraya 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解在 Ubuntu 上安装 Docker 的两种官方方法。一种很简单,但可能会给你一个稍旧的版本。另一种方法稍显复杂,但能提供最新的稳定版本。

使用 Docker 意味着开启一个新的计算领域,但如果你刚刚开始使用 Docker,安装可能看起来是一项艰巨的任务。

在 Ubuntu 上安装 Docker 有两种推荐的方法:

  • 从 Ubuntu 的仓库安装 Docker:简单的单行命令,但提供旧版本
  • 使用 Docker 的官方仓库:工作量稍多,但提供了最新的稳定版本

我将在本教程中讨论它们。

方法 1:使用 Ubuntu 的仓库安装 Docker

如果你不在意旧版本,并且不想设置和管理仓库,那么这是最适合你的方法。

从更新仓库索引开始:

sudo apt update

现在,你可以使用以下命令在 Ubuntu 中安装 Docker 以及 Docker Compose

sudo apt install docker.io docker-compose
? Docker 包被命名为 docker.io,因为在 Docker 出现之前就已经存在一个名为 docker(用于 Dockerlet 应用)的过渡包。因此,Docker 包必须被命名为其他名称。

安装完成后,你可以使用以下命令检查安装的版本:

docker -v

如你所见,它给了我 Docker 24.0.5。

方法 2:在 Ubuntu 中安装最新稳定版本的 Docker

如果你想要 Docker 的最新稳定版本,那么你可以从其官方仓库安装 Docker。

步骤 1:删除任何现有的 Docker 包

但在跳到安装部分之前,有必要删除所有以前安装的 Docker。

卸载以前的 Docker,请使用以下命令。

sudo apt remove docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc

步骤 2:安装依赖项

第一步是安装一些必要的软件包,这些软件包将用于在本教程后面安装 Docker:

sudo apt install ca-certificates curl gnupg lsb-release

你可能已经安装了部分或全部这些软件包,但确认安装没有什么坏处。上面的命令不会伤害你。

步骤 3:添加 Docker 仓库的 GPG 密钥并将其添加到 sources.list

现在,通过 apt 包管理器创建一个具有特殊权限的目录,用于存储加密密钥以进行包验证:

sudo install -m 0755 -d /etc/apt/keyrings

接下来,如下所示 使用 curl 命令 下载并导入 Docker 的 GPG 密钥环:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

下载 GPG 密钥后,使用 chmod 命令更改文件权限,以便系统上的每个用户都可以读取 GPG 密钥:

sudo chmod a+r /etc/apt/keyrings/docker.gpg

最后,将 Docker 仓库添加到 sources.list.d 文件中:

echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

就是这样!

步骤 4:安装 Docker 和 Docker Compose

现在,要使你对系统所做的更改生效,请更新系统仓库:

sudo apt update

使用以下命令获取最新版本的 Docker 以及其他 Docker 组件和依赖项:

sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
? 虽然 docker.io 包安装了大部分必需的 Docker 组件,但你需要在此处单独进行安装。

要检查已安装的版本,请使用以下命令:

docker -v

测试安装的另一个方法是在 Docker 中使用 “Hello World” 镜像。

让我告诉你如何做。

使用 hello-world 镜像来验证 Docker 安装

运行 “Hello World” 程序是我们启动任何编程之旅时都遵循的标准做法,Docker 也是如此。

Docker 为你提供了一个 “Hello World” 镜像,你可以使用它来测试安装。

要安装并运行 “Hello World” 镜像,只需使用以下命令:

sudo docker run hello-world

某些用户在执行上述命令时可能会收到错误消息“无法连接到 Docker 守护进程”:

在这种情况下,重新启动系统并再次尝试安装 Docker Hello World 镜像,它将正常工作。

? 额外提示:在 Ubuntu 中使用 Docker 而无需 sudo

如果你注意到了,我使用 sudo 来拉取 Docker 镜像,这并不是使用 Docker 最方便的方式。

如果你尝试在不使用 sudo 的情况下使用 Docker,则会出现错误:

好吧,在本节中,我将向你展示如何在没有 sudo 的情况下使用 Docker。

? 要执行所示步骤,必须具有超级用户权限!

第一步是使用以下命令 创建一个名为 docker 的新组

sudo groupadd docker

完成后,使用以下命令将用户添加到 docker 组:

sudo usermod -aG docker <用户名>
? 确保你只添加具有超级用户权限的用户。

现在,注销并重新登录以使更改生效。但如果你使用的是虚拟机,那么必须重新启动。

就是这样!从现在开始,你可以使用 docker 命令而不需要 sudo,就像我运行 Docker Hello World 镜像一样:

就这样吧。

这是安装 Docker 后要做的事情

如果你刚刚开始,请参阅每个用户都必须了解的 基本 Docker 命令列表

21 个基本 Docker 命令

我希望你觉得会有帮助。

(题图:DA/759e483b-0be8-409e-bc58-ed19d99cbdd4)


via: https://itsfoss.com/install-docker-ubuntu/

作者:Sagar Sharma 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在本系列的第五篇文章中,我们将继续介绍概率和统计中的概念。

在本系列的 前一篇文章 中,我们首先介绍了使用 TensorFlow。它是一个非常强大的开发人工智能和机器学习应用程序的库。然后我们讨论了概率论的相关知识,为我们后面的讨论打下基础。在本系列的第五篇文章中,我们将继续介绍概率和统计中的概念。

在本文中我将首先介绍 Anaconda,一个用于科学计算的 Python 发行版。它对于开发人工智能、机器学习和数据科学的程序特别有用。稍后我们将介绍一个名为 Theano 的 Python 库。但在此之前,让我们下讨论一下人工智能的未来。

在回顾和修订之前的文章时,我发觉我偶尔对人工智能前景的怀疑语气和在一些话题上毫不留情的诚实态度可能在无意中使部分读者产生了消极情绪。

这促使我开始从金融角度研究人工智能和机器学习。我想确定涉足人工智能市场的公司类型,是否有重量级的公司大力参与其中?还是只有一些初创公司在努力推动?这些公司未来会向人工智能市场投入多少资金?是几百万美元,几十亿美元还是几万亿美元?

我通过于最近知名报纸上的的预测和数据来理解基于人工智能的经济发展背后的复杂动态性。2020 年《福布斯》上的一篇文章就预测 2020 年企业在人工智能上投入的投入将达到 500 亿美元的规模。这是一笔巨大的投资。《财富》杂志上发表的一篇文章称,风险投资者正将部分关注力从人工智能转移到 Web3 和 去中心化金融 decentralised finance (DeFi)等更新潮的领域上。但《华尔街日报》在 2022 年自信地预测,“大型科技公司正在花费数十亿美元进行人工智能研究。投资者应该密切关注。”

印度《商业标准报》在 2022 年报道称,87% 的印度公司将在未来 3 年将人工智能支出提高 10%。总的来说,人工智能的未来看起来是非常安全和光明的。 令人惊讶的是,除了亚马逊、Meta(Facebook 的母公司)、Alphabet(谷歌的母公司)、微软、IBM 等顶级科技巨头在投资人工智能外,壳牌、强生、联合利华、沃尔玛等非 IT 科技类公司也在大举投资人工智能。

很明显众多世界级大公司都认为人工智能将在不久的将来发挥重要作用。但是未来的变化和新趋势是什么呢?我通过新闻文章和采访找到一些答案。在人工智能未来趋势的背景下,经常提到的术语包括 负责任的人工智能 Responsible AI 、量子人工智能、人工智能物联网、人工智能和伦理、自动机器学习等。我相信这些都是需要深入探讨的话题,在上一篇文章中我们已经讨论过人工智能和伦理,在后续的文章中我们将详细讨论一些其它的话题。

Anaconda 入门

现在让我们讨论人工智能的必要技术。Anaconda 是用于科学计算的 Python 和 R 语言的发行版。它极大地简化了包管理过程。从本文开始,我们将在有需要时使用 Anaconda。第一步,让我们安装 Anaconda。访问 安装程序下载页面 下载最新版本的 Anaconda 发行版安装程序。在撰写本文时(2022 年 10 月),64 位处理器上最新的 Anaconda 安装程序是 Anaconda3-2022.05-Linux-x86_64.sh。如果你下载了不同版本的安装程序,将后面命令中的文件名换成你实际下载的安装文件名就行。下载完成后需要检查安装程序的完整性。在安装程序目录中打开一个终端,运行以下命令:

shasum -a 256 Anaconda3-2022.05-Linux-x86_64.sh

终端上会输出哈希值和文件名。我的输出显示是:

a7c0afe862f6ea19a596801fc138bde0463abcbce1b753e8d5c474b506a2db2d Anaconda3-2022.05-Linux-x86_64.sh

然后访问 Anaconda 安装程序哈希值页面,比对下载安装文件的哈希值。如果哈希值匹配,说明下载文件完整无误,否则请重新下载。然后在终端上执行以下命令开始安装:

bash Anaconda3-2022.05-Linux-x86_64.sh

按回车键后,向下滚动查看并接受用户协议。最后,输入 yes 开始安装。出现用户交互提示时,一般直接使用 Anaconda 的默认选项就行。现在 Anaconda 就安装完成了。

默认情况下,Anaconda 会安装 Conda。这是一个包管理器和环境管理系统。Anaconda 发行版会自动安装超过 250 个软件包,并可选择安装超过 7500 个额外的开源软件包。而且使用 Anaconda 安装的任何包或库都可以在 Jupyter Notebook 中使用。在安装新包的过程中, Anaconda 会自动处理它的依赖项的更新。

至此之后我们终于不用再担心安装软件包和库的问题了,可以继续我们的人工智能和机器学习程序的开发。注意,Anaconda 只有一个命令行界面。好在我们的安装项中包括 Anaconda Navigator。这是一个用于 Anaconda 的图形用户界面。在终端上执行命令 anaconda-navigator 运行 Anaconda Navigator(图 1)。我们马上会通过例子看到它的强大功能。

图 1:Anaconda Navigator

Theano 介绍

Theano 是一个用于数学表达式计算的优化编译的 Python 库。在 Anaconda Navigator 中安装Theano 非常容易。打开 Anaconda Navigator 后点击 “ 环境 Environments ” 按钮(图 1 中用红框标记)。在打开的窗口中会显示当前安装的所有软件包的列表。在顶部的下拉列表中选择“ 尚未安装 Not installed ”选项。向下滚动并找到 Theano,然后勾选左侧的复选框。点击窗口右下角的绿色 “ 应用 Apply ” 按钮。Anaconda 会在弹出菜单中显示安装 Theano 的所有依赖项。图 2 是我安装 Theano 时的弹出菜单。可以看到,除了 Theano 之外,还安装了一个新的包,并修改了 8 个包。

想象一下,如果要手动安装 Theano,这将是多么麻烦。有了 Anaconda,我们只需要点几个按钮就行了。只需要等待一会儿,Theano 就安装好了。现在我们可以在 Jupyter Notebook 中使用 Theano 了。

图 2:安装 Theano

我们已经熟悉了用于符号计算的 Python 库 SymPy,但 Theano 将符号计算提升到了一个新的水平。图 3 是一个使用 Theano 的例子。第 1 行代码导入 Theano。第 2 行导入 theano.tensor 并将其命名为 T。我们在介绍 TensorFlow 时已经介绍过 张量 tensor 了。

图 3:使用 Theano 的代码例子

在数学上,可以将张量看作多维数组。张量是 Theano 的关键数据结构之一,它可用于存储和操作标量(数字)、向量(一维数组)、矩阵(二维数组)、张量(多维数组)等。在第 3 行中,从 Theano 导入了 function() 的函数。第 4 行导入名为 pp() 的 Theano 函数,该函数用于格式化打印。第 5 行创建了一个名为 xdouble 类型的标量符号变量。你可能会在理解符号变量这个概念上遇到一些困难。这里你可以把它看作是没有绑定具体值的 double 类型的对象。类似地,第 6 行创建了另一个名为 y 的标量符号变量。第 7 行告诉 Python 解释器,当符号变量 xy 得到值时,将这些值相加并存储在 a 里面。

为了进一步解释符号操作,仔细看第 8 行的输出是 (x+y)。这表明两个数字的实际相加还没有发生。第 9 到 11 行类似地分别定义了符号减法、乘法和除法。你可以自己使用函数 pp() 来查找 bcd 的值。第 12 行非常关键。它使用 Theano 的 function() 函数定义了一个名为 f() 的新函数。 函数 f() 的输入是 xy,输出是 [a b c d]。最后在第 13 行中,给函数 f() 提供了实际值来调用该函数。该操作的输出也显示在图 3 中。我们很容易验证所显示的输出是正确的。

图 4:用 Theano 处理矩阵

下面让我们通过图 4 的代码来看看如何使用 Theano 创建和操作矩阵。需要注意的是,图中我省略了导入代码。如果你要直接运行图 4 的代码,需要自己添加上这几行导入代码(图 3 中的前三行)。第 1 行创建了两个符号矩阵 xy。这里我使用了 复数构造函数 plural constructor imatrices,它可以同时构造多个矩阵。第 2 行到第 4 行分别对符号矩阵 xy 执行符号加法、减法和乘法。这里你可以使用 print(pp(a))print(pp(b))print(pp(c)) 来帮助理解符号操作的性质。第 5 行创建了一个函数 f(),它的输入是两个符号矩阵 xy,输出是 [a b c],它们分别表示符号加法、减法和乘法。最后,在第 6 行中,为函数 f() 提供实际的值来调用该函数。该操作的输出也显示在图 4 中。很容易验证所示的三个输出矩阵是否正确。注意,除了标量和矩阵,张量还提供了向量、行、列类型张量的构造函数。Theano 暂时就介绍到这里了,在讨论概率和统计的进阶话题时我们还会提到它。

再来一点概率论

图 5:算术平均值和标准偏差

现在我们继续讨论概率论和统计。我在上一篇文章中我建议你仔细阅读三篇维基百科文章,然后介绍了正态分布。在我们开始开发人工智能和机器学习程序之前,有必要回顾一些概率论和统计的基本概念。我们首先要介绍的是 算术平均值 arithmetic mean 标准差 standard deviation

算术平均值可以看作是一组数的平均值。标准差可以被认为是一组数的分散程度。如果标准差较小,则表示集合中的元素都接近平均值。相反,如果标准差很大,则表示集合的中的元素分布在较大的范围内。如何使用 Python 计算算术平均值和标准差呢?Python 中有一个名为 statistics 的模块,可用于求平均值和标准差。但专家用户认为这个模块太慢,因此我们选择 NumPy。

图 5 所示的代码打印两个列表 C1C2 的平均值和标准差(我暂时隐藏了两个列表的实际内容)。你能从这些值中看出什么呢?目前它们对你来说只是一些数字而已。现在我告诉你,这些列表分别包含学校 A 和学校 B 的 6 名学生的数学考试成绩(满分 50 分,及格 20 分)。均值告诉我们,两所学校的学生平均成绩都较差,但学校 B 的成绩略好于学校 A。标准差值告诉我们什么呢?学校 B 的巨大的标准差值虽然隐藏在平均值之下,但却清楚地反映了学校 B 的的教学失败。为了进一步加深理解,我将给出两个列表的值,C1 =[20,22,20,22,22,20]C2 =[18,16,17,16,15,48]。这个例子清楚地告诉我们,我们需要更复杂的参数来处理问题的复杂性。概率和统计将提供更复杂的模型来描述复杂和混乱的数据。

随机数生成是概率论的重要组成部分。但实际上我们只能生成伪随机数。伪随机数序列具有和真随机数序列近似的性质。在图 6 中我们介绍了几个生成伪随机数的函数。第 1 行导入 Python 的 random 包。第 2 行代码生成两个随机数,并将它们存储在名为 new_list 的列表中。其中函数 random.random() 生成随机数,代码 new_list = [random.random() for i in range(2)] 使用了 Python 的 列表推导 list comprehension 语法。第 3 行将此列表打印输出。注意,每次执行代码打印出的两个随机数会变化,并且连续两次打印出相同数字的概率理论上为 0。图 6 的第二个代码单元中使用了 random.choice() 函数。这个函数从给定的选项中等概率地选择数据。代码片 random.choice(["Heads", "Tails"]) 将等概率地在“Heads”和“Tails”之间选择。注意,该行代码也使用了列表推导,它会连续执行 3 次选择操作。从图 6 的输出可以看到,三次都选中了“Tails”。

图 6:伪随机数生成

现在,我们用一个简单的例子来说明概率论中著名的 大数定理 xxx 。大数定理表明从大量试验中获得的结果的平均值应该接近期望值,并且随着试验次数的增加这个平均值会越来越接近期望值。我们都知道,投掷一个均匀的骰子得到数字 6 的概率是 1/6。我们用图 7 中的 Python 代码来模拟这个实验。第 1 行导入 Python 的 random 包。第 2 行设置重复试验的次数为 1000。第 3 行将计数器 ct 初始化为 0。第 4 行是一个循环,它将迭代 1000 次。第 5 行的 random.randint(1, 6) 随机生成 1 到 6 之间的整数(包括 1 和 6)。然后检查生成的数字是否等于 6;如果是,则转到第 7 行,将计数器 ct 增加 1。循环迭代 1000 次后,第 8 行打印数字 6 出现的次数与总试验次数之间的比例。图 7 显示该比例为 0.179,略高于期望值 1/6 = 0.1666…。这与期望值的差异还是比较大的。将第 2 行中 n 的值设置为 10000,再次运行代码并观察打印的输出。很可能你会得到一个更接近期望值的数字(它也可能是一个小于期望值的数字)。不断增加第 2 行中 n 的值,你将看到输出越来越接近期望值。

图 7:大数定理

虽然大数定理的描述朴实简单,但如果你了解到哪些数学家证明了大数定理或改进了原有的证明,你一定会大吃一惊的。他们包括卡尔达诺、雅各布·伯努利、丹尼尔·伯努利、泊松、切比雪夫、马尔科夫、博雷尔、坎特利、科尔莫戈罗夫、钦钦等。这些都是各自领域的数学巨匠。

目前我们还没有涵盖概率的随机变量、概率分布等主题,它们对开发人工智能和机器学习程序是必不可少的。我们对概率和统计的讨论仍处于初级阶段,在下一篇文章中还会加强这些知识。与此同时,我们将重逢两个老朋友,Pandas 和 TensorFlow。另外我们还将介绍一个与 TensorFlow 关系密切的库 Keras。

(题图:DA/ea8d9b6a-5282-41ad-a84f-3e3815e359fb)


via: https://www.opensourceforu.com/2022/12/ai-anaconda-and-more-on-probability/

作者:Deepu Benson 选题:lujun9972 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出