Setting Up Ubuntu

Introduction

This post is where I keep notes on configurations for Ubuntu and some applications for personal reference.

Controlling the boot process

The grub file controls the behavior of the GRand Unified Bootloader, which is a flexible and powerful boot loader program. The full path to the file is /etc/default/grub; use the following command to edit the file:

sudo nano /etc/default/grub

There are two settings that may be of particular interest:

  1. GRUB_DEFAULT=0: choose Ubuntu as the default operating system.
  2. GRUB_TIMEOUT=5: boot the default system in five seconds if no keyboard input is performed during this time.

Remember to run the following command after changing the settings to update the /boot/grub/grub.cfg file:

sudo update-grub

Dealing with a dual-boot system time conflict

When Linux and Windows constitute a dual-boot system, there will be an issue concerning system time. The solution is to use the following command to tell Ubuntu that the hardware clock is set to ‘local’ time:

sudo timedatectl set-local-rtc 1 --adjust-system-clock

Note that invoking timedatectl set-local-rtc 1 will also synchronize the RTC (i.e., real-time clock, which stores hardware time) from the system clock (which stores system time), unless --adjust-system-clock is passed.

The following command can be used to update system time, but note that the program is no longer installed on Ubuntu by default (install it with sudo apt install ntpsec-ntpdate).

sudo ntpdate pool.ntp.org

Auto mount a partition on boot

Use the lsblk command to find out the UUID of a partition; example:

lsblk -o NAME,SIZE,FSTYPE,LABEL,UUID,MOUNTPOINT

# NAME   SIZE   FSTYPE  LABEL  UUID             MOUNTPOINT
# sdb    447.1G ntfs    data   32FBCAAA07932A36           
# └─sdb1 447.1G ntfs    newvol D0BE3DB6BE3D95C7           

Use the id command to find out user and group IDs; example:

id

# userid=1000(username) groupid=1000(username) group=1000(username)

Edit the /etc/fstab file to auto mount a partition (e.g., to /data) on boot. Note that you should specify the UUID of a partition, not that of a disk. In the given example, it should be D0BE3DB6BE3D95C7 (sdb1), not 32FBCAAA07932A36 (sdb). An example entry could be:

UUID="D0BE3DB6BE3D95C7" /data ntfs uid=1000,gid=1000,umask=0077

Cron jobs

Use crontab -e to add the following entries (custom commands and scripts can be found in the Custom commands and scripts section of this post):

# move expired files/directories from /data/tmp/ to trash
0 */2 * * * ${HOME}/.local/bin/cleanup --expiration-time 1209600 directory --directory /data/tmp/ >> ${HOME}/cleanup.log 2>&1

# delete expired files/directories from trash 
0 */2 * * * export XDG_RUNTIME_DIR=/run/user/$(id -u); ${HOME}/.local/bin/cleanup --expiration-time 2419200 trash >> ${HOME}/cleanup.log 2>&1

Note that by default, crons job are executed with a very limited set of environment variables. Therefore, it is necessary to declare variables before executing a command that requires them.

Environment variables

Set LANGUAGE=en to ensure various messages (e.g., man pages and help info for commands) are shown in English:

echo "export LANGUAGE=en" >> ~/.profile

File templates

if [[ -d "${HOME}/模板" ]]; then
  TEMPLATESDIR="${HOME}/模板"
elif [[ -d "${HOME}/Templates" ]]; then
  TEMPLATESDIR="${HOME}/Templates"
fi

# CSV file template
echo '' > "${TEMPLATESDIR}/CSVfile.csv"

# Python script template
echo '#!/usr/bin/python3' > "${TEMPLATESDIR}/Pythonscript.py"

# Markdown template
echo '' > "${TEMPLATESDIR}/Markdown.md"

# R Markdown template
echo '---
title: "Untitled"
author: "zenggyu"
date: "2022-06-24"
output: html_document
---' > "${TEMPLATESDIR}/RMarkdown.Rmd"

# R script template
echo '#!/usr/bin/Rscript

library(tidyverse)
library(lubridate)
library(glue)' > "${TEMPLATESDIR}/Rscript.R"

