Oxygen colours, a Get started walkthrough, Create Identity (0.14.0) 013fefef11e3
The history graph draws lanes in the Oxygen mark's palette, heads glow and the working copy breathes (still under reduced motion); signed and sealed changes are marked and coloured. All of it is themeable (oxygen.lane1-8, head, workingCopy, signed, sealed, invalid), and file letters use oxygen.added/modified/removed/renamed/conflict/resolved, defaulting to Git's. A Get started with Oxygen walkthrough ticks off identity, a repository, a commit, a bookmark, a push and the history; Oxygen: Create Identity makes the signing key without a terminal. closes #8
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-09-29 18:37 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
013fefef11e31efd0f80676878bd6e7389d2a0223484ce406daa7ea0ccdf6558- Depends on
- 0c26846a9a81 1f7fc1e3ae4a ab81294a9ba9 b4626290e660 bed495a96408 cdcffe39dd9f d2c9a5843004 f532c4266f21 f6bda77ecb9f
- Depended on by
- dac6876e4d08 b55e0d00545e
- In bookmarks
- main
- In tags
- v0.16.0 v0.16.1 v0.17.0 v0.17.1 v0.18.0
- Edits
- 30 atoms, 7 tree operations, 13 files
Browse the tree at this change
added walkthrough/bookmarks.md +3 −0
| 1 | + | # Bookmarks |
|
| 2 | + | ||
| 3 | + | Bookmarks are Oxygen's branches. Because changes commute, merging two bookmarks is the union of their changes: no merge change, and a real conflict is recorded where it is, to resolve whenever you like. |
added walkthrough/commit.md +3 −0
| 1 | + | # Committing |
|
| 2 | + | ||
| 3 | + | The working copy is itself a change, snapshotted as you work. **Commit** turns it into a change with your message, signed when you push. Changes keep their identity across edits, moves and rewrites. |
added walkthrough/history.md +3 −0
| 1 | + | # History |
|
| 2 | + | ||
| 3 | + | The history graph shows each change in its lane: heads glow, the working copy breathes, and a ◈ marks a change sealed on push, ✔ one signed on its own. |
added walkthrough/identity.md +5 −0
| 1 | + | # Your identity |
|
| 2 | + | ||
| 3 | + | Oxygen signs what you publish. `oxy identity create` makes a key pair for you (a classical and a post-quantum half, so a signature holds up later) and records you as the author of your changes. |
|
| 4 | + | ||
| 5 | + | The secret key stays in your key directory; the hub only ever sees the public half. |
added walkthrough/push.md +3 −0
| 1 | + | # Pushing |
|
| 2 | + | ||
| 3 | + | `oxy push` sends your changes, one seal over all of them, and a signed attestation moving the bookmark. The hub checks both against the repository's trust policy. |
added walkthrough/repository.md +3 −0
| 1 | + | # A repository |
|
| 2 | + | ||
| 3 | + | An Oxygen repository is a set of **changes** that commute: independent work never needs a merge. **Clone** takes one from a hub (`oxy clone URL`); **Initialize** starts one in the open folder. |
modified CHANGELOG.md +8 −0
| 1 | 1 | # Changelog |
|
| 2 | 2 | ||
| 3 | + | ## 0.14.0 (2026-09-29) |
|
| 4 | + | ||
| 5 | + | - **Oxygen colours.** The history graph draws each lane in the Oxygen mark's palette. Heads glow, and the working copy breathes (it holds still if your system asks for reduced motion). ✔ marks a change signed on its own, ◈ one sealed on push, both coloured. |
|
| 6 | + | - Every colour is themeable: `oxygen.lane1`…`oxygen.lane8`, `oxygen.head`, `oxygen.workingCopy`, `oxygen.signed`, `oxygen.sealed` and `oxygen.invalid`. |
|
| 7 | + | - File letters use `oxygen.added`, `oxygen.modified`, `oxygen.removed`, `oxygen.renamed`, `oxygen.conflict` and `oxygen.resolved`. They default to Git's colours, so a theme can style Oxygen apart. |
|
| 8 | + | - **Get started with Oxygen.** A walkthrough on the Welcome page covers creating your identity, opening or cloning a repository, committing, bookmarks, pushing and the history. Each step ticks itself off as you do it. |
|
| 9 | + | - **Oxygen: Create Identity…** makes your signing identity (`oxy identity create`) without a terminal. |
|
| 10 | + | ||
| 3 | 11 | ## 0.13.0 (2026-09-29) |
|
| 4 | 12 | ||
| 5 | 13 | - **No more flashing Source Control.** With a subrepo in the workspace, the list of changes flickered because each refresh started another. `oxy status` rewrote the subrepo's own `.oxy` files, and only the top-level `.oxy` was ignored. Every `.oxy` store is now ignored, and oxy itself (from the same day) no longer rewrites an unchanged status file. |
|
| … | 84 unchanged lines |
modified README.md +4 −0
| … | 94 unchanged lines |
||
| 95 | 95 | ||
| 96 | 96 | The **Oxygen Trust** view shows the repository's trust policy and each change's signature. **Verify Signatures** checks them all. |
|
| 97 | 97 | ||
| 98 | + | ## Colours and getting started |
|
| 99 | + | ||
| 100 | + | The history graph and the file letters use Oxygen's own themeable colours: `oxygen.lane1`…`oxygen.lane8` for the graph's lanes, plus `oxygen.head`, `oxygen.workingCopy`, `oxygen.signed`, `oxygen.sealed`, `oxygen.invalid` and `oxygen.added`/`modified`/`removed`/`renamed`/`conflict`/`resolved`. Set them under `workbench.colorCustomizations`. **Get started with Oxygen**, on the Welcome page, walks through a first repository. **Oxygen: Create Identity…** makes your signing key. |
|
| 101 | + | ||
| 98 | 102 | ## Settings |
|
| 99 | 103 | ||
| 100 | 104 | | Setting | Default | What it does | |
|
| … | 31 unchanged lines |
modified package.json +274 −2
| … | 1 unchanged lines |
||
| 2 | 2 | "name": "oxygen-vscode", |
|
| 3 | 3 | "displayName": "Oxygen", |
|
| 4 | 4 | "description": "Oxygen version control: source control view, diffs, decorations, bookmarks and history for oxy repositories", |
|
| 5 | - | "version": "0.13.0", |
|
| 5 | + | "version": "0.14.0", |
|
| 6 | 6 | "publisher": "oxygen", |
|
| 7 | 7 | "license": "MIT OR Apache-2.0", |
|
| 8 | 8 | "icon": "assets/images/oxygen.png", |
|
| … | 332 unchanged lines |
||
| 341 | 341 | "title": "Copy Permalink", |
|
| 342 | 342 | "category": "Oxygen", |
|
| 343 | 343 | "icon": "$(pinned)" |
|
| 344 | + | }, |
|
| 345 | + | { |
|
| 346 | + | "command": "oxygen.createIdentity", |
|
| 347 | + | "title": "Create Identity\u2026", |
|
| 348 | + | "category": "Oxygen" |
|
| 344 | 349 | } |
|
| 345 | 350 | ], |
|
| 346 | 351 | "views": { |
|
| … | 443 unchanged lines |
||
| 790 | 795 | "type": "boolean", |
|
| 791 | 796 | "default": true, |
|
| 792 | 797 | "description": "Show the bookmark, working copy and state in the status bar." |
|
| 798 | + | } |
|
| 799 | + | } |
|
| 800 | + | }, |
|
| 801 | + | "colors": [ |
|
| 802 | + | { |
|
| 803 | + | "id": "oxygen.lane1", |
|
| 804 | + | "description": "History graph: lane 1 (the Oxygen mark's palette).", |
|
| 805 | + | "defaults": { |
|
| 806 | + | "dark": "#35C7FF", |
|
| 807 | + | "light": "#0A74B8", |
|
| 808 | + | "highContrast": "#35C7FF", |
|
| 809 | + | "highContrastLight": "#0A74B8" |
|
| 810 | + | } |
|
| 811 | + | }, |
|
| 812 | + | { |
|
| 813 | + | "id": "oxygen.lane2", |
|
| 814 | + | "description": "History graph: lane 2 (the Oxygen mark's palette).", |
|
| 815 | + | "defaults": { |
|
| 816 | + | "dark": "#3D7BFF", |
|
| 817 | + | "light": "#2553C9", |
|
| 818 | + | "highContrast": "#3D7BFF", |
|
| 819 | + | "highContrastLight": "#2553C9" |
|
| 820 | + | } |
|
| 821 | + | }, |
|
| 822 | + | { |
|
| 823 | + | "id": "oxygen.lane3", |
|
| 824 | + | "description": "History graph: lane 3 (the Oxygen mark's palette).", |
|
| 825 | + | "defaults": { |
|
| 826 | + | "dark": "#2BD9B3", |
|
| 827 | + | "light": "#0E8A70", |
|
| 828 | + | "highContrast": "#2BD9B3", |
|
| 829 | + | "highContrastLight": "#0E8A70" |
|
| 830 | + | } |
|
| 831 | + | }, |
|
| 832 | + | { |
|
| 833 | + | "id": "oxygen.lane4", |
|
| 834 | + | "description": "History graph: lane 4 (the Oxygen mark's palette).", |
|
| 835 | + | "defaults": { |
|
| 836 | + | "dark": "#8E7CFF", |
|
| 837 | + | "light": "#5B45D6", |
|
| 838 | + | "highContrast": "#8E7CFF", |
|
| 839 | + | "highContrastLight": "#5B45D6" |
|
| 840 | + | } |
|
| 841 | + | }, |
|
| 842 | + | { |
|
| 843 | + | "id": "oxygen.lane5", |
|
| 844 | + | "description": "History graph: lane 5 (the Oxygen mark's palette).", |
|
| 845 | + | "defaults": { |
|
| 846 | + | "dark": "#56A8FF", |
|
| 847 | + | "light": "#1F6FC2", |
|
| 848 | + | "highContrast": "#56A8FF", |
|
| 849 | + | "highContrastLight": "#1F6FC2" |
|
| 850 | + | } |
|
| 851 | + | }, |
|
| 852 | + | { |
|
| 853 | + | "id": "oxygen.lane6", |
|
| 854 | + | "description": "History graph: lane 6 (the Oxygen mark's palette).", |
|
| 855 | + | "defaults": { |
|
| 856 | + | "dark": "#5FE3A1", |
|
| 857 | + | "light": "#1A7850", |
|
| 858 | + | "highContrast": "#5FE3A1", |
|
| 859 | + | "highContrastLight": "#1A7850" |
|
| 860 | + | } |
|
| 861 | + | }, |
|
| 862 | + | { |
|
| 863 | + | "id": "oxygen.lane7", |
|
| 864 | + | "description": "History graph: lane 7 (the Oxygen mark's palette).", |
|
| 865 | + | "defaults": { |
|
| 866 | + | "dark": "#5B6CFF", |
|
| 867 | + | "light": "#3440B8", |
|
| 868 | + | "highContrast": "#5B6CFF", |
|
| 869 | + | "highContrastLight": "#3440B8" |
|
| 870 | + | } |
|
| 871 | + | }, |
|
| 872 | + | { |
|
| 873 | + | "id": "oxygen.lane8", |
|
| 874 | + | "description": "History graph: lane 8 (the Oxygen mark's palette).", |
|
| 875 | + | "defaults": { |
|
| 876 | + | "dark": "#1FB6D6", |
|
| 877 | + | "light": "#0B7C93", |
|
| 878 | + | "highContrast": "#1FB6D6", |
|
| 879 | + | "highContrastLight": "#0B7C93" |
|
| 880 | + | } |
|
| 881 | + | }, |
|
| 882 | + | { |
|
| 883 | + | "id": "oxygen.head", |
|
| 884 | + | "description": "A bookmark head in the history graph.", |
|
| 885 | + | "defaults": { |
|
| 886 | + | "dark": "#5FE3A1", |
|
| 887 | + | "light": "#1A7850", |
|
| 888 | + | "highContrast": "#5FE3A1", |
|
| 889 | + | "highContrastLight": "#1A7850" |
|
| 890 | + | } |
|
| 891 | + | }, |
|
| 892 | + | { |
|
| 893 | + | "id": "oxygen.workingCopy", |
|
| 894 | + | "description": "The working copy in the history graph.", |
|
| 895 | + | "defaults": { |
|
| 896 | + | "dark": "#35C7FF", |
|
| 897 | + | "light": "#0A74B8", |
|
| 898 | + | "highContrast": "#35C7FF", |
|
| 899 | + | "highContrastLight": "#0A74B8" |
|
| 900 | + | } |
|
| 901 | + | }, |
|
| 902 | + | { |
|
| 903 | + | "id": "oxygen.signed", |
|
| 904 | + | "description": "A change signed by its author.", |
|
| 905 | + | "defaults": { |
|
| 906 | + | "dark": "#5FE3A1", |
|
| 907 | + | "light": "#1A7850", |
|
| 908 | + | "highContrast": "#5FE3A1", |
|
| 909 | + | "highContrastLight": "#1A7850" |
|
| 910 | + | } |
|
| 911 | + | }, |
|
| 912 | + | { |
|
| 913 | + | "id": "oxygen.sealed", |
|
| 914 | + | "description": "A change authenticated by a seal (one signature over a push).", |
|
| 915 | + | "defaults": { |
|
| 916 | + | "dark": "#8E7CFF", |
|
| 917 | + | "light": "#6B3FD1", |
|
| 918 | + | "highContrast": "#8E7CFF", |
|
| 919 | + | "highContrastLight": "#6B3FD1" |
|
| 920 | + | } |
|
| 921 | + | }, |
|
| 922 | + | { |
|
| 923 | + | "id": "oxygen.invalid", |
|
| 924 | + | "description": "A signature that does not verify.", |
|
| 925 | + | "defaults": { |
|
| 926 | + | "dark": "#FF5E7A", |
|
| 927 | + | "light": "#C62D47", |
|
| 928 | + | "highContrast": "#FF5E7A", |
|
| 929 | + | "highContrastLight": "#C62D47" |
|
| 930 | + | } |
|
| 931 | + | }, |
|
| 932 | + | { |
|
| 933 | + | "id": "oxygen.added", |
|
| 934 | + | "description": "A file the working copy adds. Defaults to Git's colour; themes can set Oxygen's apart.", |
|
| 935 | + | "defaults": { |
|
| 936 | + | "dark": "gitDecoration.addedResourceForeground", |
|
| 937 | + | "light": "gitDecoration.addedResourceForeground", |
|
| 938 | + | "highContrast": "gitDecoration.addedResourceForeground", |
|
| 939 | + | "highContrastLight": "gitDecoration.addedResourceForeground" |
|
| 793 | 940 | } |
|
| 941 | + | }, |
|
| 942 | + | { |
|
| 943 | + | "id": "oxygen.modified", |
|
| 944 | + | "description": "A file the working copy changes. Defaults to Git's colour; themes can set Oxygen's apart.", |
|
| 945 | + | "defaults": { |
|
| 946 | + | "dark": "gitDecoration.modifiedResourceForeground", |
|
| 947 | + | "light": "gitDecoration.modifiedResourceForeground", |
|
| 948 | + | "highContrast": "gitDecoration.modifiedResourceForeground", |
|
| 949 | + | "highContrastLight": "gitDecoration.modifiedResourceForeground" |
|
| 950 | + | } |
|
| 951 | + | }, |
|
| 952 | + | { |
|
| 953 | + | "id": "oxygen.removed", |
|
| 954 | + | "description": "A file the working copy removes. Defaults to Git's colour; themes can set Oxygen's apart.", |
|
| 955 | + | "defaults": { |
|
| 956 | + | "dark": "gitDecoration.deletedResourceForeground", |
|
| 957 | + | "light": "gitDecoration.deletedResourceForeground", |
|
| 958 | + | "highContrast": "gitDecoration.deletedResourceForeground", |
|
| 959 | + | "highContrastLight": "gitDecoration.deletedResourceForeground" |
|
| 960 | + | } |
|
| 961 | + | }, |
|
| 962 | + | { |
|
| 963 | + | "id": "oxygen.renamed", |
|
| 964 | + | "description": "A file the working copy moves. Defaults to Git's colour; themes can set Oxygen's apart.", |
|
| 965 | + | "defaults": { |
|
| 966 | + | "dark": "gitDecoration.renamedResourceForeground", |
|
| 967 | + | "light": "gitDecoration.renamedResourceForeground", |
|
| 968 | + | "highContrast": "gitDecoration.renamedResourceForeground", |
|
| 969 | + | "highContrastLight": "gitDecoration.renamedResourceForeground" |
|
| 970 | + | } |
|
| 971 | + | }, |
|
| 972 | + | { |
|
| 973 | + | "id": "oxygen.conflict", |
|
| 974 | + | "description": "A file with a conflict, or conflict markers left in it. Defaults to Git's colour; themes can set Oxygen's apart.", |
|
| 975 | + | "defaults": { |
|
| 976 | + | "dark": "gitDecoration.conflictingResourceForeground", |
|
| 977 | + | "light": "gitDecoration.conflictingResourceForeground", |
|
| 978 | + | "highContrast": "gitDecoration.conflictingResourceForeground", |
|
| 979 | + | "highContrastLight": "gitDecoration.conflictingResourceForeground" |
|
| 980 | + | } |
|
| 981 | + | }, |
|
| 982 | + | { |
|
| 983 | + | "id": "oxygen.resolved", |
|
| 984 | + | "description": "A conflict marked resolved. Defaults to Git's colour; themes can set Oxygen's apart.", |
|
| 985 | + | "defaults": { |
|
| 986 | + | "dark": "gitDecoration.stageModifiedResourceForeground", |
|
| 987 | + | "light": "gitDecoration.stageModifiedResourceForeground", |
|
| 988 | + | "highContrast": "gitDecoration.stageModifiedResourceForeground", |
|
| 989 | + | "highContrastLight": "gitDecoration.stageModifiedResourceForeground" |
|
| 990 | + | } |
|
| 991 | + | } |
|
| 992 | + | ], |
|
| 993 | + | "walkthroughs": [ |
|
| 994 | + | { |
|
| 995 | + | "id": "oxygen.gettingStarted", |
|
| 996 | + | "title": "Get started with Oxygen", |
|
| 997 | + | "description": "Sign, commit, branch without merges, push and see the history of an Oxygen repository.", |
|
| 998 | + | "steps": [ |
|
| 999 | + | { |
|
| 1000 | + | "id": "oxygen.identity", |
|
| 1001 | + | "title": "Create your identity", |
|
| 1002 | + | "description": "Every change you make is signed with a composite key (Ed25519 and ML-DSA-65) that names you. Make it once; every repository uses it.\n[Create Identity](command:oxygen.createIdentity)", |
|
| 1003 | + | "media": { |
|
| 1004 | + | "markdown": "walkthrough/identity.md" |
|
| 1005 | + | }, |
|
| 1006 | + | "completionEvents": [ |
|
| 1007 | + | "onCommand:oxygen.createIdentity" |
|
| 1008 | + | ] |
|
| 1009 | + | }, |
|
| 1010 | + | { |
|
| 1011 | + | "id": "oxygen.repository", |
|
| 1012 | + | "title": "Open or clone a repository", |
|
| 1013 | + | "description": "Clone one from your hub, start a new one here, or open a folder that already is one.\n[Clone Repository](command:oxygen.clone)\n[Initialize Repository](command:oxygen.init)", |
|
| 1014 | + | "media": { |
|
| 1015 | + | "markdown": "walkthrough/repository.md" |
|
| 1016 | + | }, |
|
| 1017 | + | "completionEvents": [ |
|
| 1018 | + | "onContext:oxygen.hasRepository" |
|
| 1019 | + | ] |
|
| 1020 | + | }, |
|
| 1021 | + | { |
|
| 1022 | + | "id": "oxygen.commit", |
|
| 1023 | + | "title": "Commit a change", |
|
| 1024 | + | "description": "Edit a file, write a message in the Source Control box and press Ctrl+Enter. There is no staging area: the change is what you see.\n[Open Source Control](command:workbench.view.scm)", |
|
| 1025 | + | "media": { |
|
| 1026 | + | "markdown": "walkthrough/commit.md" |
|
| 1027 | + | }, |
|
| 1028 | + | "completionEvents": [ |
|
| 1029 | + | "onCommand:oxygen.commit" |
|
| 1030 | + | ] |
|
| 1031 | + | }, |
|
| 1032 | + | { |
|
| 1033 | + | "id": "oxygen.bookmarks", |
|
| 1034 | + | "title": "Bookmarks: branches without merges", |
|
| 1035 | + | "description": "A bookmark names a state. Switch to a new one for a piece of work (take your edits along with *carry*), and merge it back: a merge is just the union of both, with no merge change.\n[Create Bookmark](command:oxygen.createBookmark)", |
|
| 1036 | + | "media": { |
|
| 1037 | + | "markdown": "walkthrough/bookmarks.md" |
|
| 1038 | + | }, |
|
| 1039 | + | "completionEvents": [ |
|
| 1040 | + | "onCommand:oxygen.createBookmark" |
|
| 1041 | + | ] |
|
| 1042 | + | }, |
|
| 1043 | + | { |
|
| 1044 | + | "id": "oxygen.push", |
|
| 1045 | + | "title": "Push to your hub", |
|
| 1046 | + | "description": "Publish your bookmark. The push seals your new changes with one signature and moves the bookmark on the hub, which runs its OxygenFlow pipeline.\n[Push](command:oxygen.push)", |
|
| 1047 | + | "media": { |
|
| 1048 | + | "markdown": "walkthrough/push.md" |
|
| 1049 | + | }, |
|
| 1050 | + | "completionEvents": [ |
|
| 1051 | + | "onCommand:oxygen.push" |
|
| 1052 | + | ] |
|
| 1053 | + | }, |
|
| 1054 | + | { |
|
| 1055 | + | "id": "oxygen.history", |
|
| 1056 | + | "title": "See the history", |
|
| 1057 | + | "description": "Changes form a graph, not a line: independent work runs in its own lane, in the Oxygen colours.\n[Show History](command:oxygen.showHistory)", |
|
| 1058 | + | "media": { |
|
| 1059 | + | "markdown": "walkthrough/history.md" |
|
| 1060 | + | }, |
|
| 1061 | + | "completionEvents": [ |
|
| 1062 | + | "onCommand:oxygen.showHistory" |
|
| 1063 | + | ] |
|
| 1064 | + | } |
|
| 1065 | + | ] |
|
| 794 | 1066 | } |
|
| 795 | - | } |
|
| 1067 | + | ] |
|
| 796 | 1068 | }, |
|
| 797 | 1069 | "scripts": { |
|
| 798 | 1070 | "build": "esbuild src/extension.ts --bundle --platform=node --format=cjs --external:vscode --outfile=dist/extension.js --sourcemap", |
|
| … | 15 unchanged lines |
modified src/decorations.ts +10 −10
| … | 28 unchanged lines |
||
| 29 | 29 | const entry = repo.status.paths.find((p) => p.path === rel); |
|
| 30 | 30 | const conflict = repo.status.conflicts.find((c) => c.path === rel); |
|
| 31 | 31 | if (conflict && !entry) { |
|
| 32 | - | return new vscode.FileDecoration("‼", "conflict", new vscode.ThemeColor("gitDecoration.conflictingResourceForeground")); |
|
| 32 | + | return new vscode.FileDecoration("‼", "conflict", new vscode.ThemeColor("oxygen.conflict")); |
|
| 33 | 33 | } |
|
| 34 | 34 | if (!entry) { |
|
| 35 | 35 | // A directory containing changes gets a dot, like Git. |
|
| 36 | 36 | if (repo.status.paths.some((p) => p.path.startsWith(rel + "/"))) { |
|
| 37 | - | return new vscode.FileDecoration("•", "contains changes", new vscode.ThemeColor("gitDecoration.modifiedResourceForeground")); |
|
| 37 | + | return new vscode.FileDecoration("•", "contains changes", new vscode.ThemeColor("oxygen.modified")); |
|
| 38 | 38 | } |
|
| 39 | 39 | continue; |
|
| 40 | 40 | } |
|
| 41 | 41 | const letters: Record<string, [string, string, string]> = { |
|
| 42 | - | Added: ["A", "added", "gitDecoration.addedResourceForeground"], |
|
| 43 | - | Modified: ["M", "modified", "gitDecoration.modifiedResourceForeground"], |
|
| 44 | - | Removed: ["D", "removed", "gitDecoration.deletedResourceForeground"], |
|
| 45 | - | Renamed: ["R", "renamed", "gitDecoration.renamedResourceForeground"], |
|
| 46 | - | ModeChanged: ["X", "mode changed", "gitDecoration.modifiedResourceForeground"], |
|
| 47 | - | ConflictEdited: ["‼", "conflict markers remain", "gitDecoration.conflictingResourceForeground"], |
|
| 48 | - | Resolved: ["✔", "conflict resolved", "gitDecoration.stageModifiedResourceForeground"], |
|
| 42 | + | Added: ["A", "added", "oxygen.added"], |
|
| 43 | + | Modified: ["M", "modified", "oxygen.modified"], |
|
| 44 | + | Removed: ["D", "removed", "oxygen.removed"], |
|
| 45 | + | Renamed: ["R", "renamed", "oxygen.renamed"], |
|
| 46 | + | ModeChanged: ["X", "mode changed", "oxygen.modified"], |
|
| 47 | + | ConflictEdited: ["‼", "conflict markers remain", "oxygen.conflict"], |
|
| 48 | + | Resolved: ["✔", "conflict resolved", "oxygen.resolved"], |
|
| 49 | 49 | }; |
|
| 50 | - | const [badge, tooltip, color] = letters[entry.status] ?? ["?", entry.status, "gitDecoration.modifiedResourceForeground"]; |
|
| 50 | + | const [badge, tooltip, color] = letters[entry.status] ?? ["?", entry.status, "oxygen.modified"]; |
|
| 51 | 51 | const d = new vscode.FileDecoration(badge, tooltip, new vscode.ThemeColor(color)); |
|
| 52 | 52 | d.propagate = false; |
|
| 53 | 53 | return d; |
|
| … | 10 unchanged lines |
modified src/extension.ts +24 −0
| … | 138 unchanged lines |
||
| 139 | 139 | vscode.commands.registerCommand("oxygen.createTag", (arg?: Repository | vscode.SourceControl) => createTag(arg)), |
|
| 140 | 140 | vscode.commands.registerCommand("oxygen.addWorkspace", (arg?: Repository | vscode.SourceControl) => addWorkspace(arg)), |
|
| 141 | 141 | vscode.commands.registerCommand("oxygen.clone", () => cloneRepository()), |
|
| 142 | + | vscode.commands.registerCommand("oxygen.createIdentity", () => createIdentity()), |
|
| 142 | 143 | vscode.window.registerUriHandler({ handleUri: (uri) => handleUri(uri) }), |
|
| 143 | 144 | vscode.workspace.onDidChangeWorkspaceFolders(() => void discover()), |
|
| 144 | 145 | vscode.workspace.onDidChangeConfiguration((e) => { |
|
| … | 715 unchanged lines |
||
| 860 | 861 | } |
|
| 861 | 862 | } catch (e) { |
|
| 862 | 863 | showError(e); |
|
| 864 | + | } |
|
| 865 | + | } |
|
| 866 | + | ||
| 867 | + | /** Make a signing identity (oxy identity create): a composite key in your |
|
| 868 | + | * user key directory, and the author every repository then signs with. */ |
|
| 869 | + | async function createIdentity(): Promise<void> { |
|
| 870 | + | const name = await vscode.window.showInputBox({ prompt: "Your name, as changes will show it", ignoreFocusOut: true, validateInput: (v) => (v.trim() ? undefined : "A name") }); |
|
| 871 | + | if (!name) { |
|
| 872 | + | return; |
|
| 873 | + | } |
|
| 874 | + | const email = await vscode.window.showInputBox({ prompt: "Your email", ignoreFocusOut: true, validateInput: (v) => (/.+@.+/.test(v.trim()) ? undefined : "An email address") }); |
|
| 875 | + | if (!email) { |
|
| 876 | + | return; |
|
| 877 | + | } |
|
| 878 | + | const home = process.env.HOME ?? process.cwd(); |
|
| 879 | + | const r = await oxy.run(home, ["--plain", "identity", "create", "--name", name.trim(), "--email", email.trim()]); |
|
| 880 | + | if (r.code === 0) { |
|
| 881 | + | vscode.window.showInformationMessage(`Oxygen: your identity is ready: ${name.trim()} <${email.trim()}>. Changes you commit and push are signed with it.`); |
|
| 882 | + | for (const repo of repositories.values()) { |
|
| 883 | + | void repo.refresh(); |
|
| 884 | + | } |
|
| 885 | + | } else { |
|
| 886 | + | vscode.window.showErrorMessage(`Oxygen: could not create an identity: ${r.stderr.trim() || r.stdout.toString("utf8").trim()}`); |
|
| 863 | 887 | } |
|
| 864 | 888 | } |
|
| 865 | 889 | ||
| … | 738 unchanged lines |
modified src/history.ts +20 −9
| … | 117 unchanged lines |
||
| 118 | 118 | } |
|
| 119 | 119 | const x2 = lanes.get(d)! * laneW + 10; |
|
| 120 | 120 | const y2 = j * rowH + rowH / 2; |
|
| 121 | - | edges.push(`<path d="M${x},${y} C${x},${y + rowH / 2} ${x2},${y2 - rowH / 2} ${x2},${y2}" />`); |
|
| 121 | + | edges.push(`<path class="l${(lanes.get(d)! % 8) + 1}" d="M${x},${y} C${x},${y + rowH / 2} ${x2},${y2 - rowH / 2} ${x2},${y2}" />`); |
|
| 122 | 122 | } |
|
| 123 | 123 | const cls = c.working_copy ? "wc" : c.head ? "head" : "node"; |
|
| 124 | - | nodes.push(`<circle class="${cls}" cx="${x}" cy="${y}" r="5" />`); |
|
| 124 | + | const lane = `l${(lanes.get(c.hash)! % 8) + 1}`; |
|
| 125 | + | nodes.push(`<circle class="${cls} ${lane}" cx="${x}" cy="${y}" r="${c.head ? 6 : 5}" />`); |
|
| 125 | 126 | }); |
|
| 126 | 127 | const rows = changes |
|
| 127 | 128 | .map((c, i) => { |
|
| 128 | 129 | const subject = c.message.split("\n")[0] || (c.working_copy ? "(working copy)" : "(no description)"); |
|
| 129 | - | const sig = c.working_copy ? "" : c.signature?.status === "signed" || c.signature?.status === "sealed" ? (c.signature.revoked ? "⚠" : "✔") : c.signature?.status === "invalid" ? "✖" : "·"; |
|
| 130 | + | const sig = c.working_copy ? "" : c.signature?.status === "signed" || c.signature?.status === "sealed" ? (c.signature.revoked ? "⚠" : c.signature.status === "sealed" ? "◈" : "✔") : c.signature?.status === "invalid" ? "✖" : "·"; |
|
| 131 | + | const sigClass = c.signature?.revoked ? "sig-revoked" : `sig-${c.signature?.status ?? "none"}`; |
|
| 130 | 132 | // Right-click: split, squash and rebase (webview/context menus in package.json). |
|
| 131 | 133 | const ctx = c.working_copy ? "" : ` data-vscode-context='${escape(JSON.stringify({ webviewSection: "change", root: repo.root, hash: c.hash, oxygenHead: c.head, oxygenHasDeps: c.deps.length > 0, preventDefaultContextMenuItems: true }))}'`; |
|
| 132 | 134 | return `<div class="row${c.working_copy ? " wc" : ""}" style="top:${i * rowH}px" data-hash="${c.hash}" title="${escape(c.message)}"${ctx}> |
|
| 133 | 135 | <span class="id">${c.change_id.slice(0, 8)}</span> |
|
| 134 | 136 | <span class="subject">${escape(subject)}</span> |
|
| 135 | - | <span class="meta">${escape(c.author)} · ${timeAgo(c.when_ms)} <span class="sig" title="${escape(c.signature?.status ?? "")}">${sig}</span></span> |
|
| 137 | + | <span class="meta">${escape(c.author)} · ${timeAgo(c.when_ms)} <span class="sig ${sigClass}" title="${escape(c.signature?.status ?? "")}">${sig}</span></span> |
|
| 136 | 138 | </div>`; |
|
| 137 | 139 | }) |
|
| 138 | 140 | .join(""); |
|
| … | 5 unchanged lines |
||
| 144 | 146 | h2 button { background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); border: none; padding: 2px 8px; cursor: pointer; } |
|
| 145 | 147 | .wrap { position: relative; } |
|
| 146 | 148 | svg { position: absolute; left: 0; top: 0; } |
|
| 147 | - | svg path { fill: none; stroke: var(--vscode-editorCodeLens-foreground); stroke-width: 1.5; } |
|
| 148 | - | svg circle.node { fill: var(--vscode-editorCodeLens-foreground); } |
|
| 149 | - | svg circle.head { fill: var(--vscode-charts-green); } |
|
| 150 | - | svg circle.wc { fill: var(--vscode-charts-blue); stroke: var(--vscode-charts-blue); stroke-width: 2; fill-opacity: .3; } |
|
| 149 | + | /* Lanes in the Oxygen mark's colours (themeable: oxygen.lane1…8). */ |
|
| 150 | + | ${[1, 2, 3, 4, 5, 6, 7, 8].map((n) => `.l${n} { --lane: var(--vscode-oxygen-lane${n}); }`).join("\n ")} |
|
| 151 | + | svg path { fill: none; stroke: var(--lane); stroke-width: 1.6; opacity: .85; } |
|
| 152 | + | svg circle.node { fill: var(--lane); } |
|
| 153 | + | svg circle.head { fill: var(--vscode-oxygen-head); stroke: var(--vscode-editor-background); stroke-width: 1.5; filter: drop-shadow(0 0 3px var(--vscode-oxygen-head)); } |
|
| 154 | + | svg circle.wc { fill: var(--vscode-oxygen-workingCopy); fill-opacity: .25; stroke: var(--vscode-oxygen-workingCopy); stroke-width: 2; transform-box: fill-box; transform-origin: center; animation: breathe 2.4s ease-in-out infinite; } |
|
| 155 | + | @keyframes breathe { 0%, 100% { transform: scale(1); fill-opacity: .25; } 50% { transform: scale(1.25); fill-opacity: .55; } } |
|
| 156 | + | @media (prefers-reduced-motion: reduce) { svg circle.wc { animation: none; } } |
|
| 157 | + | h2::after { content: ""; flex: 1; height: 2px; margin-left: 6px; background: linear-gradient(90deg, var(--vscode-oxygen-lane1), var(--vscode-oxygen-lane2), transparent); border-radius: 1px; } |
|
| 158 | + | .sig-signed { color: var(--vscode-oxygen-signed); } |
|
| 159 | + | .sig-sealed { color: var(--vscode-oxygen-sealed); } |
|
| 160 | + | .sig-invalid, .sig-revoked { color: var(--vscode-oxygen-invalid); } |
|
| 161 | + | .row:not(.wc):hover .id { text-decoration: underline; } |
|
| 151 | 162 | .rows { position: relative; margin-left: ${graphW}px; height: ${changes.length * rowH}px; } |
|
| 152 | 163 | .row { position: absolute; left: 0; right: 0; height: ${rowH}px; line-height: ${rowH}px; display: flex; gap: 10px; cursor: pointer; white-space: nowrap; overflow: hidden; } |
|
| 153 | 164 | .row:hover { background: var(--vscode-list-hoverBackground); } |
|
| 154 | 165 | .row.wc { color: var(--vscode-descriptionForeground); font-style: italic; cursor: default; } |
|
| 155 | - | .id { color: var(--vscode-charts-blue); font-family: var(--vscode-editor-font-family); } |
|
| 166 | + | .id { color: var(--vscode-oxygen-lane1); font-family: var(--vscode-editor-font-family); } |
|
| 156 | 167 | .subject { flex: 1; overflow: hidden; text-overflow: ellipsis; } |
|
| 157 | 168 | .meta { color: var(--vscode-descriptionForeground); font-size: 12px; } |
|
| 158 | 169 | .sig { margin-left: 4px; } |
|
| … | 22 unchanged lines |
modified src/repository.ts +7 −7
| … | 28 unchanged lines |
||
| 29 | 29 | }; |
|
| 30 | 30 | ||
| 31 | 31 | const STATUS_COLOR: Record<PathStatus, string> = { |
|
| 32 | - | Added: "gitDecoration.addedResourceForeground", |
|
| 33 | - | Modified: "gitDecoration.modifiedResourceForeground", |
|
| 34 | - | Removed: "gitDecoration.deletedResourceForeground", |
|
| 35 | - | Renamed: "gitDecoration.renamedResourceForeground", |
|
| 36 | - | ModeChanged: "gitDecoration.modifiedResourceForeground", |
|
| 37 | - | ConflictEdited: "gitDecoration.conflictingResourceForeground", |
|
| 38 | - | Resolved: "gitDecoration.stageModifiedResourceForeground", |
|
| 32 | + | Added: "oxygen.added", |
|
| 33 | + | Modified: "oxygen.modified", |
|
| 34 | + | Removed: "oxygen.removed", |
|
| 35 | + | Renamed: "oxygen.renamed", |
|
| 36 | + | ModeChanged: "oxygen.modified", |
|
| 37 | + | ConflictEdited: "oxygen.conflict", |
|
| 38 | + | Resolved: "oxygen.resolved", |
|
| 39 | 39 | }; |
|
| 40 | 40 | ||
| 41 | 41 | export class OxyResourceState implements vscode.SourceControlResourceState { |
|
| … | 253 unchanged lines |