rest

package
Version: v0.0.0-...-ec88f2e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2021 License: GPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

canvasctl: Use Canvas LMS from command line

Copyright (C) 2020 Marcus Gelderie

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

Index

Constants

View Source
const BaseUri = "api/v1"

Variables

This section is empty.

Functions

func IsNotAuthorizedError

func IsNotAuthorizedError(err error) bool

func NeedsAuthentication

func NeedsAuthentication(err error) bool

func NewAuthError

func NewAuthError(code int, body []byte, needsAuthentication bool) error

func NewHTTPError

func NewHTTPError(code int, body []byte, err error) error

Types

type API

type API interface {
	GetModules(courseId CanvasId, details bool) ([]Module, error)
	CreateModule(id CanvasId, name string) (Module, error)
	CreateModuleItem(courseId, moduleId CanvasId, item ModuleItem) (ModuleItem, error)
	GetModuleItems(courseId, moduleId CanvasId) ([]ModuleItem, error)
	GetSingleCourse(id CanvasId) (Course, error)
	GetCourses() ([]Course, error)
	GetAccounts() ([]Account, error)
	PushFile(id CanvasId, pushSpec PushFileSpec) (FileUploadResponse, error)
	ListFiles(id CanvasId) ([]FileInfo, error)
	ListAllFoldersInCourse(id CanvasId) ([]FolderInfo, error)
	ModifyFile(id CanvasId, mod url.Values) (FileInfo, error)
	ListQuizzes(id CanvasId) ([]Quiz, error)
	DeleteQuiz(courseId, quizId CanvasId) error
	CreateQuestion(courseId, quizzId CanvasId, params *url.Values) (QuizQuestion, error)
	CreateQuiz(spec QuizCreationSpec, courseId CanvasId) (Quiz, error)
	CreateQuizQuestionGroup(courseId, quizId CanvasId, title string, pick int, points float64) (QuestionGroup, error)
	SetProgressCallback(cb ProgressCallback) ProgressCallback
	ListFilesInFolder(folderId CanvasId) ([]FileInfo, error)
	ResolvePath(courseId CanvasId, path string) ([]FolderInfo, error)
	GetFile(fileId CanvasId) (FileInfo, error)
	ListFoldersInFolder(folderId CanvasId) ([]FolderInfo, error)
}

func NewAPI

func NewAPI(u, token string) (API, error)

type Account

type Account struct {
	Id                         CanvasId `json:"id"`
	Name                       string   `json:"name"`
	Uuid                       string   `json:"uuid"`
	ParentAccountId            CanvasId `json:"parent_account_id"`
	RootAccountId              CanvasId `json:"root_account_id"`
	DefaultStorageQuotaMb      uint32   `json:"default_storage_quota_mb"`
	DefaultUserStorageQuotaMb  uint32   `json:"default_user_storage_quota_mb"`
	DefaultGroupStorageQuotaMb uint32   `json:"default_group_storage_quota_mb"`
	DefaultTimeZone            string   `json:"default_time_zone"`
	SisAccountId               string   `json:"sis_account_id"`
	IntegrationId              string   `json:"integration_id"`
	SisImportId                CanvasId `json:"sis_import_id"`
	LtiGuid                    string   `json:"lti_uid"`
	WorfkflowState             string   `json:"workflow_state"`
}

func (Account) String

func (a Account) String() string

type Answer

type Answer struct {
	Id           CanvasId `json:"id"`
	Text         string   `json:"text"`
	Html         string   `json:"html"`
	Left         string   `json:"left"`
	Right        string   `json:"right"`
	Comments     string   `json:"comments"`
	CommentsHtml string   `json:"comments_html"`
	Weight       float64  `json:"weight"`
	MatchId      CanvasId `json:"match_id"`
}

type ApiImpl

type ApiImpl struct {
	Url                  *url.URL
	Token                string
	ProgressCallbackFunc ProgressCallback
}

func (*ApiImpl) CreateModule

func (c *ApiImpl) CreateModule(id CanvasId, name string) (Module, error)

func (*ApiImpl) CreateModuleItem

func (c *ApiImpl) CreateModuleItem(courseId, moduleId CanvasId, item ModuleItem) (ModuleItem, error)

func (*ApiImpl) CreateQuestion

func (a *ApiImpl) CreateQuestion(courseId, quizId CanvasId, params *url.Values) (QuizQuestion, error)

func (*ApiImpl) CreateQuiz

func (a *ApiImpl) CreateQuiz(
	spec QuizCreationSpec,
	courseId CanvasId,
) (Quiz, error)

func (*ApiImpl) CreateQuizQuestionGroup