# Shell script template
echo '#!/usr/bin/bash' > "${TEMPLATESDIR}/Shellscript.sh"

# SQL script template
echo '' > "${TEMPLATESDIR}/SQLscript.sql"

# Text file template
echo '' > "${TEMPLATESDIR}/Textfile.txt"

Miscellaneous applications

To install some frequently used applications:

sudo apt install aria2 fcitx filezilla flameshot gimp git keepassxc meld tree virtualbox virtualbox-ext-pack vlc obs-studio kdenlive

After installing flameshot, add an entry to the “Startup Applications” app to launch it at startup (e.g., Name: flameshot; Command: /usr/bin/flameshot). Then, add a keyboard shortcut in “Settings” -> “Keyboard” -> “Keyboard Shortcuts” -> “View and Customize Shortcuts” -> “Custom Shortcuts” (e.g., Name: flameshot; Command: /usr/bin/flameshot gui; Shortcut: Ctrl + Alt + A). Note that while the command for the startup setting is /usr/bin/flameshot, the command for the shortcut is /usr/bin/flameshot gui.

To install and configure ActivityWatch, execute the following commands and then add an entry to the “Startup Applications” app (e.g., Name: activitywatch; Command: /opt/activitywatch/aw-qt):

wget -O ./activitywatch.zip https://github.com/ActivityWatch/activitywatch/releases/download/<version>/activitywatch-<version>-linux-x86_64.zip # substitute <version> for the desired version number
unzip ./activitywatch.zip && sudo mv ./activitywatch/ /opt/
sudo chmod a+rx /opt/activitywatch/qw-qt

echo '[aw-watcher-window]
exclude_title = true
poll_time = 1.0
' > "${HOME}/.config/activitywatch/aw-watcher-window/aw-watcher-window.toml"

echo '[aw-watcher-afk]
timeout = 180
poll_time = 5.0
' > "${HOME}/.config/activitywatch/aw-watcher-afk/aw-watcher-afk.toml"

More detailed installation/setup guides for some other software are listed below:

Custom commands and scripts

Save the following script to ~/.local/bin/cleanup and make it executable (chmod u+x ~/.local/bin/cleanup):

#!/usr/bin/env python3

import os
import re
import sys
import time
import argparse
import subprocess
from pathlib import Path
from datetime import datetime
from urllib.parse import urlparse
from urllib.request import url2pathname

def cleanup_directory(args):
  """
  Move items in the specified directory to trash after expiration time.
  """
  current_user = subprocess.run(["id", "-n", "-u"], capture_output = True, text = True).stdout.rstrip()
  current_time = time.time()
  expiration_time = args.expiration_time
  directory = Path(args.directory).absolute()
  if subprocess.run(["which", "gio"], capture_output = True, text = True).stdout == "":
    print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"), "`gio`: command not found.")
    sys.exit(1)
  elif not directory.exists():
    print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"), str(directory), "does not exist.")
    sys.exit(1)
  elif not directory.is_dir():
    print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"), str(directory), "is not a directory.")
    sys.exit(1)
  elif str(directory) == "/" or str(directory.parent) == "/":
    print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"), str(directory), "is root directory or is directly under root directory.")
    sys.exit(1)
  else:
    for child in directory.iterdir():
      # deals with bad link and user privilege
      if not (child.exists() and child.owner() == current_user):
        continue
      if current_time - child.stat().st_atime > expiration_time and \
         current_time - child.stat().st_mtime > expiration_time and \
         current_time - child.stat().st_ctime > expiration_time:
        if child.is_file():
          print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"),
          "Moving",
          str(child.absolute()),
          "(" + "access time:", datetime.fromtimestamp(child.stat().st_atime).strftime("%Y-%m-%d %H:%M:%S") + ",",
          "modify time:", datetime.fromtimestamp(child.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") + ",",
          "change time:", datetime.fromtimestamp(child.stat().st_ctime).strftime("%Y-%m-%d %H:%M:%S") + ")",
          "to trash...",
          end = " ")
          # use uri instead of raw path to avoid meta characters
          result = subprocess.run(["gio", "trash", child.absolute().as_uri()], capture_output = True, text = True)
          if result.returncode == 0:
            print("Succeeded.")
          else:
            print("Failed.", "Standard Error:", result.stderr)
        elif child.is_dir():
          keep = False
          for grandchild in child.rglob("*"):
            # deals with bad link
            if not grandchild.exists():
              continue
            if not (current_time - grandchild.stat().st_atime > expiration_time and \
                    current_time - grandchild.stat().st_mtime > expiration_time and \
                    current_time - grandchild.stat().st_ctime > expiration_time and \
                    grandchild.owner() == current_user):
              keep = True
              break
          if not keep:
            print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"),
            "Moving",
            str(child.absolute()),
            "(" + "access time:", datetime.fromtimestamp(child.stat().st_atime).strftime("%Y-%m-%d %H:%M:%S") + ",",
            "modify time:", datetime.fromtimestamp(child.stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S") + ",",
            "change time:", datetime.fromtimestamp(child.stat().st_ctime).strftime("%Y-%m-%d %H:%M:%S") + ")",
            "to trash...",
            end = " ")
            # use uri instead of raw path to avoid meta characters
            result = subprocess.run(["gio", "trash", child.absolute().as_uri()], capture_output = True, text = True)
            if result.returncode == 0:
              print("Succeeded.")
            else:
              print("Failed.", "Standard Error:", result.stderr)
    return None

def cleanup_trash(args):
  """
  Delete items in trash after expiration time.
  """
  current_time = time.time()
  expiration_time = args.expiration_time
  if subprocess.run(["which", "gio"], capture_output = True, text = True).stdout == "":
    print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"), "`gio`: command not found.")
    sys.exit(1)
  elif os.environ.get("XDG_RUNTIME_DIR") is None:
    print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"), "Environment variable ${XDG_RUNTIME_DIR} is not set.")
    sys.exit(1)
  else:
    # use the `-u` option to get uri instead of raw path, so as to avoid meta characters
    trash_list = subprocess.run(["gio", "list", "-h", "-n", "-u", "-a", "time::changed", "trash://"], capture_output = True, text = True).stdout.rstrip("\n").split("\n")
    for trash_item in trash_list:
      trash_item_info = re.search("^(trash://\\S+)\t\\d+\t\\((regular|directory)\\)\ttime::changed=(\\d+)$", trash_item)
      if trash_item != "" and trash_item_info is None:
        print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"), "Cannot identify any trash item because regex pattern is not matched.")
        break
      else:
        trash_item_uri, _, trash_item_ctime = trash_item_info.groups()
        trash_item_ctime = float(trash_item_ctime)
        if current_time - trash_item_ctime > expiration_time:
          print(datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S"),
          "Deleting",
          url2pathname(urlparse(trash_item_uri).path),
          "(" + "change time:", datetime.fromtimestamp(trash_item_ctime).strftime("%Y-%m-%d %H:%M:%S") + ")",
          "from trash...",
          end = " ")
          result = subprocess.run(["gio", "remove", trash_item_uri], capture_output = True, text = True)
          if result.returncode == 0:
            print("Succeeded.")
          else:
            print("Failed.", "Standard Error:", result.stderr)
    return None

if __name__ == "__main__":
  parser = argparse.ArgumentParser()
  subparsers = parser.add_subparsers(dest = "subcommand", required = True)
  parser.add_argument("-t", "--expiration-time", dest = "expiration_time", type = int, required = True, help = "Amount of time (in seconds) after which the item(s) will be moved to trash or deleted from trash.")
  # argument parser for `cleanup directory`
  parser_cleanup_directory = subparsers.add_parser("directory")
  parser_cleanup_directory.set_defaults(func = cleanup_directory)
  parser_cleanup_directory.add_argument("-d", "--directory", dest = "directory", type = str, required = True, help = "Path of the directory whose items will be cleaned after expiration time.")
  # argument parser for `cleanup trash`
  parser_cleanup_trash = subparsers.add_parser("trash")
  parser_cleanup_trash.set_defaults(func = cleanup_trash)
  # execute command
  args = parser.parse_args()
  args.func(args)

Related

Next
Previous
comments powered by Disqus