func (a *ApiImpl) CreateQuizQuestionGroup(courseId, quizId CanvasId, title string,
	pickCount int, questionPoints float64) (QuestionGroup, error)

func (*ApiImpl) DeleteQuiz

func (a *ApiImpl) DeleteQuiz(courseId, quizId CanvasId) error

func (*ApiImpl) GetAccounts

func (c *ApiImpl) GetAccounts() ([]Account, error)

func (*ApiImpl) GetCourses

func (c *ApiImpl) GetCourses() ([]Course, error)

func (*ApiImpl) GetFile

func (c *ApiImpl) GetFile(fileId CanvasId) (FileInfo, error)

func (*ApiImpl) GetModuleItems

func (c *ApiImpl) GetModuleItems(courseId, moduleId CanvasId) ([]ModuleItem, error)

func (*ApiImpl) GetModules

func (c *ApiImpl) GetModules(courseId CanvasId, details bool) ([]Module, error)

func (*ApiImpl) GetSingleCourse

func (c *ApiImpl) GetSingleCourse(id CanvasId) (Course, error)

func (*ApiImpl) ListAllFoldersInCourse

func (c *ApiImpl) ListAllFoldersInCourse(courseId CanvasId) ([]FolderInfo, error)

func (*ApiImpl) ListFiles

func (c *ApiImpl) ListFiles(id CanvasId) ([]FileInfo, error)

func (*ApiImpl) ListFilesInFolder

func (c *ApiImpl) ListFilesInFolder(folderId CanvasId) ([]FileInfo, error)

func (*ApiImpl) ListFoldersInFolder

func (c *ApiImpl) ListFoldersInFolder(folderId CanvasId) ([]FolderInfo, error)

func (*ApiImpl) ListQuizzes

func (a *ApiImpl) ListQuizzes(id CanvasId) ([]Quiz, error)

func (*ApiImpl) ModifyFile

func (c *ApiImpl) ModifyFile(id CanvasId, mod url.Values) (FileInfo, error)

func (*ApiImpl) PushFile

func (c *ApiImpl) PushFile(id CanvasId, pushSpec PushFileSpec) (FileUploadResponse, error)

func (*ApiImpl) ResolvePath

func (c *ApiImpl) ResolvePath(courseId CanvasId, path string) ([]FolderInfo, error)

func (*ApiImpl) SetProgressCallback

func (c *ApiImpl) SetProgressCallback(cb ProgressCallback) ProgressCallback

type CanvasErrorMessage

type CanvasErrorMessage struct {
	Message string `json:"message"`
}

func (CanvasErrorMessage) String

func (m CanvasErrorMessage) String() string

type CanvasErrors

type CanvasErrors struct {
	Errors []CanvasErrorMessage `json:"errors"`
}

func UnmarshalErrors

func UnmarshalErrors(bytes []byte) CanvasErrors

type CanvasId

type CanvasId uint64

func (CanvasId) ToBase10String

func (id CanvasId) ToBase10String() string

type Command

type Command struct {
	Method string
	Path   string
	Data   []byte
}

type ContentDetails

type ContentDetails struct {
	PointsPossible  float64   `json:"points_possible"`
	DueAt           time.Time `json:"due_at"`
	UnlockAt        time.Time `json:"unlock_at"`
	LockAt          time.Time `json:"lock_at"`
	LockedForUser   bool      `json:"locked_for_user"`
	LockExplanation string    `json:"lock_explanation"`
	LockInfo        string    `json:"lock_info"`
}

type Course

type Course struct {
	Name        string    `json:"name"`
	Id          CanvasId  `json:"id"`
	AccountId   CanvasId  `json:"account_id"`
	SisCourseId string    `json:"sis_course_id"`
	Uuid        string    `json:"uuid"`
	Code        string    `json:"course_code"`
	StartedAt   time.Time `json:"start_at"`
}

func (Course) String

func (a Course) String() string

type FileInfo

type FileInfo struct {
	Id              CanvasId  `json:"id"`
	Uuid            string    `json:"uuid"`
	FolderId        CanvasId  `json:"folder_id"`
	DisplayName     string    `json:"display_name"`
	Filename        string    `json:"filename"`
	ContentType     string    `json:"content-type"`
	Url             string    `json:"url"`
	Size            int       `json:"size"`
	CreatedAt       time.Time `json:"createdAt"`
	UpdatedAt       time.Time `json:"updatedAt"`
	UnlockAt        time.Time `json:"unlockAt"`
	Locked          bool      `json:"locked"`
	Hidden          bool      `json:"hidden"`
	LockAt          time.Time `json:"lockAt"`
	HiddenForUser   bool      `json:"hidden_for_user"`
	ThumbnailUrl    string    `json:"thumbnailUrl"`
	ModifiedAt      time.Time `json:"modifiedAt"`
	MimeClass       string    `json:"mimeClass"`
	MediaEntryId    string    `json:"mediaEntry_id"`
	LockedForUser   bool      `json:"lockedFor_user"`
	LockInfo        string    `json:"lockInfo"`
	LockExplanation string    `json:"lockExplanation"`
	PreviewUrl      string    `json:"previewUrl"`
}

func (FileInfo) String

func (f FileInfo) String() string

type FileModBuilder

type FileModBuilder interface {
	Published(bool) FileModBuilder
	Move(newParentFolderId CanvasId) FileModBuilder
	Build() url.Values
}

func MakeFileModification

func MakeFileModification() FileModBuilder

type FileUploadResponse

type FileUploadResponse struct {
	Id          CanvasId `json:"id"`
	Url         string   `json:"url"`
	ContentType string   `json:"content-type"`
	DisplayName string   `json:"display_name"`
	Size        int64    `json:"size"`
}

type FolderInfo

type FolderInfo struct {
	ContextType    string    `json:"context_type"`
	ContextId      CanvasId  `json:"context_id"`
	FilesCount     int       `json:"files_count"`
	Position       int       `json:"position"`
	UpdatedAt      time.Time `json:"updated_at"`
	FoldersUrl     string    `json:"folders_url"`
	FilesUrl       string    `json:"files_url"`
	FullName       string    `json:"full_name"`
	LockAt         time.Time `json:"lock_at"`
	Id             CanvasId  `json:"id"`
	FoldersCount   int       `json:"folders_count"`
	Name           string    `json:"name"`
	ParentFolderId CanvasId  `json:"parent_folder_id"`
	CreatedAt      time.Time `json:"created_at"`
	UnlockAt       time.Time `json:"unlock_at"`
	Hidden         bool      `json:"hidden"`
	HiddenForUser  bool      `json:"hidden_for_user"`
	Locked         bool      `json:"locked"`
	LockedForUser  bool      `json:"locked_for_user"`
	ForSubmissions bool      `json:"for_submissions"`
}

func (FolderInfo) String

func (f FolderInfo) String() string

type HTTPError

type HTTPError interface {
	Code() int
	InnerError() error
	Body() []byte
	error
}

func IsHTTPError

func IsHTTPError(err error) (HTTPError, bool)

type Match

type Match struct {
	Text    string   `json:"text"`
	MatchId CanvasId `json:"match_id"`
}

type Module

type Module struct {
	Id                        CanvasId     `json:"id"`
	Name                      string       `json:"name"`
	Published                 bool         `json:"published"`
	Position                  uint32       `json:"position"`
	Items                     []ModuleItem `json:"items"`
	ItemsCount                uint32       `json:"items_count"`
	ItemsUrl                  string       `json:"items_url"`
	WorkflowState             string       `json:"workflow_state"`
	UnlockAt                  string       `json:"unlock_at"`
	RequireSequentialProgress bool         `json:"require_sequential_progress"`
	PrerequisiteModuleIds     []CanvasId   `json:"prerequisite_module_ids"`
	State                     string       `json:"state"`
	CompletedAt               string       `json:"completed_at"`
	PublishFinalGrade         bool         `json:"publish_final_grade"`
}

func (Module) String

func (m Module) String() string

type ModuleItem

type ModuleItem struct {
	Id                    CanvasId       `json:"id"`
	Title                 string         `json:"title"`
	ModuleId              CanvasId       `json:"module_id"`
	Position              int            `json:"position"`
	Indent                int            `json:"indent"`
	Type                  string         `json:"type"`
	ContentId             CanvasId       `json:"content_id"`
	HtmlUrl               string         `json:"html_url"`
	Url                   string         `json:"url"`
	PageUrl               string         `json:"page_url"`
	ExternalUrl           string         `json:"external_url"`
	NewTab                bool           `json:"new_tab"`
	CompletionRequirement string         `json:"completion_requirement"`
	Details               ContentDetails `json:"content_details"`
	Published             bool           `json:"published"`
}

func (ModuleItem) String

func (m ModuleItem) String() string

type ProgressCallback

type ProgressCallback func(ProgressCallbackContext)

type ProgressCallbackContext

type ProgressCallbackContext int
const (
	ProgresStart ProgressCallbackContext = iota
	ProgressContinue
	ProgressStop
)

type PushFileSpec

type PushFileSpec struct {
	File           io.Reader
	FileSize       uint64
	RemoteFilename string
	RemotePath     string
}

type QuestionGroup

type QuestionGroup struct {
	Id                       CanvasId `json:"id"`
	QuizId                   CanvasId `json:"quiz_id"`
	Name                     string   `json:"name"`
	PickCount                int      `json:"pick_count"`
	QuestionPoints           float64  `json:"question_points"`
	Position                 int      `json:"position"`
	AssessmentQuestionBankId CanvasId `json:"assessment_question_bank_id"`
}

type Quiz

type Quiz struct {
	Id                            CanvasId    `json:"id"`
	Title                         string      `json:"title"`
	HtmlUrl                       string      `json:"html_url"`
	MobileUrl                     string      `json:"mobile_url"`
	PreviewUrl                    string      `json:"preview_url"`
	Description                   string      `json:"description"`
	QuizType                      string      `json:"quiz_type"`
	AssignmentGroupId             CanvasId    `json:"assignment_group_id"`
	TimeLimit                     int64       `json:"time_limit"`
	ShuffleAnswers                bool        `json:"shuffle_answers"`
	HideResults                   string      `json:"hide_results"`
	ShowCorrectAnswers            bool        `json:"show_correct_answers"`
	ShowCorrectAnswersLastAttempt bool        `json:"show_correct_answers_last_attempt"`
	ShowCorrectAnswersAt          time.Time   `json:"show_correct_answers_at"`
	HideCorrectAnswersAt          time.Time   `json:"hide_correct_answers_at"`
	OneTimeResults                bool        `json:"one_time_results"`
	ScoringPolicy                 string      `json:"scoring_policy"`
	AllowedAttempts               int64       `json:"allowed_attempts"`
	OneQuestionAtATime            bool        `json:"one_question_at_a_time"`
	QuestionCount                 int64       `json:"question_count"`
	PointsPossible                float64     `json:"points_possible"`
	CantGoBack                    bool        `json:"cant_go_back"`
	AccessCode                    string      `json:"access_code"`
	IpFilter                      string      `json:"ip_filter"`
	DueAt                         time.Time   `json:"due_at"`
	LockAt                        time.Time   `json:"lock_at"`
	UnlockAt                      time.Time   `json:"unlock_at"`
	Published                     bool        `json:"published"`
	Unpublishable                 bool        `json:"unpublishable"`
	LockedForUser                 bool        `json:"locked_for_user"`
	LockInfo                      interface{} `json:"lock_info"`
	LockExplanation               string      `json:"lock_explanation"`
	SpeedgraderUrl                string      `json:"speedgrader_url"`
	QuizExtensionsUrl             string      `json:"quiz_extensions_url"`
	Permissions                   interface{} `json:"permissions"`
	AllDates                      interface{} `json:"all_dates"`
	VersionNumber                 int64       `json:"version_number"`
	QuestionTypes                 []string    `json:"question_types"`
	AnonymousSubmissions          bool        `json:"anonymous_submissions"`
}

func (Quiz) String

func (q Quiz) String() string

type QuizCreationSpec

type QuizCreationSpec struct {
	Title                         string
	QuizType                      *string
	Text                          *string
	HideResults                   *string
	ShowCorrectAnswersAt          *string
	HideCorrectAnswersAt          *string
	ScoringPolicy                 *string
	AccessCode                    *string
	IPFilter                      *string
	DueAt                         *string
	LockAt                        *string
	UnlockAt                      *string
	TimeLimit                     *int
	AllowedAttempts               *int
	ShuffleAnswers                *bool
	ShowCorrectAnswers            *bool
	ShowCorrectAnswersLastAttempt *bool
	OneQuestionAtATime            *bool
	CantGoBack                    *bool
	Published                     *bool
	OneTimeResults                *bool
	AssignmentGroupId             *CanvasId
}

type QuizQuestion

type QuizQuestion struct {
	Id                CanvasId `json:"id"`
	QuizId            CanvasId `json:"quiz_id"`
	Position          int      `json:"position"`
	QuestionName      string   `json:"question_name"`
	QuestionType      string   `json:"question_type"`
	QuestionText      string   `json:"question_text"`
	PointsPossible    float64  `json:"points_possible"`
	CorrectComments   string   `json:"correct_comments"`
	IncorrectComments string   `json:"incorrect_comments"`
	NeutralComments   string   `json:"neutral_comments"`
	Answers           []Answer `json:"answers"`
	Matches           []Match  `json:"matches"`
}

Directories

Path Synopsis
canvasctl: Use Canvas LMS from command line Copyright (C) 2020 Marcus Gelderie This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
canvasctl: Use Canvas LMS from command line Copyright (C) 2020 Marcus Gelderie This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
t or T : Toggle theme light dark auto
y or Y : Canonical URL